Skip to content

Commit

Permalink
Narrow generic conditional and indexed access return types when check…
Browse files Browse the repository at this point in the history
…ing return statements (#56941)
  • Loading branch information
gabritto authored Nov 6, 2024
1 parent 5e2e321 commit 30979c2
Show file tree
Hide file tree
Showing 41 changed files with 11,876 additions and 42 deletions.
14 changes: 14 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
var preSwitchCaseFlow: FlowNode | undefined;
var activeLabelList: ActiveLabel | undefined;
var hasExplicitReturn: boolean;
var inReturnPosition: boolean;
var hasFlowEffects: boolean;

// state used for emit helpers
Expand Down Expand Up @@ -622,6 +623,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
currentExceptionTarget = undefined;
activeLabelList = undefined;
hasExplicitReturn = false;
inReturnPosition = false;
hasFlowEffects = false;
inAssignmentPattern = false;
emitFlags = NodeFlags.None;
Expand Down Expand Up @@ -967,7 +969,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
const saveContainer = container;
const saveThisParentContainer = thisParentContainer;
const savedBlockScopeContainer = blockScopeContainer;
const savedInReturnPosition = inReturnPosition;

if (node.kind === SyntaxKind.ArrowFunction && node.body.kind !== SyntaxKind.Block) inReturnPosition = true;
// Depending on what kind of node this is, we may have to adjust the current container
// and block-container. If the current node is a container, then it is automatically
// considered the current block-container as well. Also, for containers that we know
Expand Down Expand Up @@ -1071,6 +1075,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
bindChildren(node);
}

inReturnPosition = savedInReturnPosition;
container = saveContainer;
thisParentContainer = saveThisParentContainer;
blockScopeContainer = savedBlockScopeContainer;
Expand Down Expand Up @@ -1571,7 +1576,10 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
}

function bindReturnOrThrow(node: ReturnStatement | ThrowStatement): void {
const savedInReturnPosition = inReturnPosition;
inReturnPosition = true;
bind(node.expression);
inReturnPosition = savedInReturnPosition;
if (node.kind === SyntaxKind.ReturnStatement) {
hasExplicitReturn = true;
if (currentReturnTarget) {
Expand Down Expand Up @@ -2016,10 +2024,16 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
hasFlowEffects = false;
bindCondition(node.condition, trueLabel, falseLabel);
currentFlow = finishFlowLabel(trueLabel);
if (inReturnPosition) {
node.flowNodeWhenTrue = currentFlow;
}
bind(node.questionToken);
bind(node.whenTrue);
addAntecedent(postExpressionLabel, currentFlow);
currentFlow = finishFlowLabel(falseLabel);
if (inReturnPosition) {
node.flowNodeWhenFalse = currentFlow;
}
bind(node.colonToken);
bind(node.whenFalse);
addAntecedent(postExpressionLabel, currentFlow);
Expand Down
449 changes: 409 additions & 40 deletions src/compiler/checker.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3481,6 +3481,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
propagateChildFlags(node.whenTrue) |
propagateChildFlags(node.colonToken) |
propagateChildFlags(node.whenFalse);
node.flowNodeWhenFalse = undefined;
node.flowNodeWhenTrue = undefined;
return node;
}

Expand Down
15 changes: 13 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,10 @@ export interface ConditionalExpression extends Expression {
readonly whenTrue: Expression;
readonly colonToken: ColonToken;
readonly whenFalse: Expression;
/** @internal*/
flowNodeWhenTrue: FlowNode | undefined;
/** @internal */
flowNodeWhenFalse: FlowNode | undefined;
}

export type FunctionBody = Block;
Expand Down Expand Up @@ -6240,6 +6244,7 @@ export interface NodeLinks {
decoratorSignature?: Signature; // Signature for decorator as if invoked by the runtime.
spreadIndices?: { first: number | undefined, last: number | undefined }; // Indices of first and last spread elements in array literal
parameterInitializerContainsUndefined?: boolean; // True if this is a parameter declaration whose type annotation contains "undefined".
contextualReturnType?: Type; // If the node is a return statement's expression, then this is the contextual return type.
fakeScopeForSignatureDeclaration?: "params" | "typeParams"; // If present, this is a fake scope injected into an enclosing declaration chain.
assertionExpressionType?: Type; // Cached type of the expression of a type assertion
potentialThisCollisions?: Node[];
Expand Down Expand Up @@ -6506,6 +6511,8 @@ export const enum ObjectFlags {
IsGenericIndexType = 1 << 23, // Union or intersection contains generic index type
/** @internal */
IsGenericType = IsGenericObjectType | IsGenericIndexType,
/** @internal */
IsNarrowingType = 1 << 24, // Substitution type that comes from type narrowing

// Flags that require TypeFlags.Union
/** @internal */
Expand Down Expand Up @@ -6905,12 +6912,16 @@ export interface StringMappingType extends InstantiableType {
}

// Type parameter substitution (TypeFlags.Substitution)
// Substitution types are created for type parameters or indexed access types that occur in the
// - Substitution types are created for type parameters or indexed access types that occur in the
// true branch of a conditional type. For example, in 'T extends string ? Foo<T> : Bar<T>', the
// reference to T in Foo<T> is resolved as a substitution type that substitutes 'string & T' for T.
// Thus, if Foo has a 'string' constraint on its type parameter, T will satisfy it.
// Substitution type are also created for NoInfer<T> types. Those are represented as substitution
// - Substitution types are also created for NoInfer<T> types. Those are represented as substitution
// types where the constraint is type 'unknown' (which is never generated for the case above).
// - Substitution types are also created for return type narrowing:
// if a type parameter `T` is linked to a parameter `x` and `x`'s narrowed type is `S`,
// we represent that with a substitution type with base `T` and constraint `S`.
// The resulting substitution type has `ObjectFlags.IsNarrowedType` set.
export interface SubstitutionType extends InstantiableType {
objectFlags: ObjectFlags;
baseType: Type; // Target type
Expand Down
13 changes: 13 additions & 0 deletions tests/baselines/reference/arrowExpressionJs.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//// [tests/cases/compiler/arrowExpressionJs.ts] ////

=== mytest.js ===
/**
* @template T
* @param {T|undefined} value value or not
* @returns {T} result value
*/
const cloneObjectGood = value => /** @type {T} */({ ...value });
>cloneObjectGood : Symbol(cloneObjectGood, Decl(mytest.js, 5, 5))
>value : Symbol(value, Decl(mytest.js, 5, 23))
>value : Symbol(value, Decl(mytest.js, 5, 23))

22 changes: 22 additions & 0 deletions tests/baselines/reference/arrowExpressionJs.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//// [tests/cases/compiler/arrowExpressionJs.ts] ////

=== mytest.js ===
/**
* @template T
* @param {T|undefined} value value or not
* @returns {T} result value
*/
const cloneObjectGood = value => /** @type {T} */({ ...value });
>cloneObjectGood : <T>(value: T | undefined) => T
> : ^ ^^ ^^ ^^^^^
>value => /** @type {T} */({ ...value }) : <T>(value: T | undefined) => T
> : ^ ^^ ^^ ^^^^^
>value : T | undefined
> : ^^^^^^^^^^^^^
>({ ...value }) : T
> : ^
>{ ...value } : {}
> : ^^
>value : T | undefined
> : ^^^^^^^^^^^^^

Loading

0 comments on commit 30979c2

Please sign in to comment.