diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 115e2447360..e70e6502f84 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -23240,9 +23240,10 @@ namespace ts { function isConstantReference(node: Node): boolean { switch (node.kind) { - case SyntaxKind.Identifier: + case SyntaxKind.Identifier: { const symbol = getResolvedSymbol(node as Identifier); - return isConstVariable(symbol) || !!symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration).kind === SyntaxKind.Parameter && !isParameterAssigned(symbol); + return isConstVariable(symbol) || isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol); + } case SyntaxKind.PropertyAccessExpression: case SyntaxKind.ElementAccessExpression: // The resolvedSymbol property is initialized by checkPropertyAccess or checkElementAccess before we get here. @@ -24376,37 +24377,38 @@ namespace ts { node.kind === SyntaxKind.PropertyDeclaration)!; } - // Check if a parameter is assigned anywhere within its declaring function. - function isParameterAssigned(symbol: Symbol) { + // Check if a parameter or catch variable is assigned anywhere + function isSymbolAssigned(symbol: Symbol) { if (!symbol.valueDeclaration) { return false; } - const func = getRootDeclaration(symbol.valueDeclaration).parent as FunctionLikeDeclaration; - const links = getNodeLinks(func); + const parent = getRootDeclaration(symbol.valueDeclaration).parent; + const links = getNodeLinks(parent); if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) { links.flags |= NodeCheckFlags.AssignmentsMarked; - if (!hasParentWithAssignmentsMarked(func)) { - markParameterAssignments(func); + if (!hasParentWithAssignmentsMarked(parent)) { + markNodeAssignments(parent); } } return symbol.isAssigned || false; } function hasParentWithAssignmentsMarked(node: Node) { - return !!findAncestor(node.parent, node => isFunctionLike(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + return !!findAncestor(node.parent, node => + (isFunctionLike(node) || isCatchClause(node)) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); } - function markParameterAssignments(node: Node) { + function markNodeAssignments(node: Node) { if (node.kind === SyntaxKind.Identifier) { if (isAssignmentTarget(node)) { const symbol = getResolvedSymbol(node as Identifier); - if (symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration).kind === SyntaxKind.Parameter) { + if (isParameterOrCatchClauseVariable(symbol)) { symbol.isAssigned = true; } } } else { - forEachChild(node, markParameterAssignments); + forEachChild(node, markNodeAssignments); } } @@ -24644,7 +24646,7 @@ namespace ts { // analysis to include the immediately enclosing function. while (flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isParameterAssigned(localOrExportSymbol))) { + (isConstVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(localOrExportSymbol))) { flowContainer = getControlFlowContainer(flowContainer); } // We only look for uninitialized variables in strict null checking mode, and only when we can analyze diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 64f822a8422..8ac91f86f99 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7409,4 +7409,13 @@ namespace ts { export function isInfinityOrNaNString(name: string | __String): boolean { return name === "Infinity" || name === "-Infinity" || name === "NaN"; } + + export function isCatchClauseVariableDeclaration(node: Node) { + return node.kind === SyntaxKind.VariableDeclaration && node.parent.kind === SyntaxKind.CatchClause; + } + + export function isParameterOrCatchClauseVariable(symbol: Symbol) { + const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); + return !!declaration && (isParameter(declaration) || isCatchClauseVariableDeclaration(declaration)); + } } diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index 916dbd0210e..b0c47694a72 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -138,7 +138,8 @@ namespace Harness { "skipDefaultLibCheck", "preserveConstEnums", "skipLibCheck", - "exactOptionalPropertyTypes" + "exactOptionalPropertyTypes", + "useUnknownInCatchVariables" ]; private fileName: string; private justName: string; diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).js b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).js new file mode 100644 index 00000000000..a53358ddd11 --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).js @@ -0,0 +1,51 @@ +//// [controlFlowAliasingCatchVariables.ts] +try {} +catch (e) { + const isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} + +try {} +catch (e) { + const isString = typeof e === 'string'; + + e = 1; + + if (isString) { + e.toUpperCase(); // e any/unknown + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} + + +//// [controlFlowAliasingCatchVariables.js] +try { } +catch (e) { + var isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} +try { } +catch (e) { + var isString = typeof e === 'string'; + e = 1; + if (isString) { + e.toUpperCase(); // e any/unknown + } + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).symbols b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).symbols new file mode 100644 index 00000000000..65cbd78ea0f --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).symbols @@ -0,0 +1,56 @@ +=== tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts === +try {} +catch (e) { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + const isString = typeof e === 'string'; +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 2, 9)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + if (isString) { +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 2, 9)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } + + if (typeof e === 'string') { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } +} + +try {} +catch (e) { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + const isString = typeof e === 'string'; +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 14, 9)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + e = 1; +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + if (isString) { +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 14, 9)) + + e.toUpperCase(); // e any/unknown +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + } + + if (typeof e === 'string') { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } +} + diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).types b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).types new file mode 100644 index 00000000000..1acb8762c91 --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=false).types @@ -0,0 +1,76 @@ +=== tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts === +try {} +catch (e) { +>e : any + + const isString = typeof e === 'string'; +>isString : boolean +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : any +>'string' : "string" + + if (isString) { +>isString : boolean + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } + + if (typeof e === 'string') { +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : any +>'string' : "string" + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } +} + +try {} +catch (e) { +>e : any + + const isString = typeof e === 'string'; +>isString : boolean +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : any +>'string' : "string" + + e = 1; +>e = 1 : 1 +>e : any +>1 : 1 + + if (isString) { +>isString : boolean + + e.toUpperCase(); // e any/unknown +>e.toUpperCase() : any +>e.toUpperCase : any +>e : any +>toUpperCase : any + } + + if (typeof e === 'string') { +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : any +>'string' : "string" + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } +} + diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).errors.txt b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).errors.txt new file mode 100644 index 00000000000..c0e5a830690 --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).errors.txt @@ -0,0 +1,33 @@ +tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts(20,11): error TS2339: Property 'toUpperCase' does not exist on type 'unknown'. + + +==== tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts (1 errors) ==== + try {} + catch (e) { + const isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } + } + + try {} + catch (e) { + const isString = typeof e === 'string'; + + e = 1; + + if (isString) { + e.toUpperCase(); // e any/unknown + ~~~~~~~~~~~ +!!! error TS2339: Property 'toUpperCase' does not exist on type 'unknown'. + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).js b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).js new file mode 100644 index 00000000000..a53358ddd11 --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).js @@ -0,0 +1,51 @@ +//// [controlFlowAliasingCatchVariables.ts] +try {} +catch (e) { + const isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} + +try {} +catch (e) { + const isString = typeof e === 'string'; + + e = 1; + + if (isString) { + e.toUpperCase(); // e any/unknown + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} + + +//// [controlFlowAliasingCatchVariables.js] +try { } +catch (e) { + var isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} +try { } +catch (e) { + var isString = typeof e === 'string'; + e = 1; + if (isString) { + e.toUpperCase(); // e any/unknown + } + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).symbols b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).symbols new file mode 100644 index 00000000000..65cbd78ea0f --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).symbols @@ -0,0 +1,56 @@ +=== tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts === +try {} +catch (e) { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + const isString = typeof e === 'string'; +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 2, 9)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + if (isString) { +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 2, 9)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } + + if (typeof e === 'string') { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 1, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } +} + +try {} +catch (e) { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + const isString = typeof e === 'string'; +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 14, 9)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + e = 1; +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + if (isString) { +>isString : Symbol(isString, Decl(controlFlowAliasingCatchVariables.ts, 14, 9)) + + e.toUpperCase(); // e any/unknown +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + } + + if (typeof e === 'string') { +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) + + e.toUpperCase(); // e string +>e.toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) +>e : Symbol(e, Decl(controlFlowAliasingCatchVariables.ts, 13, 7)) +>toUpperCase : Symbol(String.toUpperCase, Decl(lib.es5.d.ts, --, --)) + } +} + diff --git a/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).types b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).types new file mode 100644 index 00000000000..f2b63b5772f --- /dev/null +++ b/tests/baselines/reference/controlFlowAliasingCatchVariables(useunknownincatchvariables=true).types @@ -0,0 +1,76 @@ +=== tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts === +try {} +catch (e) { +>e : unknown + + const isString = typeof e === 'string'; +>isString : boolean +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : unknown +>'string' : "string" + + if (isString) { +>isString : boolean + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } + + if (typeof e === 'string') { +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : unknown +>'string' : "string" + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } +} + +try {} +catch (e) { +>e : unknown + + const isString = typeof e === 'string'; +>isString : boolean +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : unknown +>'string' : "string" + + e = 1; +>e = 1 : 1 +>e : unknown +>1 : 1 + + if (isString) { +>isString : boolean + + e.toUpperCase(); // e any/unknown +>e.toUpperCase() : any +>e.toUpperCase : any +>e : unknown +>toUpperCase : any + } + + if (typeof e === 'string') { +>typeof e === 'string' : boolean +>typeof e : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>e : unknown +>'string' : "string" + + e.toUpperCase(); // e string +>e.toUpperCase() : string +>e.toUpperCase : () => string +>e : string +>toUpperCase : () => string + } +} + diff --git a/tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts b/tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts new file mode 100644 index 00000000000..a746fa1cd0a --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowAliasingCatchVariables.ts @@ -0,0 +1,28 @@ +// @useUnknownInCatchVariables: true,false + +try {} +catch (e) { + const isString = typeof e === 'string'; + if (isString) { + e.toUpperCase(); // e string + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +} + +try {} +catch (e) { + const isString = typeof e === 'string'; + + e = 1; + + if (isString) { + e.toUpperCase(); // e any/unknown + } + + if (typeof e === 'string') { + e.toUpperCase(); // e string + } +}