From 86f50c6030fc284c68a726de95c7f51891a42c1f Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 30 Dec 2023 18:15:57 -1000 Subject: [PATCH 01/13] Preserve narrowings in closures created past last assignment --- src/compiler/checker.ts | 86 +++++++++++++++++++++++++++++++-------- src/compiler/types.ts | 2 +- src/compiler/utilities.ts | 13 +----- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3d4f288f1293b..fafbb59ff84a1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -253,6 +253,7 @@ import { getContainingClassExcludingClassDecorators, getContainingClassStaticBlock, getContainingFunction, + getContainingFunctionDeclaration, getContainingFunctionOrClassStaticBlock, getDeclarationModifierFlagsFromSymbol, getDeclarationOfKind, @@ -667,7 +668,6 @@ import { isOutermostOptionalChain, isParameter, isParameterDeclaration, - isParameterOrCatchClauseVariable, isParameterPropertyDeclaration, isParenthesizedExpression, isParenthesizedTypeNode, @@ -27451,7 +27451,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.Identifier: if (!isThisInTypeQuery(node)) { const symbol = getResolvedSymbol(node as Identifier); - return isConstantVariable(symbol) || isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol); + return isConstantVariable(symbol) || isParameterOrLetOrCatchVariable(symbol) && !isSymbolAssigned(symbol); } break; case SyntaxKind.PropertyAccessExpression: @@ -28728,10 +28728,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Check if a parameter or catch variable is assigned anywhere function isSymbolAssigned(symbol: Symbol) { - if (!symbol.valueDeclaration) { + return !isPastLastAssignment(symbol, /*location*/ undefined); + } + + // Return true if there are no assignments to the given symbol or if the given location + // is past the last assignment to the symbol. + function isPastLastAssignment(symbol: Symbol, location: Node | undefined) { + const parent = findAncestor(symbol.valueDeclaration, isAssignmentMarkingContainer); + if (!parent) { return false; } - const parent = getRootDeclaration(symbol.valueDeclaration).parent; const links = getNodeLinks(parent); if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) { links.flags |= NodeCheckFlags.AssignmentsMarked; @@ -28739,7 +28745,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { markNodeAssignments(parent); } } - return symbol.isAssigned || false; + return !symbol.lastAssignmentPos || location && symbol.lastAssignmentPos < location.pos; } // Check if a parameter or catch variable (or their bindings elements) is assigned anywhere @@ -28757,27 +28763,71 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function hasParentWithAssignmentsMarked(node: Node) { - return !!findAncestor(node.parent, node => (isFunctionLike(node) || isCatchClause(node)) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + return !!findAncestor(node.parent, node => isAssignmentMarkingContainer(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + } + + function isAssignmentMarkingContainer(node: Node) { + return isFunctionLikeDeclaration(node) || isBlock(node) || isSourceFile(node) || isForStatement(node) || isForInOrOfStatement(node) || isCatchClause(node); } + // For all assignments within the given root node, record the last assignment source position for all + // referenced parameters, let variables, and catch variables. When assignments occur in nested functions, + // record Number.MAX_VALUE as the assignment position. When assignments occur in compound statements, + // record the ending source position of the compound statement as the assignment position (this is more + // conservative than full control flow analysis, but requires only a single walk over the AST). function markNodeAssignments(node: Node) { - if (node.kind === SyntaxKind.Identifier) { - if (isAssignmentTarget(node)) { - const symbol = getResolvedSymbol(node as Identifier); - if (isParameterOrCatchClauseVariable(symbol)) { - symbol.isAssigned = true; - } + let statementEnd: number | undefined; + markAssignments(node); + function markAssignments(node: Node) { + switch (node.kind) { + case SyntaxKind.Identifier: + if (isAssignmentTarget(node)) { + const symbol = getResolvedSymbol(node as Identifier); + if (isParameterOrLetOrCatchVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { + const referencingFunction = getContainingFunctionDeclaration(node) || getSourceFileOfNode(node); + const declaringFunction = getContainingFunctionDeclaration(symbol.valueDeclaration!) || getSourceFileOfNode(node); + symbol.lastAssignmentPos = referencingFunction === declaringFunction ? statementEnd ?? node.pos : Number.MAX_VALUE; + } + } + return; + case SyntaxKind.VariableStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.ClassDeclaration: + const saveStatementEnd = statementEnd; + statementEnd ??= node.end; + forEachChild(node, markAssignments); + statementEnd = saveStatementEnd; + return; + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + return; + } + if (!isTypeNode(node)) { + forEachChild(node, markAssignments); } } - else { - forEachChild(node, markNodeAssignments); - } - } + } function isConstantVariable(symbol: Symbol) { return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0; } + function isParameterOrLetOrCatchVariable(symbol: Symbol) { + const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); + return !!(declaration && (isParameter(declaration) || + isVariableDeclaration(declaration) && (declaration.parent.kind === SyntaxKind.CatchClause || declaration.parent.flags & NodeFlags.Let))); + } + function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean { const links = getNodeLinks(declaration); @@ -29134,7 +29184,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { while ( flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(localOrExportSymbol)) + (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || (isParameterOrLetOrCatchVariable(localOrExportSymbol)) && isPastLastAssignment(localOrExportSymbol, node)) ) { flowContainer = getControlFlowContainer(flowContainer); } @@ -29143,7 +29193,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // declaration container are the same). const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) || type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 || - isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || + isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || node.parent.kind === SyntaxKind.NonNullExpression || declaration.kind === SyntaxKind.VariableDeclaration && (declaration as VariableDeclaration).exclamationToken || declaration.flags & NodeFlags.Ambient; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e56bba5ab4859..8bd4d4ea26249 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5826,7 +5826,7 @@ export interface Symbol { /** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums /** @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter. /** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol? - /** @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments + /** @internal */ lastAssignmentPos?: number; // Last node that assigns value to symbol /** @internal */ assignmentDeclarationMembers?: Map; // detected late-bound assignment declarations associated with the symbol } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 398b7f1e61305..412df8e917685 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8178,7 +8178,7 @@ function Symbol(this: Symbol, flags: SymbolFlags, name: __String) { this.exportSymbol = undefined; this.constEnumOnlyModule = undefined; this.isReferenced = undefined; - this.isAssigned = undefined; + this.lastAssignmentPos = undefined; (this as any).links = undefined; // used by TransientSymbol } @@ -10346,17 +10346,6 @@ export function isInfinityOrNaNString(name: string | __String): boolean { return name === "Infinity" || name === "-Infinity" || name === "NaN"; } -/** @internal */ -export function isCatchClauseVariableDeclaration(node: Node) { - return node.kind === SyntaxKind.VariableDeclaration && node.parent.kind === SyntaxKind.CatchClause; -} - -/** @internal */ -export function isParameterOrCatchClauseVariable(symbol: Symbol) { - const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); - return !!declaration && (isParameter(declaration) || isCatchClauseVariableDeclaration(declaration)); -} - /** @internal */ export function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction; From f698783e61bbb2ce7ca9e88e74c1e4f1296e8490 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 30 Dec 2023 18:20:29 -1000 Subject: [PATCH 02/13] Accept new baselines --- .../reference/controlFlowAliasing.errors.txt | 12 +----------- .../baselines/reference/controlFlowAliasing.symbols | 4 ++++ tests/baselines/reference/controlFlowAliasing.types | 12 ++++++------ .../reference/implicitConstParameters.errors.txt | 5 +---- .../reference/implicitConstParameters.types | 2 +- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/tests/baselines/reference/controlFlowAliasing.errors.txt b/tests/baselines/reference/controlFlowAliasing.errors.txt index c062509c6b643..25a8858c4ca4f 100644 --- a/tests/baselines/reference/controlFlowAliasing.errors.txt +++ b/tests/baselines/reference/controlFlowAliasing.errors.txt @@ -14,10 +14,6 @@ controlFlowAliasing.ts(112,13): error TS2339: Property 'foo' does not exist on t Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. controlFlowAliasing.ts(115,13): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. -controlFlowAliasing.ts(134,13): error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. - Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. -controlFlowAliasing.ts(137,13): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. - Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. controlFlowAliasing.ts(154,19): error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. controlFlowAliasing.ts(157,19): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. @@ -28,7 +24,7 @@ controlFlowAliasing.ts(280,5): error TS2448: Block-scoped variable 'a' used befo controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being assigned. -==== controlFlowAliasing.ts (15 errors) ==== +==== controlFlowAliasing.ts (13 errors) ==== // Narrowing by aliased conditional expressions function f10(x: string | number) { @@ -187,15 +183,9 @@ controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being a const isFoo = obj.kind === 'foo'; if (isFoo) { obj.foo; // Not narrowed because obj is mutable - ~~~ -!!! error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. -!!! error TS2339: Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. } else { obj.bar; // Not narrowed because obj is mutable - ~~~ -!!! error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. -!!! error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. } } diff --git a/tests/baselines/reference/controlFlowAliasing.symbols b/tests/baselines/reference/controlFlowAliasing.symbols index b0de1519352c4..802b6246fa2b6 100644 --- a/tests/baselines/reference/controlFlowAliasing.symbols +++ b/tests/baselines/reference/controlFlowAliasing.symbols @@ -371,11 +371,15 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { >isFoo : Symbol(isFoo, Decl(controlFlowAliasing.ts, 131, 9)) obj.foo; // Not narrowed because obj is mutable +>obj.foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) +>foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) } else { obj.bar; // Not narrowed because obj is mutable +>obj.bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) +>bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) } } diff --git a/tests/baselines/reference/controlFlowAliasing.types b/tests/baselines/reference/controlFlowAliasing.types index b93de24a3aaf9..6c0e6bd8721e6 100644 --- a/tests/baselines/reference/controlFlowAliasing.types +++ b/tests/baselines/reference/controlFlowAliasing.types @@ -441,15 +441,15 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { >isFoo : boolean obj.foo; // Not narrowed because obj is mutable ->obj.foo : any ->obj : { kind: "foo"; foo: string; } | { kind: "bar"; bar: number; } ->foo : any +>obj.foo : string +>obj : { kind: "foo"; foo: string; } +>foo : string } else { obj.bar; // Not narrowed because obj is mutable ->obj.bar : any ->obj : { kind: "foo"; foo: string; } | { kind: "bar"; bar: number; } ->bar : any +>obj.bar : number +>obj : { kind: "bar"; bar: number; } +>bar : number } } diff --git a/tests/baselines/reference/implicitConstParameters.errors.txt b/tests/baselines/reference/implicitConstParameters.errors.txt index ffc18dbc49234..e1a85b51d9d20 100644 --- a/tests/baselines/reference/implicitConstParameters.errors.txt +++ b/tests/baselines/reference/implicitConstParameters.errors.txt @@ -1,8 +1,7 @@ -implicitConstParameters.ts(38,27): error TS18048: 'x' is possibly 'undefined'. implicitConstParameters.ts(44,27): error TS18048: 'x' is possibly 'undefined'. -==== implicitConstParameters.ts (2 errors) ==== +==== implicitConstParameters.ts (1 errors) ==== function doSomething(cb: () => void) { cb(); } @@ -41,8 +40,6 @@ implicitConstParameters.ts(44,27): error TS18048: 'x' is possibly 'undefined'. x = "abc"; // causes x to be considered non-const if (x) { doSomething(() => x.length); - ~ -!!! error TS18048: 'x' is possibly 'undefined'. } } diff --git a/tests/baselines/reference/implicitConstParameters.types b/tests/baselines/reference/implicitConstParameters.types index ef602acb7e11c..9cace5f5ad323 100644 --- a/tests/baselines/reference/implicitConstParameters.types +++ b/tests/baselines/reference/implicitConstParameters.types @@ -116,7 +116,7 @@ function f4(x: string | undefined) { >doSomething : (cb: () => void) => void >() => x.length : () => number >x.length : number ->x : string | undefined +>x : string >length : number } } From bdbc3884e2e9ca8d7ef8af7b5d61bb5ae8e35926 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 30 Dec 2023 18:20:39 -1000 Subject: [PATCH 03/13] Add tests --- .../narrowingPastLastAssignment.errors.txt | 123 ++++++ .../narrowingPastLastAssignment.symbols | 274 ++++++++++++++ .../narrowingPastLastAssignment.types | 354 ++++++++++++++++++ .../compiler/narrowingPastLastAssignment.ts | 117 ++++++ 4 files changed, 868 insertions(+) create mode 100644 tests/baselines/reference/narrowingPastLastAssignment.errors.txt create mode 100644 tests/baselines/reference/narrowingPastLastAssignment.symbols create mode 100644 tests/baselines/reference/narrowingPastLastAssignment.types create mode 100644 tests/cases/compiler/narrowingPastLastAssignment.ts diff --git a/tests/baselines/reference/narrowingPastLastAssignment.errors.txt b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt new file mode 100644 index 0000000000000..7590912410bde --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt @@ -0,0 +1,123 @@ +narrowingPastLastAssignment.ts(67,9): error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. +narrowingPastLastAssignment.ts(69,20): error TS7005: Variable 'x' implicitly has an 'any' type. + + +==== narrowingPastLastAssignment.ts (2 errors) ==== + function action(f: Function) {} + + // Narrowings are preserved in closures created past last assignment + + function f1(x: string | number) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* number */ }); + } + + // Narrowings are not preserved in inner function and class declarations (due to hoisting) + + function f2() { + let x: string | number; + x = 42; + let a = () => { x /* number */ }; + let f = function() { x /* number */ }; + let C = class { + foo() { x /* number */ } + }; + let o = { + foo() { x /* number */ } + }; + function g() { x /* string | number */ } + class A { + foo() { x /* string | number */ } + } + } + + // Narrowings are not preserved when assignments occur in inner functions + + function f3(x: string | number) { + action(() => { x = "abc" }); + x = 42; + action(() => { x /* string | number */ }); + } + + // Assignment effects in compoud statements extend to the entire statement + + function f4(cond: () => boolean) { + let x: string | number = 0; + while (cond()) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); + } + + function f5(x: string | number, cond: () => boolean) { + if (cond()) { + x = 1; + action(() => { x /* string | number */ }); + } + else { + x = 2; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); + } + + // Implicit any variables have a known type following last assignment + + function f6() { + let x; + ~ +!!! error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. + x = "abc"; + action(() => { x }); // Error + ~ +!!! error TS7005: Variable 'x' implicitly has an 'any' type. + x = 42; + action(() => { x /* number */ }); + } + + // Narrowings on catch variables are preserved past last assignment + + function f7() { + try { + } + catch (e) { + if (e instanceof Error) { + let f = () => { e /* Error */ } + } + } + } + + // Repros from #35124 + + function f10() { + let i: number | undefined; + i = 0; + return (k: number) => k === i + 1; + } + + function makeAdder(n?: number) { + n ??= 0; + return (m: number) => n + m; + } + + function f11() { + let r; + r = "b"; + () => r; + } + + // Repro from #52104 + + const fooMap: Map> = new Map() + const values = [1, 2, 3, 4, 5]; + let foo = fooMap.get("a"); + if (foo == null) { + foo = []; + } + values.forEach(v => foo.push(v)); + \ No newline at end of file diff --git a/tests/baselines/reference/narrowingPastLastAssignment.symbols b/tests/baselines/reference/narrowingPastLastAssignment.symbols new file mode 100644 index 0000000000000..006603f0d010d --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.symbols @@ -0,0 +1,274 @@ +//// [tests/cases/compiler/narrowingPastLastAssignment.ts] //// + +=== narrowingPastLastAssignment.ts === +function action(f: Function) {} +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 0, 16)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.decorators.d.ts, --, --)) + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { +>f1 : Symbol(f1, Decl(narrowingPastLastAssignment.ts, 0, 31)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { +>f2 : Symbol(f2, Decl(narrowingPastLastAssignment.ts, 9, 1)) + + let x: string | number; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let a = () => { x /* number */ }; +>a : Symbol(a, Decl(narrowingPastLastAssignment.ts, 16, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let f = function() { x /* number */ }; +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 17, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let C = class { +>C : Symbol(C, Decl(narrowingPastLastAssignment.ts, 18, 7)) + + foo() { x /* number */ } +>foo : Symbol(C.foo, Decl(narrowingPastLastAssignment.ts, 18, 19)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + }; + let o = { +>o : Symbol(o, Decl(narrowingPastLastAssignment.ts, 21, 7)) + + foo() { x /* number */ } +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 21, 13)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + }; + function g() { x /* string | number */ } +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 23, 6)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + class A { +>A : Symbol(A, Decl(narrowingPastLastAssignment.ts, 24, 44)) + + foo() { x /* string | number */ } +>foo : Symbol(A.foo, Decl(narrowingPastLastAssignment.ts, 25, 13)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { +>f3 : Symbol(f3, Decl(narrowingPastLastAssignment.ts, 28, 1)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + action(() => { x = "abc" }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { +>f4 : Symbol(f4, Decl(narrowingPastLastAssignment.ts, 36, 1)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 40, 12)) + + let x: string | number = 0; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + while (cond()) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 40, 12)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + } + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) +} + +function f5(x: string | number, cond: () => boolean) { +>f5 : Symbol(f5, Decl(narrowingPastLastAssignment.ts, 49, 1)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 51, 31)) + + if (cond()) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 51, 31)) + + x = 1; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + } + else { + x = 2; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + } + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) +} + +// Implicit any variables have a known type following last assignment + +function f6() { +>f6 : Symbol(f6, Decl(narrowingPastLastAssignment.ts, 61, 1)) + + let x; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) + + action(() => { x }); // Error +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { +>f7 : Symbol(f7, Decl(narrowingPastLastAssignment.ts, 71, 1)) + + try { + } + catch (e) { +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) + + if (e instanceof Error) { +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2022.error.d.ts, --, --)) + + let f = () => { e /* Error */ } +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 80, 15)) +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) + } + } +} + +// Repros from #35124 + +function f10() { +>f10 : Symbol(f10, Decl(narrowingPastLastAssignment.ts, 83, 1)) + + let i: number | undefined; +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) + + i = 0; +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) + + return (k: number) => k === i + 1; +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 90, 12)) +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 90, 12)) +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) +} + +function makeAdder(n?: number) { +>makeAdder : Symbol(makeAdder, Decl(narrowingPastLastAssignment.ts, 91, 1)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) + + n ??= 0; +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) + + return (m: number) => n + m; +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 95, 12)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 95, 12)) +} + +function f11() { +>f11 : Symbol(f11, Decl(narrowingPastLastAssignment.ts, 96, 1)) + + let r; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) + + r = "b"; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) + + () => r; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) +} + +// Repro from #52104 + +const fooMap: Map> = new Map() +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 106, 5)) +>Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) + +const values = [1, 2, 3, 4, 5]; +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 107, 5)) + +let foo = fooMap.get("a"); +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) +>fooMap.get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 106, 5)) +>get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) + +if (foo == null) { +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) + + foo = []; +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) +} +values.forEach(v => foo.push(v)); +>values.forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 107, 5)) +>forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 112, 15)) +>foo.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) +>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 112, 15)) + diff --git a/tests/baselines/reference/narrowingPastLastAssignment.types b/tests/baselines/reference/narrowingPastLastAssignment.types new file mode 100644 index 0000000000000..99cde6a0fd2e2 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.types @@ -0,0 +1,354 @@ +//// [tests/cases/compiler/narrowingPastLastAssignment.ts] //// + +=== narrowingPastLastAssignment.ts === +function action(f: Function) {} +>action : (f: Function) => void +>f : Function + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { +>f1 : (x: string | number) => void +>x : string | number + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { +>f2 : () => void + + let x: string | number; +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + let a = () => { x /* number */ }; +>a : () => void +>() => { x /* number */ } : () => void +>x : number + + let f = function() { x /* number */ }; +>f : () => void +>function() { x /* number */ } : () => void +>x : number + + let C = class { +>C : typeof C +>class { foo() { x /* number */ } } : typeof C + + foo() { x /* number */ } +>foo : () => void +>x : number + + }; + let o = { +>o : { foo(): void; } +>{ foo() { x /* number */ } } : { foo(): void; } + + foo() { x /* number */ } +>foo : () => void +>x : number + + }; + function g() { x /* string | number */ } +>g : () => void +>x : string | number + + class A { +>A : A + + foo() { x /* string | number */ } +>foo : () => void +>x : string | number + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { +>f3 : (x: string | number) => void +>x : string | number + + action(() => { x = "abc" }); +>action(() => { x = "abc" }) : void +>action : (f: Function) => void +>() => { x = "abc" } : () => void +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { +>f4 : (cond: () => boolean) => void +>cond : () => boolean + + let x: string | number = 0; +>x : string | number +>0 : 0 + + while (cond()) { +>cond() : boolean +>cond : () => boolean + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +function f5(x: string | number, cond: () => boolean) { +>f5 : (x: string | number, cond: () => boolean) => void +>x : string | number +>cond : () => boolean + + if (cond()) { +>cond() : boolean +>cond : () => boolean + + x = 1; +>x = 1 : 1 +>x : string | number +>1 : 1 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + else { + x = 2; +>x = 2 : 2 +>x : string | number +>2 : 2 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +// Implicit any variables have a known type following last assignment + +function f6() { +>f6 : () => void + + let x; +>x : any + + x = "abc"; +>x = "abc" : "abc" +>x : any +>"abc" : "abc" + + action(() => { x }); // Error +>action(() => { x }) : void +>action : (f: Function) => void +>() => { x } : () => void +>x : any + + x = 42; +>x = 42 : 42 +>x : any +>42 : 42 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { +>f7 : () => void + + try { + } + catch (e) { +>e : unknown + + if (e instanceof Error) { +>e instanceof Error : boolean +>e : unknown +>Error : ErrorConstructor + + let f = () => { e /* Error */ } +>f : () => void +>() => { e /* Error */ } : () => void +>e : Error + } + } +} + +// Repros from #35124 + +function f10() { +>f10 : () => (k: number) => boolean + + let i: number | undefined; +>i : number | undefined + + i = 0; +>i = 0 : 0 +>i : number | undefined +>0 : 0 + + return (k: number) => k === i + 1; +>(k: number) => k === i + 1 : (k: number) => boolean +>k : number +>k === i + 1 : boolean +>k : number +>i + 1 : number +>i : number +>1 : 1 +} + +function makeAdder(n?: number) { +>makeAdder : (n?: number) => (m: number) => number +>n : number | undefined + + n ??= 0; +>n ??= 0 : number +>n : number | undefined +>0 : 0 + + return (m: number) => n + m; +>(m: number) => n + m : (m: number) => number +>m : number +>n + m : number +>n : number +>m : number +} + +function f11() { +>f11 : () => void + + let r; +>r : any + + r = "b"; +>r = "b" : "b" +>r : any +>"b" : "b" + + () => r; +>() => r : () => string +>r : string +} + +// Repro from #52104 + +const fooMap: Map> = new Map() +>fooMap : Map +>new Map() : Map +>Map : MapConstructor + +const values = [1, 2, 3, 4, 5]; +>values : number[] +>[1, 2, 3, 4, 5] : number[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 +>5 : 5 + +let foo = fooMap.get("a"); +>foo : number[] | undefined +>fooMap.get("a") : number[] | undefined +>fooMap.get : (key: string) => number[] | undefined +>fooMap : Map +>get : (key: string) => number[] | undefined +>"a" : "a" + +if (foo == null) { +>foo == null : boolean +>foo : number[] | undefined + + foo = []; +>foo = [] : never[] +>foo : number[] | undefined +>[] : never[] +} +values.forEach(v => foo.push(v)); +>values.forEach(v => foo.push(v)) : void +>values.forEach : (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void +>values : number[] +>forEach : (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void +>v => foo.push(v) : (v: number) => number +>v : number +>foo.push(v) : number +>foo.push : (...items: number[]) => number +>foo : number[] +>push : (...items: number[]) => number +>v : number + diff --git a/tests/cases/compiler/narrowingPastLastAssignment.ts b/tests/cases/compiler/narrowingPastLastAssignment.ts new file mode 100644 index 0000000000000..ef10a3abafa2a --- /dev/null +++ b/tests/cases/compiler/narrowingPastLastAssignment.ts @@ -0,0 +1,117 @@ +// @strict: true +// @noEmit: true +// @target: esnext + +function action(f: Function) {} + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* number */ }); +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { + let x: string | number; + x = 42; + let a = () => { x /* number */ }; + let f = function() { x /* number */ }; + let C = class { + foo() { x /* number */ } + }; + let o = { + foo() { x /* number */ } + }; + function g() { x /* string | number */ } + class A { + foo() { x /* string | number */ } + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { + action(() => { x = "abc" }); + x = 42; + action(() => { x /* string | number */ }); +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { + let x: string | number = 0; + while (cond()) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); +} + +function f5(x: string | number, cond: () => boolean) { + if (cond()) { + x = 1; + action(() => { x /* string | number */ }); + } + else { + x = 2; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); +} + +// Implicit any variables have a known type following last assignment + +function f6() { + let x; + x = "abc"; + action(() => { x }); // Error + x = 42; + action(() => { x /* number */ }); +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { + try { + } + catch (e) { + if (e instanceof Error) { + let f = () => { e /* Error */ } + } + } +} + +// Repros from #35124 + +function f10() { + let i: number | undefined; + i = 0; + return (k: number) => k === i + 1; +} + +function makeAdder(n?: number) { + n ??= 0; + return (m: number) => n + m; +} + +function f11() { + let r; + r = "b"; + () => r; +} + +// Repro from #52104 + +const fooMap: Map> = new Map() +const values = [1, 2, 3, 4, 5]; +let foo = fooMap.get("a"); +if (foo == null) { + foo = []; +} +values.forEach(v => foo.push(v)); From 2739399c0632b309b7b398b34f89204a9b7756d5 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sat, 30 Dec 2023 18:36:33 -1000 Subject: [PATCH 04/13] Preserve formatting (though it's odd) --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fafbb59ff84a1..d5b973d91682a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29193,7 +29193,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // declaration container are the same). const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) || type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 || - isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || + isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || node.parent.kind === SyntaxKind.NonNullExpression || declaration.kind === SyntaxKind.VariableDeclaration && (declaration as VariableDeclaration).exclamationToken || declaration.flags & NodeFlags.Ambient; From a490ef463320a2d1f001dde0552b453838e84083 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Sun, 31 Dec 2023 07:11:18 -1000 Subject: [PATCH 05/13] Fix formatting --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d5b973d91682a..36e2071b82461 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -28816,7 +28816,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { forEachChild(node, markAssignments); } } - } + } function isConstantVariable(symbol: Symbol) { return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0; From e2d90d981e9f7a7013524910c4d7fb83d744876e Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 1 Jan 2024 09:17:14 -1000 Subject: [PATCH 06/13] Update tests --- tests/baselines/reference/controlFlowAliasing.errors.txt | 4 ++-- tests/baselines/reference/controlFlowAliasing.js | 8 ++++---- tests/baselines/reference/controlFlowAliasing.symbols | 4 ++-- tests/baselines/reference/controlFlowAliasing.types | 4 ++-- .../reference/implicitConstParameters.errors.txt | 2 +- tests/baselines/reference/implicitConstParameters.js | 4 ++-- tests/baselines/reference/implicitConstParameters.symbols | 2 +- tests/baselines/reference/implicitConstParameters.types | 2 +- tests/cases/compiler/implicitConstParameters.ts | 2 +- .../cases/conformance/controlFlow/controlFlowAliasing.ts | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/baselines/reference/controlFlowAliasing.errors.txt b/tests/baselines/reference/controlFlowAliasing.errors.txt index 25a8858c4ca4f..b1dcf7be43f9c 100644 --- a/tests/baselines/reference/controlFlowAliasing.errors.txt +++ b/tests/baselines/reference/controlFlowAliasing.errors.txt @@ -182,10 +182,10 @@ controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being a let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } diff --git a/tests/baselines/reference/controlFlowAliasing.js b/tests/baselines/reference/controlFlowAliasing.js index de9b8ca84b43f..a37a261e2ac58 100644 --- a/tests/baselines/reference/controlFlowAliasing.js +++ b/tests/baselines/reference/controlFlowAliasing.js @@ -134,10 +134,10 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } @@ -423,10 +423,10 @@ function f25(arg) { var obj = arg; var isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } function f26(outer) { diff --git a/tests/baselines/reference/controlFlowAliasing.symbols b/tests/baselines/reference/controlFlowAliasing.symbols index 802b6246fa2b6..0fc737c2828d4 100644 --- a/tests/baselines/reference/controlFlowAliasing.symbols +++ b/tests/baselines/reference/controlFlowAliasing.symbols @@ -370,13 +370,13 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { if (isFoo) { >isFoo : Symbol(isFoo, Decl(controlFlowAliasing.ts, 131, 9)) - obj.foo; // Not narrowed because obj is mutable + obj.foo; >obj.foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) >foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; >obj.bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) >bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) diff --git a/tests/baselines/reference/controlFlowAliasing.types b/tests/baselines/reference/controlFlowAliasing.types index 6c0e6bd8721e6..ea5ee6edf01f3 100644 --- a/tests/baselines/reference/controlFlowAliasing.types +++ b/tests/baselines/reference/controlFlowAliasing.types @@ -440,13 +440,13 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { if (isFoo) { >isFoo : boolean - obj.foo; // Not narrowed because obj is mutable + obj.foo; >obj.foo : string >obj : { kind: "foo"; foo: string; } >foo : string } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; >obj.bar : number >obj : { kind: "bar"; bar: number; } >bar : number diff --git a/tests/baselines/reference/implicitConstParameters.errors.txt b/tests/baselines/reference/implicitConstParameters.errors.txt index e1a85b51d9d20..2be671d75de0d 100644 --- a/tests/baselines/reference/implicitConstParameters.errors.txt +++ b/tests/baselines/reference/implicitConstParameters.errors.txt @@ -37,7 +37,7 @@ implicitConstParameters.ts(44,27): error TS18048: 'x' is possibly 'undefined'. } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); } diff --git a/tests/baselines/reference/implicitConstParameters.js b/tests/baselines/reference/implicitConstParameters.js index 001a81d352df8..c14592085405a 100644 --- a/tests/baselines/reference/implicitConstParameters.js +++ b/tests/baselines/reference/implicitConstParameters.js @@ -36,7 +36,7 @@ function f3(x: string | undefined) { } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); } @@ -88,7 +88,7 @@ function f3(x) { } } function f4(x) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(function () { return x.length; }); } diff --git a/tests/baselines/reference/implicitConstParameters.symbols b/tests/baselines/reference/implicitConstParameters.symbols index 298355c44015a..16ba4c949c325 100644 --- a/tests/baselines/reference/implicitConstParameters.symbols +++ b/tests/baselines/reference/implicitConstParameters.symbols @@ -86,7 +86,7 @@ function f4(x: string | undefined) { >f4 : Symbol(f4, Decl(implicitConstParameters.ts, 32, 1)) >x : Symbol(x, Decl(implicitConstParameters.ts, 34, 12)) - x = "abc"; // causes x to be considered non-const + x = "abc"; >x : Symbol(x, Decl(implicitConstParameters.ts, 34, 12)) if (x) { diff --git a/tests/baselines/reference/implicitConstParameters.types b/tests/baselines/reference/implicitConstParameters.types index 9cace5f5ad323..4b21f714f817e 100644 --- a/tests/baselines/reference/implicitConstParameters.types +++ b/tests/baselines/reference/implicitConstParameters.types @@ -103,7 +103,7 @@ function f4(x: string | undefined) { >f4 : (x: string | undefined) => void >x : string | undefined - x = "abc"; // causes x to be considered non-const + x = "abc"; >x = "abc" : "abc" >x : string | undefined >"abc" : "abc" diff --git a/tests/cases/compiler/implicitConstParameters.ts b/tests/cases/compiler/implicitConstParameters.ts index 97996789124f6..f5c2cd5e79828 100644 --- a/tests/cases/compiler/implicitConstParameters.ts +++ b/tests/cases/compiler/implicitConstParameters.ts @@ -35,7 +35,7 @@ function f3(x: string | undefined) { } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); } diff --git a/tests/cases/conformance/controlFlow/controlFlowAliasing.ts b/tests/cases/conformance/controlFlow/controlFlowAliasing.ts index b32f80abae94f..f9a3defe79bf9 100644 --- a/tests/cases/conformance/controlFlow/controlFlowAliasing.ts +++ b/tests/cases/conformance/controlFlow/controlFlowAliasing.ts @@ -134,10 +134,10 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } From adda3eeec558dbfa8bd373710126ed00374d2836 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 3 Jan 2024 09:26:05 -0800 Subject: [PATCH 07/13] Support for all types of mutable variables --- src/compiler/checker.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 36e2071b82461..0c2cc4e388adb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -253,7 +253,6 @@ import { getContainingClassExcludingClassDecorators, getContainingClassStaticBlock, getContainingFunction, - getContainingFunctionDeclaration, getContainingFunctionOrClassStaticBlock, getDeclarationModifierFlagsFromSymbol, getDeclarationOfKind, @@ -476,7 +475,6 @@ import { isCallLikeOrFunctionLikeExpression, isCallOrNewExpression, isCallSignatureDeclaration, - isCatchClause, isCatchClauseVariableDeclarationOrBindingElement, isCheckJsEnabledForFile, isClassDeclaration, @@ -27451,7 +27449,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.Identifier: if (!isThisInTypeQuery(node)) { const symbol = getResolvedSymbol(node as Identifier); - return isConstantVariable(symbol) || isParameterOrLetOrCatchVariable(symbol) && !isSymbolAssigned(symbol); + return isConstantVariable(symbol) || isParameterOrMutableVariable(symbol) && !isSymbolAssigned(symbol); } break; case SyntaxKind.PropertyAccessExpression: @@ -28734,7 +28732,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Return true if there are no assignments to the given symbol or if the given location // is past the last assignment to the symbol. function isPastLastAssignment(symbol: Symbol, location: Node | undefined) { - const parent = findAncestor(symbol.valueDeclaration, isAssignmentMarkingContainer); + const parent = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); if (!parent) { return false; } @@ -28763,11 +28761,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function hasParentWithAssignmentsMarked(node: Node) { - return !!findAncestor(node.parent, node => isAssignmentMarkingContainer(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + return !!findAncestor(node.parent, node => isFunctionOrSourceFile(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); } - function isAssignmentMarkingContainer(node: Node) { - return isFunctionLikeDeclaration(node) || isBlock(node) || isSourceFile(node) || isForStatement(node) || isForInOrOfStatement(node) || isCatchClause(node); + function isFunctionOrSourceFile(node: Node) { + return isFunctionLikeDeclaration(node) || isSourceFile(node); } // For all assignments within the given root node, record the last assignment source position for all @@ -28783,9 +28781,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.Identifier: if (isAssignmentTarget(node)) { const symbol = getResolvedSymbol(node as Identifier); - if (isParameterOrLetOrCatchVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { - const referencingFunction = getContainingFunctionDeclaration(node) || getSourceFileOfNode(node); - const declaringFunction = getContainingFunctionDeclaration(symbol.valueDeclaration!) || getSourceFileOfNode(node); + if (isParameterOrMutableVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { + const referencingFunction = findAncestor(node, isFunctionOrSourceFile); + const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); symbol.lastAssignmentPos = referencingFunction === declaringFunction ? statementEnd ?? node.pos : Number.MAX_VALUE; } } @@ -28822,10 +28820,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0; } - function isParameterOrLetOrCatchVariable(symbol: Symbol) { + function isParameterOrMutableVariable(symbol: Symbol) { const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); return !!(declaration && (isParameter(declaration) || - isVariableDeclaration(declaration) && (declaration.parent.kind === SyntaxKind.CatchClause || declaration.parent.flags & NodeFlags.Let))); + isVariableDeclaration(declaration) && (declaration.parent.kind === SyntaxKind.CatchClause || !(declaration.parent.flags & NodeFlags.Constant)))); } function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean { @@ -29184,7 +29182,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { while ( flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || (isParameterOrLetOrCatchVariable(localOrExportSymbol)) && isPastLastAssignment(localOrExportSymbol, node)) + (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || (isParameterOrMutableVariable(localOrExportSymbol)) && isPastLastAssignment(localOrExportSymbol, node)) ) { flowContainer = getControlFlowContainer(flowContainer); } From 9a1faffcbd6f1563a07540098c98f10c9ab1e497 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 3 Jan 2024 09:26:32 -0800 Subject: [PATCH 08/13] Accept new baselines --- tests/baselines/reference/enumIndexer.types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/baselines/reference/enumIndexer.types b/tests/baselines/reference/enumIndexer.types index ef25de3ab05a2..53ddde562b2cd 100644 --- a/tests/baselines/reference/enumIndexer.types +++ b/tests/baselines/reference/enumIndexer.types @@ -38,5 +38,5 @@ var x = _arr.map(o => MyEnumType[o.key] === enumValue); // these are not same ty >o.key : string >o : { key: string; } >key : string ->enumValue : MyEnumType +>enumValue : MyEnumType.foo From 1cccdf26f8939f94aaaef103bafb3456e583b1f1 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 8 Jan 2024 08:50:42 -0800 Subject: [PATCH 09/13] Exclude var variables and exported/global variables --- src/compiler/checker.ts | 110 ++++++++++++++++++++++++++-------------- src/compiler/types.ts | 2 +- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0c2cc4e388adb..b95efca2c9dd1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -475,6 +475,7 @@ import { isCallLikeOrFunctionLikeExpression, isCallOrNewExpression, isCallSignatureDeclaration, + isCatchClause, isCatchClauseVariableDeclarationOrBindingElement, isCheckJsEnabledForFile, isClassDeclaration, @@ -27449,7 +27450,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.Identifier: if (!isThisInTypeQuery(node)) { const symbol = getResolvedSymbol(node as Identifier); - return isConstantVariable(symbol) || isParameterOrMutableVariable(symbol) && !isSymbolAssigned(symbol); + return isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol); } break; case SyntaxKind.PropertyAccessExpression: @@ -28769,25 +28770,50 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // For all assignments within the given root node, record the last assignment source position for all - // referenced parameters, let variables, and catch variables. When assignments occur in nested functions, - // record Number.MAX_VALUE as the assignment position. When assignments occur in compound statements, - // record the ending source position of the compound statement as the assignment position (this is more - // conservative than full control flow analysis, but requires only a single walk over the AST). + // referenced parameters and mutable local variables. When assignments occur in nested functions or + // references occur in export specifiers, record Number.MAX_VALUE as the assignment position. When + // assignments occur in compound statements, record the ending source position of the compound statement + // as the assignment position (this is more conservative than full control flow analysis, but requires + // only a single walk over the AST). function markNodeAssignments(node: Node) { - let statementEnd: number | undefined; - markAssignments(node); - function markAssignments(node: Node) { - switch (node.kind) { - case SyntaxKind.Identifier: - if (isAssignmentTarget(node)) { - const symbol = getResolvedSymbol(node as Identifier); - if (isParameterOrMutableVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { - const referencingFunction = findAncestor(node, isFunctionOrSourceFile); - const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); - symbol.lastAssignmentPos = referencingFunction === declaringFunction ? statementEnd ?? node.pos : Number.MAX_VALUE; - } + switch (node.kind) { + case SyntaxKind.Identifier: + if (isAssignmentTarget(node)) { + const symbol = getResolvedSymbol(node as Identifier); + if (isParameterOrMutableLocalVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { + const referencingFunction = findAncestor(node, isFunctionOrSourceFile); + const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); + symbol.lastAssignmentPos = referencingFunction === declaringFunction ? extendAssignmentPosition(node, symbol.valueDeclaration!) : Number.MAX_VALUE; } - return; + } + return; + case SyntaxKind.ExportSpecifier: + const exportDeclaration = (node as ExportSpecifier).parent.parent; + if (!(node as ExportSpecifier).isTypeOnly && !exportDeclaration.isTypeOnly && !exportDeclaration.moduleSpecifier) { + const symbol = resolveEntityName((node as ExportSpecifier).propertyName || (node as ExportSpecifier).name, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true); + if (symbol && isParameterOrMutableLocalVariable(symbol)) { + symbol.lastAssignmentPos = Number.MAX_VALUE; + } + } + return; + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + return; + } + if (isTypeNode(node)) { + return; + } + forEachChild(node, markNodeAssignments); + } + + // Extend the position of the given assignment target node to the end of any intervening variable statement, + // expression statement, compound statement, or class declaration occurring between the node and the given + // declaration node. + function extendAssignmentPosition(node: Node, declaration: Declaration) { + let pos = node.pos; + while (node && node.pos > declaration.pos) { + switch (node.kind) { case SyntaxKind.VariableStatement: case SyntaxKind.ExpressionStatement: case SyntaxKind.IfStatement: @@ -28800,30 +28826,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.SwitchStatement: case SyntaxKind.TryStatement: case SyntaxKind.ClassDeclaration: - const saveStatementEnd = statementEnd; - statementEnd ??= node.end; - forEachChild(node, markAssignments); - statementEnd = saveStatementEnd; - return; - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - return; - } - if (!isTypeNode(node)) { - forEachChild(node, markAssignments); + pos = node.end; } + node = node.parent; } + return pos; } function isConstantVariable(symbol: Symbol) { return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0; } - function isParameterOrMutableVariable(symbol: Symbol) { + function isParameterOrMutableLocalVariable(symbol: Symbol) { + // Return true if symbol is a parameter, a catch clause variable, or a mutable local variable const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); - return !!(declaration && (isParameter(declaration) || - isVariableDeclaration(declaration) && (declaration.parent.kind === SyntaxKind.CatchClause || !(declaration.parent.flags & NodeFlags.Constant)))); + return !!declaration && ( + isParameter(declaration) || + isVariableDeclaration(declaration) && (isCatchClause(declaration.parent) || isMutableLocalVariableDeclaration(declaration))); + } + + function isMutableLocalVariableDeclaration(declaration: VariableDeclaration) { + // Return true if symbol is a non-exported and non-global `let` variable + return !!(declaration.parent.flags & NodeFlags.Let) && !( + getCombinedModifierFlags(declaration) & ModifierFlags.Export || + declaration.parent.parent.kind === SyntaxKind.VariableStatement && isGlobalSourceFile(declaration.parent.parent.parent)); } function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean { @@ -29176,13 +29202,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const isModuleExports = symbol.flags & SymbolFlags.ModuleExports; const typeIsAutomatic = type === autoType || type === autoArrayType; const isAutomaticTypeInNonNull = typeIsAutomatic && node.parent.kind === SyntaxKind.NonNullExpression; - // When the control flow originates in a function expression or arrow function and we are referencing - // a const variable or parameter from an outer function, we extend the origin of the control flow - // analysis to include the immediately enclosing function. + // When the control flow originates in a function expression, arrow function, method, or accessor, and + // we are referencing a closed-over const variable or parameter or mutable local variable past its last + // assignment, we extend the origin of the control flow analysis to include the immediately enclosing + // control flow container. while ( - flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || - flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || (isParameterOrMutableVariable(localOrExportSymbol)) && isPastLastAssignment(localOrExportSymbol, node)) + flowContainer !== declarationContainer && ( + flowContainer.kind === SyntaxKind.FunctionExpression || + flowContainer.kind === SyntaxKind.ArrowFunction || + isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer) + ) && ( + isConstantVariable(localOrExportSymbol) && type !== autoArrayType || + isParameterOrMutableLocalVariable(localOrExportSymbol) && isPastLastAssignment(localOrExportSymbol, node) + ) ) { flowContainer = getControlFlowContainer(flowContainer); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8bd4d4ea26249..0376471ced20b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5825,8 +5825,8 @@ export interface Symbol { /** @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol /** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums /** @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter. + /** @internal */ lastAssignmentPos?: number; // Source position of last node that assigns value to symbol /** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol? - /** @internal */ lastAssignmentPos?: number; // Last node that assigns value to symbol /** @internal */ assignmentDeclarationMembers?: Map; // detected late-bound assignment declarations associated with the symbol } From 5aff92be8104838cea572051b35765514a3c7ccc Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 8 Jan 2024 08:52:25 -0800 Subject: [PATCH 10/13] Update tests and add more tests in a module --- .../compiler/narrowingPastLastAssignment.ts | 52 ++++++++++++++++--- .../narrowingPastLastAssignmentInModule.ts | 30 +++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/cases/compiler/narrowingPastLastAssignmentInModule.ts diff --git a/tests/cases/compiler/narrowingPastLastAssignment.ts b/tests/cases/compiler/narrowingPastLastAssignment.ts index ef10a3abafa2a..ae66ab03db719 100644 --- a/tests/cases/compiler/narrowingPastLastAssignment.ts +++ b/tests/cases/compiler/narrowingPastLastAssignment.ts @@ -65,6 +65,27 @@ function f5(x: string | number, cond: () => boolean) { action(() => { x /* number */ }); } +function f5a(cond: boolean) { + if (cond) { + let x: number | undefined; + x = 1; + action(() => { x /* number */ }); + } + else { + let x: number | undefined; + x = 2; + action(() => { x /* number */ }); + } +} + +function f5b() { + for (let x = 0; x < 10; x++) { + if (x === 1 || x === 2) { + action(() => { x /* 1 | 2 */ }) + } + } +} + // Implicit any variables have a known type following last assignment function f6() { @@ -87,6 +108,23 @@ function f7() { } } +// Narrowings are not preserved for global variables + +let g: string | number; +g = "abc"; +action(() => { g /* string | number */ }); + +// Narrowings are not preserved for exported namespace members + +namespace Foo { + export let x: string | number; + x = "abc"; + action(() => { x /* string | number */ }); + let y: string | number; + y = "abc"; + action(() => { y /* string */ }); +} + // Repros from #35124 function f10() { @@ -108,10 +146,12 @@ function f11() { // Repro from #52104 -const fooMap: Map> = new Map() -const values = [1, 2, 3, 4, 5]; -let foo = fooMap.get("a"); -if (foo == null) { - foo = []; +function f12() { + const fooMap: Map> = new Map() + const values = [1, 2, 3, 4, 5]; + let foo = fooMap.get("a"); + if (foo == null) { + foo = []; + } + values.forEach(v => foo.push(v)); } -values.forEach(v => foo.push(v)); diff --git a/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts b/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts new file mode 100644 index 0000000000000..b036091fc851a --- /dev/null +++ b/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts @@ -0,0 +1,30 @@ +// @strict: true +// @noEmit: true +// @target: esnext + +function action(f: Function) {} + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +x1 = "abc"; +action(() => { x1 /* string | number */ }); + +export { x2 }; +let x2: string | number; +x2 = "abc"; +action(() => { x2 /* string | number */ }); + +export { x3 as foo }; +let x3: string | number; +x3 = "abc"; +action(() => { x3 /* string | number */ }); + +let x4: string | number; +x4 = "abc"; +action(() => { x4 /* string */ }); +export default x4; + +let x5: string | number; +x5 = "abc"; +action(() => { x5 /* string */ }); From e0ccb0da7633c472b967b5370cad3c889ab51ad1 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 8 Jan 2024 08:52:52 -0800 Subject: [PATCH 11/13] Accept new baselines --- tests/baselines/reference/enumIndexer.types | 2 +- .../narrowingPastLastAssignment.errors.txt | 56 +++++- .../narrowingPastLastAssignment.symbols | 179 +++++++++++++----- .../narrowingPastLastAssignment.types | 136 ++++++++++++- ...arrowingPastLastAssignmentInModule.symbols | 70 +++++++ .../narrowingPastLastAssignmentInModule.types | 89 +++++++++ 6 files changed, 472 insertions(+), 60 deletions(-) create mode 100644 tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols create mode 100644 tests/baselines/reference/narrowingPastLastAssignmentInModule.types diff --git a/tests/baselines/reference/enumIndexer.types b/tests/baselines/reference/enumIndexer.types index 53ddde562b2cd..ef25de3ab05a2 100644 --- a/tests/baselines/reference/enumIndexer.types +++ b/tests/baselines/reference/enumIndexer.types @@ -38,5 +38,5 @@ var x = _arr.map(o => MyEnumType[o.key] === enumValue); // these are not same ty >o.key : string >o : { key: string; } >key : string ->enumValue : MyEnumType.foo +>enumValue : MyEnumType diff --git a/tests/baselines/reference/narrowingPastLastAssignment.errors.txt b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt index 7590912410bde..0477f4c4877a1 100644 --- a/tests/baselines/reference/narrowingPastLastAssignment.errors.txt +++ b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt @@ -1,5 +1,5 @@ -narrowingPastLastAssignment.ts(67,9): error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. -narrowingPastLastAssignment.ts(69,20): error TS7005: Variable 'x' implicitly has an 'any' type. +narrowingPastLastAssignment.ts(88,9): error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. +narrowingPastLastAssignment.ts(90,20): error TS7005: Variable 'x' implicitly has an 'any' type. ==== narrowingPastLastAssignment.ts (2 errors) ==== @@ -66,6 +66,27 @@ narrowingPastLastAssignment.ts(69,20): error TS7005: Variable 'x' implicitly has action(() => { x /* number */ }); } + function f5a(cond: boolean) { + if (cond) { + let x: number | undefined; + x = 1; + action(() => { x /* number */ }); + } + else { + let x: number | undefined; + x = 2; + action(() => { x /* number */ }); + } + } + + function f5b() { + for (let x = 0; x < 10; x++) { + if (x === 1 || x === 2) { + action(() => { x /* 1 | 2 */ }) + } + } + } + // Implicit any variables have a known type following last assignment function f6() { @@ -92,6 +113,23 @@ narrowingPastLastAssignment.ts(69,20): error TS7005: Variable 'x' implicitly has } } + // Narrowings are not preserved for global variables + + let g: string | number; + g = "abc"; + action(() => { g /* string | number */ }); + + // Narrowings are not preserved for exported namespace members + + namespace Foo { + export let x: string | number; + x = "abc"; + action(() => { x /* string | number */ }); + let y: string | number; + y = "abc"; + action(() => { y /* string */ }); + } + // Repros from #35124 function f10() { @@ -113,11 +151,13 @@ narrowingPastLastAssignment.ts(69,20): error TS7005: Variable 'x' implicitly has // Repro from #52104 - const fooMap: Map> = new Map() - const values = [1, 2, 3, 4, 5]; - let foo = fooMap.get("a"); - if (foo == null) { - foo = []; + function f12() { + const fooMap: Map> = new Map() + const values = [1, 2, 3, 4, 5]; + let foo = fooMap.get("a"); + if (foo == null) { + foo = []; + } + values.forEach(v => foo.push(v)); } - values.forEach(v => foo.push(v)); \ No newline at end of file diff --git a/tests/baselines/reference/narrowingPastLastAssignment.symbols b/tests/baselines/reference/narrowingPastLastAssignment.symbols index 006603f0d010d..4ace603d16418 100644 --- a/tests/baselines/reference/narrowingPastLastAssignment.symbols +++ b/tests/baselines/reference/narrowingPastLastAssignment.symbols @@ -152,123 +152,214 @@ function f5(x: string | number, cond: () => boolean) { >x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) } +function f5a(cond: boolean) { +>f5a : Symbol(f5a, Decl(narrowingPastLastAssignment.ts, 61, 1)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 63, 13)) + + if (cond) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 63, 13)) + + let x: number | undefined; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + + x = 1; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + } + else { + let x: number | undefined; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + + x = 2; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + } +} + +function f5b() { +>f5b : Symbol(f5b, Decl(narrowingPastLastAssignment.ts, 74, 1)) + + for (let x = 0; x < 10; x++) { +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + + if (x === 1 || x === 2) { +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + + action(() => { x /* 1 | 2 */ }) +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + } + } +} + // Implicit any variables have a known type following last assignment function f6() { ->f6 : Symbol(f6, Decl(narrowingPastLastAssignment.ts, 61, 1)) +>f6 : Symbol(f6, Decl(narrowingPastLastAssignment.ts, 82, 1)) let x; ->x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) x = "abc"; ->x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) action(() => { x }); // Error >action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) ->x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) x = 42; ->x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) action(() => { x /* number */ }); >action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) ->x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 66, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) } // Narrowings on catch variables are preserved past last assignment function f7() { ->f7 : Symbol(f7, Decl(narrowingPastLastAssignment.ts, 71, 1)) +>f7 : Symbol(f7, Decl(narrowingPastLastAssignment.ts, 92, 1)) try { } catch (e) { ->e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) if (e instanceof Error) { ->e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) >Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2022.error.d.ts, --, --)) let f = () => { e /* Error */ } ->f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 80, 15)) ->e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 78, 11)) +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 101, 15)) +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) } } } +// Narrowings are not preserved for global variables + +let g: string | number; +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +g = "abc"; +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +action(() => { g /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +// Narrowings are not preserved for exported namespace members + +namespace Foo { +>Foo : Symbol(Foo, Decl(narrowingPastLastAssignment.ts, 110, 42)) + + export let x: string | number; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + let y: string | number; +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) + + y = "abc"; +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) + + action(() => { y /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) +} + // Repros from #35124 function f10() { ->f10 : Symbol(f10, Decl(narrowingPastLastAssignment.ts, 83, 1)) +>f10 : Symbol(f10, Decl(narrowingPastLastAssignment.ts, 121, 1)) let i: number | undefined; ->i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) i = 0; ->i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) return (k: number) => k === i + 1; ->k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 90, 12)) ->k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 90, 12)) ->i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 88, 7)) +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 128, 12)) +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 128, 12)) +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) } function makeAdder(n?: number) { ->makeAdder : Symbol(makeAdder, Decl(narrowingPastLastAssignment.ts, 91, 1)) ->n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) +>makeAdder : Symbol(makeAdder, Decl(narrowingPastLastAssignment.ts, 129, 1)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) n ??= 0; ->n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) return (m: number) => n + m; ->m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 95, 12)) ->n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 93, 19)) ->m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 95, 12)) +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 133, 12)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 133, 12)) } function f11() { ->f11 : Symbol(f11, Decl(narrowingPastLastAssignment.ts, 96, 1)) +>f11 : Symbol(f11, Decl(narrowingPastLastAssignment.ts, 134, 1)) let r; ->r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) r = "b"; ->r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) () => r; ->r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 99, 7)) +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) } // Repro from #52104 -const fooMap: Map> = new Map() ->fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 106, 5)) +function f12() { +>f12 : Symbol(f12, Decl(narrowingPastLastAssignment.ts, 140, 1)) + + const fooMap: Map> = new Map() +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 145, 9)) >Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) >Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) >Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) -const values = [1, 2, 3, 4, 5]; ->values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 107, 5)) + const values = [1, 2, 3, 4, 5]; +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 146, 9)) -let foo = fooMap.get("a"); ->foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) + let foo = fooMap.get("a"); +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) >fooMap.get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) ->fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 106, 5)) +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 145, 9)) >get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) -if (foo == null) { ->foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) + if (foo == null) { +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) - foo = []; ->foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) -} -values.forEach(v => foo.push(v)); + foo = []; +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) + } + values.forEach(v => foo.push(v)); >values.forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) ->values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 107, 5)) +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 146, 9)) >forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) ->v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 112, 15)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 151, 19)) >foo.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) ->foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 108, 3)) +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) >push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) ->v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 112, 15)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 151, 19)) +} diff --git a/tests/baselines/reference/narrowingPastLastAssignment.types b/tests/baselines/reference/narrowingPastLastAssignment.types index 99cde6a0fd2e2..18dce42399162 100644 --- a/tests/baselines/reference/narrowingPastLastAssignment.types +++ b/tests/baselines/reference/narrowingPastLastAssignment.types @@ -196,6 +196,74 @@ function f5(x: string | number, cond: () => boolean) { >x : number } +function f5a(cond: boolean) { +>f5a : (cond: boolean) => void +>cond : boolean + + if (cond) { +>cond : boolean + + let x: number | undefined; +>x : number | undefined + + x = 1; +>x = 1 : 1 +>x : number | undefined +>1 : 1 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number + } + else { + let x: number | undefined; +>x : number | undefined + + x = 2; +>x = 2 : 2 +>x : number | undefined +>2 : 2 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number + } +} + +function f5b() { +>f5b : () => void + + for (let x = 0; x < 10; x++) { +>x : number +>0 : 0 +>x < 10 : boolean +>x : number +>10 : 10 +>x++ : number +>x : number + + if (x === 1 || x === 2) { +>x === 1 || x === 2 : boolean +>x === 1 : boolean +>x : number +>1 : 1 +>x === 2 : boolean +>x : number +>2 : 2 + + action(() => { x /* 1 | 2 */ }) +>action(() => { x /* 1 | 2 */ }) : void +>action : (f: Function) => void +>() => { x /* 1 | 2 */ } : () => void +>x : 1 | 2 + } + } +} + // Implicit any variables have a known type following last assignment function f6() { @@ -250,6 +318,56 @@ function f7() { } } +// Narrowings are not preserved for global variables + +let g: string | number; +>g : string | number + +g = "abc"; +>g = "abc" : "abc" +>g : string | number +>"abc" : "abc" + +action(() => { g /* string | number */ }); +>action(() => { g /* string | number */ }) : void +>action : (f: Function) => void +>() => { g /* string | number */ } : () => void +>g : string | number + +// Narrowings are not preserved for exported namespace members + +namespace Foo { +>Foo : typeof Foo + + export let x: string | number; +>x : string | number + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + let y: string | number; +>y : string | number + + y = "abc"; +>y = "abc" : "abc" +>y : string | number +>"abc" : "abc" + + action(() => { y /* string */ }); +>action(() => { y /* string */ }) : void +>action : (f: Function) => void +>() => { y /* string */ } : () => void +>y : string +} + // Repros from #35124 function f10() { @@ -308,12 +426,15 @@ function f11() { // Repro from #52104 -const fooMap: Map> = new Map() +function f12() { +>f12 : () => void + + const fooMap: Map> = new Map() >fooMap : Map >new Map() : Map >Map : MapConstructor -const values = [1, 2, 3, 4, 5]; + const values = [1, 2, 3, 4, 5]; >values : number[] >[1, 2, 3, 4, 5] : number[] >1 : 1 @@ -322,7 +443,7 @@ const values = [1, 2, 3, 4, 5]; >4 : 4 >5 : 5 -let foo = fooMap.get("a"); + let foo = fooMap.get("a"); >foo : number[] | undefined >fooMap.get("a") : number[] | undefined >fooMap.get : (key: string) => number[] | undefined @@ -330,16 +451,16 @@ let foo = fooMap.get("a"); >get : (key: string) => number[] | undefined >"a" : "a" -if (foo == null) { + if (foo == null) { >foo == null : boolean >foo : number[] | undefined - foo = []; + foo = []; >foo = [] : never[] >foo : number[] | undefined >[] : never[] -} -values.forEach(v => foo.push(v)); + } + values.forEach(v => foo.push(v)); >values.forEach(v => foo.push(v)) : void >values.forEach : (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void >values : number[] @@ -351,4 +472,5 @@ values.forEach(v => foo.push(v)); >foo : number[] >push : (...items: number[]) => number >v : number +} diff --git a/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols b/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols new file mode 100644 index 0000000000000..c4b5b29a98b81 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols @@ -0,0 +1,70 @@ +//// [tests/cases/compiler/narrowingPastLastAssignmentInModule.ts] //// + +=== narrowingPastLastAssignmentInModule.ts === +function action(f: Function) {} +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>f : Symbol(f, Decl(narrowingPastLastAssignmentInModule.ts, 0, 16)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.decorators.d.ts, --, --)) + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +x1 = "abc"; +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +action(() => { x1 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +export { x2 }; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 8, 8)) + +let x2: string | number; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +x2 = "abc"; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +action(() => { x2 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +export { x3 as foo }; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) +>foo : Symbol(foo, Decl(narrowingPastLastAssignmentInModule.ts, 13, 8)) + +let x3: string | number; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +x3 = "abc"; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +action(() => { x3 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +let x4: string | number; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +x4 = "abc"; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +action(() => { x4 /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +export default x4; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +let x5: string | number; +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + +x5 = "abc"; +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + +action(() => { x5 /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + diff --git a/tests/baselines/reference/narrowingPastLastAssignmentInModule.types b/tests/baselines/reference/narrowingPastLastAssignmentInModule.types new file mode 100644 index 0000000000000..ccb173341e07a --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignmentInModule.types @@ -0,0 +1,89 @@ +//// [tests/cases/compiler/narrowingPastLastAssignmentInModule.ts] //// + +=== narrowingPastLastAssignmentInModule.ts === +function action(f: Function) {} +>action : (f: Function) => void +>f : Function + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +>x1 : string | number + +x1 = "abc"; +>x1 = "abc" : "abc" +>x1 : string | number +>"abc" : "abc" + +action(() => { x1 /* string | number */ }); +>action(() => { x1 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x1 /* string | number */ } : () => void +>x1 : string | number + +export { x2 }; +>x2 : string | number + +let x2: string | number; +>x2 : string | number + +x2 = "abc"; +>x2 = "abc" : "abc" +>x2 : string | number +>"abc" : "abc" + +action(() => { x2 /* string | number */ }); +>action(() => { x2 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x2 /* string | number */ } : () => void +>x2 : string | number + +export { x3 as foo }; +>x3 : string | number +>foo : string | number + +let x3: string | number; +>x3 : string | number + +x3 = "abc"; +>x3 = "abc" : "abc" +>x3 : string | number +>"abc" : "abc" + +action(() => { x3 /* string | number */ }); +>action(() => { x3 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x3 /* string | number */ } : () => void +>x3 : string | number + +let x4: string | number; +>x4 : string | number + +x4 = "abc"; +>x4 = "abc" : "abc" +>x4 : string | number +>"abc" : "abc" + +action(() => { x4 /* string */ }); +>action(() => { x4 /* string */ }) : void +>action : (f: Function) => void +>() => { x4 /* string */ } : () => void +>x4 : string + +export default x4; +>x4 : string | number + +let x5: string | number; +>x5 : string | number + +x5 = "abc"; +>x5 = "abc" : "abc" +>x5 : string | number +>"abc" : "abc" + +action(() => { x5 /* string */ }); +>action(() => { x5 /* string */ }) : void +>action : (f: Function) => void +>() => { x5 /* string */ } : () => void +>x5 : string + From cd9b30abca9e0514e45b7e602a1c582769a6be65 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 8 Jan 2024 08:57:35 -0800 Subject: [PATCH 12/13] Fix formatting --- src/compiler/checker.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b95efca2c9dd1..8c1148bc457a8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -28842,14 +28842,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); return !!declaration && ( isParameter(declaration) || - isVariableDeclaration(declaration) && (isCatchClause(declaration.parent) || isMutableLocalVariableDeclaration(declaration))); + isVariableDeclaration(declaration) && (isCatchClause(declaration.parent) || isMutableLocalVariableDeclaration(declaration)) + ); } function isMutableLocalVariableDeclaration(declaration: VariableDeclaration) { // Return true if symbol is a non-exported and non-global `let` variable return !!(declaration.parent.flags & NodeFlags.Let) && !( getCombinedModifierFlags(declaration) & ModifierFlags.Export || - declaration.parent.parent.kind === SyntaxKind.VariableStatement && isGlobalSourceFile(declaration.parent.parent.parent)); + declaration.parent.parent.kind === SyntaxKind.VariableStatement && isGlobalSourceFile(declaration.parent.parent.parent) + ); } function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean { From 28da1c58c24d7c876db9a8727637fb204df1ac26 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 8 Jan 2024 16:41:28 -0800 Subject: [PATCH 13/13] Reinstate isCatchClauseVariableDeclaration --- src/compiler/utilities.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 412df8e917685..843249e4bba29 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -10346,6 +10346,11 @@ export function isInfinityOrNaNString(name: string | __String): boolean { return name === "Infinity" || name === "-Infinity" || name === "NaN"; } +/** @internal */ +export function isCatchClauseVariableDeclaration(node: Node) { + return node.kind === SyntaxKind.VariableDeclaration && node.parent.kind === SyntaxKind.CatchClause; +} + /** @internal */ export function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction;