-
Notifications
You must be signed in to change notification settings - Fork 12.9k
disallow recursive references for block-scoped bindings #2309
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -439,14 +439,56 @@ module ts { | |
var declaration = forEach(result.declarations, d => isBlockOrCatchScoped(d) ? d : undefined); | ||
|
||
Debug.assert(declaration !== undefined, "Block-scoped variable declaration is undefined"); | ||
if (!isDefinedBefore(declaration, errorLocation)) { | ||
|
||
// first check if usage is lexically located after the declaration | ||
var isUsedBeforeDeclaration = !isDefinedBefore(declaration, errorLocation); | ||
if (!isUsedBeforeDeclaration) { | ||
// lexical check succedded however code still can be illegal. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. succeeded |
||
// - block scoped variables cannot be used in its initializers | ||
// let x = x; // illegal but usage is lexically after definition | ||
// - in ForIn/ForOf statements variable cannot be contained in expression part | ||
// for (let x in x) | ||
// for (let x of x) | ||
|
||
// climb up to the variable declaration skipping binding patterns | ||
var variableDeclaration = <VariableDeclaration>getAncestor(declaration, SyntaxKind.VariableDeclaration); | ||
var container = getEnclosingBlockScopeContainer(variableDeclaration); | ||
|
||
if (variableDeclaration.parent.parent.kind === SyntaxKind.VariableStatement || | ||
variableDeclaration.parent.parent.kind === SyntaxKind.ForStatement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's variableDeclaration.parent? Is it the variable declaration list? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I noticed parameters are not handled. Is it okay for parameters to reference themselves in their default initializers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, variable declarations are parented by variable declaration lists |
||
// variable statement/for statement case, use site should not be inside initializer | ||
isUsedBeforeDeclaration = isChildNode(errorLocation, variableDeclaration.initializer, container); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @mhegazy said, the following case should not be an error: let x = () => x; |
||
} | ||
else if (variableDeclaration.parent.parent.kind === SyntaxKind.ForOfStatement || | ||
variableDeclaration.parent.parent.kind === SyntaxKind.ForInStatement) { | ||
// ForIn/ForOf case - use site should not be used in expression part | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix indentation of comment |
||
isUsedBeforeDeclaration = isChildNode(errorLocation, (<ForInStatement>variableDeclaration.parent.parent).expression, container); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like neither of these checks handle initializers nested inside binding patterns: let [x = x] = 0;
for (let [x = x] of []) { } There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I am not sure what is the desired behavior when a nested initializer uses a declaration from higher up in the binding pattern: let { x, y = x } = { };
let { y = x, x } = { };
let { x, y: { z = x } } = { }; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my spec reading-fu says that it should not be allowed:
and
BindingInitialization step is done as a last part of evaluation of LexicalBinding, so since evaluation is not yet finished - variables cannot be accessed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, so the distinction is between BindingPattern, which is a recursive construct, and LexicalBinding, which is not. In this case, the entire LexicalBinding must precede the use. |
||
} | ||
if (isUsedBeforeDeclaration) { | ||
error(errorLocation, Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationNameToString(declaration.name)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems very confusing to the user to call this "used before its declaration" |
||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/* Starting from 'initial' node walk up the parent chain until 'stopAt' node is reached. | ||
* If at any point current node is equal to 'parent' node - return true. | ||
* Return false if 'stopAt' node is reached. | ||
*/ | ||
function isChildNode(initial: Node, parent: Node, stopAt: Node): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isDescedentNode. Child means immediate child. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually isDescendentOf. Also, I would make stopAt optional. |
||
if (!parent) { | ||
return false; | ||
} | ||
for (var current = initial; current && current !== stopAt; current = current.parent) { | ||
if (current === parent) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
// An alias symbol is created by one of the following declarations: | ||
// import <symbol> = ... | ||
// import <symbol> from ... | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -203,6 +203,33 @@ module ts { | |
isCatchClauseVariableDeclaration(declaration); | ||
} | ||
|
||
export function getEnclosingBlockScopeContainer(node: Node): Node { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did anything change here (can't tell cuz it moved)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, code was just moved from emitter to utilities |
||
var current = node; | ||
while (current) { | ||
if (isFunctionLike(current)) { | ||
return current; | ||
} | ||
switch (current.kind) { | ||
case SyntaxKind.SourceFile: | ||
case SyntaxKind.CaseBlock: | ||
case SyntaxKind.CatchClause: | ||
case SyntaxKind.ModuleDeclaration: | ||
case SyntaxKind.ForStatement: | ||
case SyntaxKind.ForInStatement: | ||
case SyntaxKind.ForOfStatement: | ||
return current; | ||
case SyntaxKind.Block: | ||
// function block is not considered block-scope container | ||
// see comment in binder.ts: bind(...), case for SyntaxKind.Block | ||
if (!isFunctionLike(current.parent)) { | ||
return current; | ||
} | ||
} | ||
|
||
current = current.parent; | ||
} | ||
} | ||
|
||
export function isCatchClauseVariableDeclaration(declaration: Declaration) { | ||
return declaration && | ||
declaration.kind === SyntaxKind.VariableDeclaration && | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
tests/cases/conformance/es6/for-ofStatements/for-of55.ts(2,15): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
|
||
|
||
==== tests/cases/conformance/es6/for-ofStatements/for-of55.ts (1 errors) ==== | ||
let v = [1]; | ||
for (let v of v) { | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. | ||
v; | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
tests/cases/compiler/recursiveLetConst.ts(2,9): error TS2448: Block-scoped variable 'x' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(3,12): error TS2448: Block-scoped variable 'x1' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(4,11): error TS2448: Block-scoped variable 'y' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(5,14): error TS2448: Block-scoped variable 'y1' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(6,14): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(7,16): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(8,15): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(9,15): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
tests/cases/compiler/recursiveLetConst.ts(10,17): error TS2448: Block-scoped variable 'v' used before its declaration. | ||
|
||
|
||
==== tests/cases/compiler/recursiveLetConst.ts (9 errors) ==== | ||
'use strict' | ||
let x = x + 1; | ||
~ | ||
!!! error TS2448: Block-scoped variable 'x' used before its declaration. | ||
let [x1] = x1 + 1; | ||
~~ | ||
!!! error TS2448: Block-scoped variable 'x1' used before its declaration. | ||
const y = y + 2; | ||
~ | ||
!!! error TS2448: Block-scoped variable 'y' used before its declaration. | ||
const [y1] = y1 + 1; | ||
~~ | ||
!!! error TS2448: Block-scoped variable 'y1' used before its declaration. | ||
for (let v = v; ; ) { } | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. | ||
for (let [v] = v; ;) { } | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. | ||
for (let v in v) { } | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. | ||
for (let v of v) { } | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. | ||
for (let [v] of v) { } | ||
~ | ||
!!! error TS2448: Block-scoped variable 'v' used before its declaration. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
//// [recursiveLetConst.ts] | ||
'use strict' | ||
let x = x + 1; | ||
let [x1] = x1 + 1; | ||
const y = y + 2; | ||
const [y1] = y1 + 1; | ||
for (let v = v; ; ) { } | ||
for (let [v] = v; ;) { } | ||
for (let v in v) { } | ||
for (let v of v) { } | ||
for (let [v] of v) { } | ||
|
||
//// [recursiveLetConst.js] | ||
'use strict'; | ||
let x = x + 1; | ||
let [x1] = x1 + 1; | ||
const y = y + 2; | ||
const [y1] = y1 + 1; | ||
for (let v = v;;) { | ||
} | ||
for (let [v] = v;;) { | ||
} | ||
for (let v in v) { | ||
} | ||
for (let v of v) { | ||
} | ||
for (let [v] of v) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// @target:es6 | ||
'use strict' | ||
let x = x + 1; | ||
let [x1] = x1 + 1; | ||
const y = y + 2; | ||
const [y1] = y1 + 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about: This should be fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch, thanks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about That is a false negative case: it generates no TS error, but it does crash at runtime. That false negative is expected, and therefore it is important to test that it is indeed happening as expected. |
||
for (let v = v; ; ) { } | ||
for (let [v] = v; ;) { } | ||
for (let v in v) { } | ||
for (let v of v) { } | ||
for (let [v] of v) { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move everything guarded by
if (nameNotFoundMessage)
into another function?