Skip to content

Fix temp vars referenced in parameter #38130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 76 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -1529,6 +1529,63 @@ namespace ts {
}
}

function useOuterVariableScopeInParameter(result: Symbol, location: Node, lastLocation: Node) {
const target = getEmitScriptTarget(compilerOptions);
const functionLocation = <FunctionLikeDeclaration>location;
if (isParameter(lastLocation) && functionLocation.body && result.valueDeclaration.pos >= functionLocation.body.pos && result.valueDeclaration.end <= functionLocation.body.end) {
// check for several cases where we introduce temporaries that require moving the name/initializer of the parameter to the body
// - static field in a class expression
// - optional chaining pre-es2020
// - nullish coalesce pre-es2020
// - spread assignment in binding pattern pre-es2017
if (target >= ScriptTarget.ES2015) {
const links = getNodeLinks(functionLocation);
if (links.declarationRequiresScopeChange === undefined) {
links.declarationRequiresScopeChange = forEach(functionLocation.parameters, requiresScopeChange) || false;
}
return !links.declarationRequiresScopeChange;
}
}
return false;

function requiresScopeChange(node: ParameterDeclaration): boolean {
return requiresScopeChangeWorker(node.name)
|| !!node.initializer && requiresScopeChangeWorker(node.initializer);
}

function requiresScopeChangeWorker(node: Node): boolean {
switch (node.kind) {
case SyntaxKind.ArrowFunction:
case SyntaxKind.FunctionExpression:
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.Constructor:
// do not descend into these
return false;
case SyntaxKind.MethodDeclaration:
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.PropertyAssignment:
return requiresScopeChangeWorker((node as MethodDeclaration | AccessorDeclaration | PropertyAssignment).name);
case SyntaxKind.PropertyDeclaration:
// static properties in classes introduce temporary variables
if (hasStaticModifier(node)) {
return target < ScriptTarget.ESNext || !compilerOptions.useDefineForClassFields;
}
return requiresScopeChangeWorker((node as PropertyDeclaration).name);
default:
// null coalesce and optional chain pre-es2020 produce temporary variables
if (isNullishCoalesce(node) || isOptionalChain(node)) {
return target < ScriptTarget.ES2020;
}
if (isBindingElement(node) && node.dotDotDotToken && isObjectBindingPattern(node.parent)) {
return target < ScriptTarget.ES2017;
}
if (isTypeNode(node)) return false;
return forEachChild(node, requiresScopeChangeWorker) || false;
}
}
}

/**
* Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and
* the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with
@@ -1563,7 +1620,7 @@ namespace ts {
let lastLocation: Node | undefined;
let lastSelfReferenceLocation: Node | undefined;
let propertyWithInvalidInitializer: Node | undefined;
let associatedDeclarationForContainingInitializer: ParameterDeclaration | BindingElement | undefined;
let associatedDeclarationForContainingInitializerOrBindingName: ParameterDeclaration | BindingElement | undefined;
let withinDeferredContext = false;
const errorLocation = location;
let grandparent: Node;
@@ -1592,9 +1649,7 @@ namespace ts {
}
if (meaning & result.flags & SymbolFlags.Variable) {
// expression inside parameter will lookup as normal variable scope when targeting es2015+
const functionLocation = <FunctionLikeDeclaration>location;
if (compilerOptions.target && compilerOptions.target >= ScriptTarget.ES2015 && isParameter(lastLocation) &&
functionLocation.body && result.valueDeclaration.pos >= functionLocation.body.pos && result.valueDeclaration.end <= functionLocation.body.end) {
if (useOuterVariableScopeInParameter(result, location, lastLocation)) {
useResult = false;
}
else if (result.flags & SymbolFlags.FunctionScopedVariable) {
@@ -1822,15 +1877,21 @@ namespace ts {
location = getJSDocHost(location);
break;
case SyntaxKind.Parameter:
if (lastLocation && lastLocation === (location as ParameterDeclaration).initializer) {
associatedDeclarationForContainingInitializer = location as ParameterDeclaration;
if (lastLocation && (
lastLocation === (location as ParameterDeclaration).initializer ||
lastLocation === (location as ParameterDeclaration).name && isBindingPattern(lastLocation))) {
if (!associatedDeclarationForContainingInitializerOrBindingName) {
associatedDeclarationForContainingInitializerOrBindingName = location as ParameterDeclaration;
}
}
break;
case SyntaxKind.BindingElement:
if (lastLocation && lastLocation === (location as BindingElement).initializer) {
if (lastLocation && (
lastLocation === (location as BindingElement).initializer ||
lastLocation === (location as BindingElement).name && isBindingPattern(lastLocation))) {
const root = getRootDeclaration(location);
if (root.kind === SyntaxKind.Parameter) {
associatedDeclarationForContainingInitializer = location as BindingElement;
associatedDeclarationForContainingInitializerOrBindingName = location as BindingElement;
}
}
break;
@@ -1941,17 +2002,17 @@ namespace ts {
}
}

// If we're in a parameter initializer, we can't reference the values of the parameter whose initializer we're within or parameters to the right
if (result && associatedDeclarationForContainingInitializer && !withinDeferredContext && (meaning & SymbolFlags.Value) === SymbolFlags.Value) {
// If we're in a parameter initializer or binding name, we can't reference the values of the parameter whose initializer we're within or parameters to the right
if (result && associatedDeclarationForContainingInitializerOrBindingName && !withinDeferredContext && (meaning & SymbolFlags.Value) === SymbolFlags.Value) {
const candidate = getMergedSymbol(getLateBoundSymbol(result));
const root = (getRootDeclaration(associatedDeclarationForContainingInitializer) as ParameterDeclaration);
const root = (getRootDeclaration(associatedDeclarationForContainingInitializerOrBindingName) as ParameterDeclaration);
// A parameter initializer or binding pattern initializer within a parameter cannot refer to itself
if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializer)) {
error(errorLocation, Diagnostics.Parameter_0_cannot_be_referenced_in_its_initializer, declarationNameToString(associatedDeclarationForContainingInitializer.name));
if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializerOrBindingName)) {
error(errorLocation, Diagnostics.Parameter_0_cannot_reference_itself, declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name));
}
// And it cannot refer to any declarations which come after it
else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializer.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) {
error(errorLocation, Diagnostics.Initializer_of_parameter_0_cannot_reference_identifier_1_declared_after_it, declarationNameToString(associatedDeclarationForContainingInitializer.name), declarationNameToString(<Identifier>errorLocation));
else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializerOrBindingName.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) {
error(errorLocation, Diagnostics.Parameter_0_cannot_reference_identifier_1_declared_after_it, declarationNameToString(associatedDeclarationForContainingInitializerOrBindingName.name), declarationNameToString(<Identifier>errorLocation));
}
}
if (result && errorLocation && meaning & SymbolFlags.Value && result.flags & SymbolFlags.Alias) {
4 changes: 2 additions & 2 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
@@ -1477,11 +1477,11 @@
"category": "Error",
"code": 2371
},
"Parameter '{0}' cannot be referenced in its initializer.": {
"Parameter '{0}' cannot reference itself.": {
"category": "Error",
"code": 2372
},
"Initializer of parameter '{0}' cannot reference identifier '{1}' declared after it.": {
"Parameter '{0}' cannot reference identifier '{1}' declared after it.": {
"category": "Error",
"code": 2373
},
13 changes: 8 additions & 5 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
@@ -4,11 +4,14 @@ namespace ts {
enableEmitNotification: noop,
enableSubstitution: noop,
endLexicalEnvironment: returnUndefined,
getCompilerOptions: notImplemented,
getCompilerOptions: () => ({}),
getEmitHost: notImplemented,
getEmitResolver: notImplemented,
setLexicalEnvironmentFlags: noop,
getLexicalEnvironmentFlags: () => 0,
hoistFunctionDeclaration: noop,
hoistVariableDeclaration: noop,
addInitializationStatement: noop,
isEmitNotificationEnabled: notImplemented,
isSubstitutionEnabled: notImplemented,
onEmitNode: noop,
@@ -852,13 +855,13 @@ namespace ts {
* This function needs to be called whenever we transform the statement
* list of a source file, namespace, or function-like body.
*/
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number, visitor?: (node: Node) => VisitResult<Node>): number;
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult<Node>): number | undefined;
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult<Node>): number | undefined {
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number, visitor?: (node: Node) => VisitResult<Node>, filter?: (node: Node) => boolean): number;
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult<Node>, filter?: (node: Node) => boolean): number | undefined;
export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult<Node>, filter: (node: Node) => boolean = returnTrue): number | undefined {
const numStatements = source.length;
while (statementOffset !== undefined && statementOffset < numStatements) {
const statement = source[statementOffset];
if (getEmitFlags(statement) & EmitFlags.CustomPrologue) {
if (getEmitFlags(statement) & EmitFlags.CustomPrologue && filter(statement)) {
append(target, visitor ? visitNode(statement, visitor, isStatement) : statement);
}
else {
Loading