Skip to content

Commit 533ed3d

Browse files
error on variables that are used but never initialized (#55887)
Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
1 parent 627fbcb commit 533ed3d

19 files changed

+1970
-23
lines changed

src/compiler/checker.ts

+31-10
Original file line numberDiff line numberDiff line change
@@ -29602,7 +29602,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2960229602
node.kind === SyntaxKind.PropertyDeclaration)!;
2960329603
}
2960429604

29605-
// Check if a parameter or catch variable is assigned anywhere
29605+
// Check if a parameter, catch variable, or mutable local variable is assigned anywhere definitely
29606+
function isSymbolAssignedDefinitely(symbol: Symbol) {
29607+
if (symbol.lastAssignmentPos !== undefined) {
29608+
return symbol.lastAssignmentPos < 0;
29609+
}
29610+
return isSymbolAssigned(symbol) && symbol.lastAssignmentPos !== undefined && symbol.lastAssignmentPos < 0;
29611+
}
29612+
29613+
// Check if a parameter, catch variable, or mutable local variable is assigned anywhere
2960629614
function isSymbolAssigned(symbol: Symbol) {
2960729615
return !isPastLastAssignment(symbol, /*location*/ undefined);
2960829616
}
@@ -29621,7 +29629,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2962129629
markNodeAssignments(parent);
2962229630
}
2962329631
}
29624-
return !symbol.lastAssignmentPos || location && symbol.lastAssignmentPos < location.pos;
29632+
return !symbol.lastAssignmentPos || location && Math.abs(symbol.lastAssignmentPos) < location.pos;
2962529633
}
2962629634

2962729635
// Check if a parameter or catch variable (or their bindings elements) is assigned anywhere
@@ -29655,12 +29663,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2965529663
function markNodeAssignments(node: Node) {
2965629664
switch (node.kind) {
2965729665
case SyntaxKind.Identifier:
29658-
if (isAssignmentTarget(node)) {
29666+
const assigmentTarget = getAssignmentTargetKind(node);
29667+
if (assigmentTarget !== AssignmentKind.None) {
2965929668
const symbol = getResolvedSymbol(node as Identifier);
29660-
if (isParameterOrMutableLocalVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) {
29661-
const referencingFunction = findAncestor(node, isFunctionOrSourceFile);
29662-
const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile);
29663-
symbol.lastAssignmentPos = referencingFunction === declaringFunction ? extendAssignmentPosition(node, symbol.valueDeclaration!) : Number.MAX_VALUE;
29669+
const hasDefiniteAssignment = assigmentTarget === AssignmentKind.Definite || (symbol.lastAssignmentPos !== undefined && symbol.lastAssignmentPos < 0);
29670+
if (isParameterOrMutableLocalVariable(symbol)) {
29671+
if (symbol.lastAssignmentPos === undefined || Math.abs(symbol.lastAssignmentPos) !== Number.MAX_VALUE) {
29672+
const referencingFunction = findAncestor(node, isFunctionOrSourceFile);
29673+
const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile);
29674+
symbol.lastAssignmentPos = referencingFunction === declaringFunction ? extendAssignmentPosition(node, symbol.valueDeclaration!) : Number.MAX_VALUE;
29675+
}
29676+
if (hasDefiniteAssignment && symbol.lastAssignmentPos > 0) {
29677+
symbol.lastAssignmentPos *= -1;
29678+
}
2966429679
}
2966529680
}
2966629681
return;
@@ -29670,7 +29685,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2967029685
if (!(node as ExportSpecifier).isTypeOnly && !exportDeclaration.isTypeOnly && !exportDeclaration.moduleSpecifier && name.kind !== SyntaxKind.StringLiteral) {
2967129686
const symbol = resolveEntityName(name, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true);
2967229687
if (symbol && isParameterOrMutableLocalVariable(symbol)) {
29673-
symbol.lastAssignmentPos = Number.MAX_VALUE;
29688+
const sign = symbol.lastAssignmentPos !== undefined && symbol.lastAssignmentPos < 0 ? -1 : 1;
29689+
symbol.lastAssignmentPos = sign * Number.MAX_VALUE;
2967429690
}
2967529691
}
2967629692
return;
@@ -30414,6 +30430,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3041430430

3041530431
const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol);
3041630432
let declaration = localOrExportSymbol.valueDeclaration;
30433+
const immediateDeclaration = declaration;
3041730434

3041830435
// If the identifier is declared in a binding pattern for which we're currently computing the implied type and the
3041930436
// reference occurs with the same binding pattern, return the non-inferrable any type. This for example occurs in
@@ -30503,7 +30520,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3050330520
// We only look for uninitialized variables in strict null checking mode, and only when we can analyze
3050430521
// the entire control flow graph from the variable's declaration (i.e. when the flow container and
3050530522
// declaration container are the same).
30506-
const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) ||
30523+
const isNeverInitialized = immediateDeclaration && isVariableDeclaration(immediateDeclaration) && !immediateDeclaration.initializer && !immediateDeclaration.exclamationToken && isMutableLocalVariableDeclaration(immediateDeclaration) && !isSymbolAssignedDefinitely(symbol);
30524+
const assumeInitialized = isParameter || isAlias ||
30525+
(isOuterVariable && !isNeverInitialized) ||
30526+
isSpreadDestructuringAssignmentTarget || isModuleExports || isSameScopedBindingElement(node, declaration) ||
3050730527
type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 ||
3050830528
isInTypeQuery(node) || isInAmbientOrTypeNode(node) || node.parent.kind === SyntaxKind.ExportSpecifier) ||
3050930529
node.parent.kind === SyntaxKind.NonNullExpression ||
@@ -43495,7 +43515,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4349543515
case SyntaxKind.MethodDeclaration:
4349643516
case SyntaxKind.GetAccessor:
4349743517
case SyntaxKind.SetAccessor:
43498-
if (node.body) { // Don't report unused parameters in overloads
43518+
// Only report unused parameters on the implementation, not overloads.
43519+
if (node.body) {
4349943520
checkUnusedLocalsAndParameters(node, addDiagnostic);
4350043521
}
4350143522
checkUnusedTypeParameters(node, addDiagnostic);

src/compiler/transformers/classFields.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ import {
228228
} from "../_namespaces/ts.js";
229229

230230
const enum ClassPropertySubstitutionFlags {
231+
None = 0,
231232
/**
232233
* Enables substitutions for class expressions with static fields
233234
* which have initializers that reference the class name.
@@ -401,7 +402,7 @@ export function transformClassFields(context: TransformationContext): (x: Source
401402
context.onEmitNode = onEmitNode;
402403

403404
let shouldTransformPrivateStaticElementsInFile = false;
404-
let enabledSubstitutions: ClassPropertySubstitutionFlags;
405+
let enabledSubstitutions = ClassPropertySubstitutionFlags.None;
405406

406407
let classAliases: Identifier[];
407408

src/compiler/transformers/es2015.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ import {
218218
} from "../_namespaces/ts.js";
219219

220220
const enum ES2015SubstitutionFlags {
221+
None = 0,
221222
/** Enables substitutions for captured `this` */
222223
CapturedThis = 1 << 0,
223224
/** Enables substitutions for block-scoped bindings. */
@@ -523,7 +524,7 @@ export function transformES2015(context: TransformationContext): (x: SourceFile
523524
* They are persisted between each SourceFile transformation and should not
524525
* be reset.
525526
*/
526-
let enabledSubstitutions: ES2015SubstitutionFlags;
527+
let enabledSubstitutions = ES2015SubstitutionFlags.None;
527528

528529
return chainBundle(context, transformSourceFile);
529530

src/compiler/transformers/es2017.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import {
104104
type SuperContainer = ClassDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration;
105105

106106
const enum ES2017SubstitutionFlags {
107+
None = 0,
107108
/** Enables substitutions for async methods with `super` calls. */
108109
AsyncMethodsWithSuper = 1 << 0,
109110
}
@@ -132,7 +133,7 @@ export function transformES2017(context: TransformationContext): (x: SourceFile
132133
* Keeps track of whether expression substitution has been enabled for specific edge cases.
133134
* They are persisted between each SourceFile transformation and should not be reset.
134135
*/
135-
let enabledSubstitutions: ES2017SubstitutionFlags;
136+
let enabledSubstitutions = ES2017SubstitutionFlags.None;
136137

137138
/**
138139
* This keeps track of containers where `super` is valid, for use with

src/compiler/transformers/es2018.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import {
113113
} from "../_namespaces/ts.js";
114114

115115
const enum ESNextSubstitutionFlags {
116+
None = 0,
116117
/** Enables substitutions for async methods with `super` calls. */
117118
AsyncMethodsWithSuper = 1 << 0,
118119
}
@@ -170,7 +171,7 @@ export function transformES2018(context: TransformationContext): (x: SourceFile
170171
context.onSubstituteNode = onSubstituteNode;
171172

172173
let exportedVariableStatement = false;
173-
let enabledSubstitutions: ESNextSubstitutionFlags;
174+
let enabledSubstitutions = ESNextSubstitutionFlags.None;
174175
let enclosingFunctionFlags: FunctionFlags;
175176
let parametersWithPrecedingObjectRestOrSpread: Set<ParameterDeclaration> | undefined;
176177
let enclosingSuperContainerFlags: NodeCheckFlags = 0;

src/compiler/transformers/ts.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ import {
208208
const USE_NEW_TYPE_METADATA_FORMAT = false;
209209

210210
const enum TypeScriptSubstitutionFlags {
211+
None = 0,
211212
/** Enables substitutions for namespace exports. */
212213
NamespaceExports = 1 << 1,
213214
/* Enables substitutions for unqualified enum members */
@@ -270,7 +271,7 @@ export function transformTypeScript(context: TransformationContext) {
270271
* Keeps track of whether expression substitution has been enabled for specific edge cases.
271272
* They are persisted between each SourceFile transformation and should not be reset.
272273
*/
273-
let enabledSubstitutions: TypeScriptSubstitutionFlags;
274+
let enabledSubstitutions = TypeScriptSubstitutionFlags.None;
274275

275276
/**
276277
* Keeps track of whether we are within any containing namespaces when performing

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5970,7 +5970,7 @@ export interface Symbol {
59705970
/** @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
59715971
/** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums
59725972
/** @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.
5973-
/** @internal */ lastAssignmentPos?: number; // Source position of last node that assigns value to symbol
5973+
/** @internal */ lastAssignmentPos?: number; // Source position of last node that assigns value to symbol. Negative if it is assigned anywhere definitely
59745974
/** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
59755975
/** @internal */ assignmentDeclarationMembers?: Map<number, Declaration>; // detected late-bound assignment declarations associated with the symbol
59765976
}

src/compiler/utilities.ts

+5
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ import {
153153
forEachChild,
154154
forEachChildRecursively,
155155
ForInOrOfStatement,
156+
ForInStatement,
157+
ForOfStatement,
156158
ForStatement,
157159
FunctionBody,
158160
FunctionDeclaration,
@@ -7906,6 +7908,9 @@ function accessKind(node: Node): AccessKind {
79067908
return node === (parent as ShorthandPropertyAssignment).objectAssignmentInitializer ? AccessKind.Read : accessKind(parent.parent);
79077909
case SyntaxKind.ArrayLiteralExpression:
79087910
return accessKind(parent);
7911+
case SyntaxKind.ForInStatement:
7912+
case SyntaxKind.ForOfStatement:
7913+
return node === (parent as ForInStatement | ForOfStatement).initializer ? AccessKind.Write : AccessKind.Read;
79097914
default:
79107915
return AccessKind.Read;
79117916
}

tests/baselines/reference/findAllRefsObjectBindingElementPropertyName06.baseline.jsonc

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// for (<|var { [|property1|]: p1 } of elems|>) {
1414
// }
1515
// var p2;
16-
// for (<|{ [|{| isWriteAccess: true |}property1|] : p2 } of elems|>) {
16+
// for (<|{ [|property1|] : p2 } of elems|>) {
1717
// }
1818

1919
// === Definitions ===
@@ -94,7 +94,7 @@
9494
// for (<|var { [|property1|]: p1 } of elems|>) {
9595
// }
9696
// var p2;
97-
// for (<|{ [|{| isWriteAccess: true |}property1|] : p2 } of elems|>) {
97+
// for (<|{ [|property1|] : p2 } of elems|>) {
9898
// }
9999

100100
// === Definitions ===
@@ -180,7 +180,7 @@
180180
// for (<|var { /*FIND ALL REFS*/[|property1|]: p1 } of elems|>) {
181181
// }
182182
// var p2;
183-
// for (<|{ [|{| isWriteAccess: true |}property1|] : p2 } of elems|>) {
183+
// for (<|{ [|property1|] : p2 } of elems|>) {
184184
// }
185185

186186
// === Definitions ===
@@ -270,7 +270,7 @@
270270
// for (<|var { [|property1|]: p1 } of elems|>) {
271271
// }
272272
// var p2;
273-
// for (<|{ /*FIND ALL REFS*/[|{| isWriteAccess: true, isDefinition: true |}property1|] : p2 } of elems|>) {
273+
// for (<|{ /*FIND ALL REFS*/[|{| isDefinition: true |}property1|] : p2 } of elems|>) {
274274
// }
275275

276276
// === Definitions ===
@@ -358,7 +358,7 @@
358358
// for (<|var { [|{| defId: 0 |}property1|]: p1 } of elems|>) {
359359
// }
360360
// var p2;
361-
// for (<|{ [|{| defId: 0, isWriteAccess: true |}property1|] : p2 } of elems|>) {
361+
// for (<|{ [|{| defId: 0 |}property1|] : p2 } of elems|>) {
362362
// }
363363

364364
// === Definitions ===

tests/baselines/reference/narrowingPastLastAssignment.errors.txt

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
narrowingPastLastAssignment.ts(88,9): error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined.
22
narrowingPastLastAssignment.ts(90,20): error TS7005: Variable 'x' implicitly has an 'any' type.
3+
narrowingPastLastAssignment.ts(161,9): error TS18048: 'foo' is possibly 'undefined'.
34

45

5-
==== narrowingPastLastAssignment.ts (2 errors) ====
6+
==== narrowingPastLastAssignment.ts (3 errors) ====
67
function action(f: Function) {}
78

89
// Narrowings are preserved in closures created past last assignment
@@ -160,4 +161,15 @@ narrowingPastLastAssignment.ts(90,20): error TS7005: Variable 'x' implicitly has
160161
}
161162
values.forEach(v => foo.push(v));
162163
}
163-
164+
165+
function f13() {
166+
// Test for captured 'var' declaration (as opposed to parameters, let, const).
167+
var foo: string | undefined;
168+
foo = '';
169+
170+
return () => {
171+
foo.toLocaleLowerCase();
172+
~~~
173+
!!! error TS18048: 'foo' is possibly 'undefined'.
174+
}
175+
}

tests/baselines/reference/narrowingPastLastAssignment.symbols

+17
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,20 @@ function f12() {
363363
>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 151, 19))
364364
}
365365

366+
function f13() {
367+
>f13 : Symbol(f13, Decl(narrowingPastLastAssignment.ts, 152, 1))
368+
369+
// Test for captured 'var' declaration (as opposed to parameters, let, const).
370+
var foo: string | undefined;
371+
>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 156, 7))
372+
373+
foo = '';
374+
>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 156, 7))
375+
376+
return () => {
377+
foo.toLocaleLowerCase();
378+
>foo.toLocaleLowerCase : Symbol(String.toLocaleLowerCase, Decl(lib.es5.d.ts, --, --), Decl(lib.es2020.string.d.ts, --, --))
379+
>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 156, 7))
380+
>toLocaleLowerCase : Symbol(String.toLocaleLowerCase, Decl(lib.es5.d.ts, --, --), Decl(lib.es2020.string.d.ts, --, --))
381+
}
382+
}

tests/baselines/reference/narrowingPastLastAssignment.types

+32
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,35 @@ function f12() {
731731
> : ^^^^^^
732732
}
733733

734+
function f13() {
735+
>f13 : () => () => void
736+
> : ^^^^^^^^^^^^^^^^
737+
738+
// Test for captured 'var' declaration (as opposed to parameters, let, const).
739+
var foo: string | undefined;
740+
>foo : string | undefined
741+
> : ^^^^^^^^^^^^^^^^^^
742+
743+
foo = '';
744+
>foo = '' : ""
745+
> : ^^
746+
>foo : string | undefined
747+
> : ^^^^^^^^^^^^^^^^^^
748+
>'' : ""
749+
> : ^^
750+
751+
return () => {
752+
>() => { foo.toLocaleLowerCase(); } : () => void
753+
> : ^^^^^^^^^^
754+
755+
foo.toLocaleLowerCase();
756+
>foo.toLocaleLowerCase() : string
757+
> : ^^^^^^
758+
>foo.toLocaleLowerCase : { (locales?: string | string[]): string; (locales?: Intl.LocalesArgument): string; }
759+
> : ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^
760+
>foo : string | undefined
761+
> : ^^^^^^^^^^^^^^^^^^
762+
>toLocaleLowerCase : { (locales?: string | string[]): string; (locales?: Intl.LocalesArgument): string; }
763+
> : ^^^ ^^^ ^^^ ^^^ ^^^ ^^^ ^^^
764+
}
765+
}

0 commit comments

Comments
 (0)