Repository navigation
[JSC] Parser: in a generator, reject yield in the parameters of an arrow function nested in another's parameters
#702
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
Open
robobun
wants to merge
6
commits into
main
Choose a base branch
from
robobun/49bd07c0/yield-in-nested-arrow-parameters
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
6272490
[JSC] Parser: in a generator, reject `yield` in the parameters of an …
robobun 35a655d
[JSC] Parser: an arrow function with `yield` as an identifier in its …
robobun b7b3646
[JSC] Parser: keep the SourceProviderCache item of an arrow function …
robobun 6c4442b
[JSC] Parser: note `yield` as an identifier where it is bound or refe…
robobun 50ac435
[JSC] Parser: an escaped `yield` as a shorthand property does not mar…
robobun 54004fe
[JSC] Parser: isArrowFunctionParameters() parses the parameters with …
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
221 changes: 221 additions & 0 deletions
221
JSTests/stress/yield-in-arrow-function-nested-in-arrow-function-parameters.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| // The parameters of an arrow function in a generator are parsed with [+Yield]: `yield` is not an identifier in them, and a | ||
| // YieldExpression is an early error. That holds for an arrow function in the parameters of another arrow function too. | ||
| // | ||
| // The parser first reads "( ... )" as an expression. To see that it is a parameter list it parses it again, in a scope | ||
| // that is never a generator, and an arrow function in there goes to the source provider cache with `yield` as an | ||
| // identifier. The parse that knows about the generator then skipped that arrow function from the cache, so all of the | ||
| // code below that must throw was accepted. It was always rejected with --useSourceProviderCache=false, and the messages | ||
| // here are the ones that run gives. | ||
|
|
||
| function shouldBe(actual, expected, what) { | ||
| if (actual !== expected) | ||
| throw new Error(`${what}: expected ${String(expected)} but got ${String(actual)}`); | ||
| } | ||
|
|
||
| function shouldThrowSyntaxError(source, message) { | ||
| let error; | ||
| try { | ||
| (0, eval)(source); | ||
| } catch (e) { | ||
| error = e; | ||
| } | ||
| if (!(error instanceof SyntaxError)) | ||
| throw new Error(`${source}: expected a SyntaxError but got ${String(error)}`); | ||
| if (message !== undefined) | ||
| shouldBe(error.message, message, source); | ||
| } | ||
|
|
||
| function shouldParse(source) { | ||
| try { | ||
| (0, eval)(source); | ||
| } catch (e) { | ||
| if (e instanceof SyntaxError) | ||
| throw new Error(`${source}: ${String(e)}`); | ||
| } | ||
| } | ||
|
|
||
| const parameterName = "Cannot use 'yield' as a parameter name in a generator function."; | ||
| const yieldExpression = "Unexpected keyword 'yield'. Cannot use yield expression out of generator."; | ||
| const shorthand = "Cannot use abbreviated destructuring syntax for keyword 'yield'."; | ||
| const shorthandProperty = "Cannot use 'yield' as a shorthand property name in a generator function."; | ||
| const escaped = "Unexpected escaped characters in keyword token: 'yi\\u0065ld'"; | ||
|
|
||
| // [text before the expression, text after it]. The messages below are those of the first one. | ||
| const generators = [ | ||
| ["(function* () { (", "); })"], | ||
| ["(function* () { return ", "; })"], | ||
| ["(function* (p = ", ") { })"], | ||
| ["function* g() { (", "); }"], | ||
| ["({ *g() { (", "); } })"], | ||
| ["({ *g(p = ", ") { } })"], | ||
| ["(class { *g() { (", "); } })", "strict"], | ||
| ["(class { static *g(p = ", ") { } })", "strict"], | ||
| ["(async function* () { (", "); })"], | ||
| ["(async function* (p = ", ") { })"], | ||
| ["(function* () { { let q; (", "); } })"], | ||
| ["(function* (p = function* () { (", "); }) { })"], | ||
| ]; | ||
|
|
||
| // [expression, message, true if the expression is not valid where `yield` is an identifier either] | ||
| const invalid = [ | ||
| // The arrow function is in the parameters of another arrow function. | ||
| ["(a = (yield) => 1) => a", parameterName], | ||
| ["(a = (b = yield) => b) => a", yieldExpression], | ||
| ["(a = (b = yield 1) => b) => a", "Unexpected number '1'. Expected ')' to end a compound expression.", true], | ||
| ["(a = (yield = 1) => 1) => a", parameterName], | ||
| ["(a = (...yield) => 1) => a", parameterName], | ||
| ["(a = ([yield]) => 1) => a", parameterName], | ||
| ["(a = ({ yield }) => 1) => a", shorthand], | ||
| ["(a = ({ b = yield }) => 1) => a", yieldExpression], | ||
| ["(a = async (yield) => 1) => a", parameterName], | ||
| ["(a = async (b = yield) => b) => a", yieldExpression], | ||
| ["(b = (yield) => 1, c) => b", parameterName], | ||
| ["(c, b = (yield) => 1) => b", parameterName], | ||
| ["({ a = (yield) => 1 }) => a", parameterName], | ||
| ["({ k: a = (b = yield) => b }) => a", yieldExpression], | ||
| ["([a = (yield) => 1]) => a", parameterName], | ||
| ["(...[a = (yield) => 1]) => a", parameterName], | ||
| ["async (a = (yield) => 1) => a", parameterName], | ||
| ["async ({ a = (b = yield) => b }) => a", yieldExpression], | ||
| // Other ways to use `yield` as an identifier. | ||
| ["(a = (b = { yield }) => b) => a", shorthandProperty], | ||
| ["(a = (b = ({ yield } = { })) => b) => a", shorthandProperty], | ||
| ["(a = (b = [yield] = []) => b) => a", yieldExpression], | ||
| ["(a = (yi\\u0065ld) => 1) => a", escaped], | ||
| ["(a = (b = yi\\u0065ld) => b) => a", escaped], | ||
| // Deeper. | ||
| ["(a = (b = (yield) => 1) => b) => a", parameterName], | ||
| ["(a = (b = (c = yield) => c) => b) => a", yieldExpression], | ||
| ["(a = (b = (c = (d = (yield) => 1) => d) => c) => b) => a", parameterName], | ||
| ["({ x = 1 }, a = (b = (c = yield) => c) => b) => a", yieldExpression], | ||
| // "( ... )" is not a parameter list, but the parser has to try that to find out. The enclosing pattern is parsed twice. | ||
| ["[a = (b = (yield) => 1)] = []", parameterName], | ||
| ["[a = ({ [(yield) => 1]: 1 })] = []", parameterName], | ||
| ["[a = (b = (c = yield) => c)] = []", yieldExpression], | ||
| // One arrow function. These were rejected before, and the messages are the same. | ||
| ["(yield) => 1", parameterName], | ||
| ["(a = yield) => a", yieldExpression], | ||
| ["({ a = yield }) => a", yieldExpression], | ||
| ["(...[a = yield]) => a", yieldExpression], | ||
| ]; | ||
|
|
||
| for (const [before, after] of generators) { | ||
| for (const [expression, message] of invalid) | ||
| shouldThrowSyntaxError(before + expression + after, before === generators[0][0] ? message : undefined); | ||
| } | ||
|
|
||
| // `yield` is an identifier in the body of an arrow function, in a function that is not an arrow function, and in an arrow | ||
| // function that is in one of those. | ||
| const valid = [ | ||
| "(a = (b) => yield) => a", | ||
| "(a = (b) => { var yield; }) => a", | ||
| "(a = (b) => { (yield) => 1; }) => a", | ||
| "(a = (b) => (c = (yield) => 1) => c) => a", | ||
| "(a = function (yield) { yield; }) => a", | ||
| "(a = function (b = yield) { }) => a", | ||
| "(a = function () { (b = (yield) => 1) => b; }) => a", | ||
| "(a = function* () { yield 1; }) => a", | ||
| "(a = { yield: 1 }.yield) => a", | ||
| // `yield` as a property name is not an identifier. | ||
| "(a = ({ yield: b }) => b) => a", | ||
| "(a = ({ yield: b = 1 }, c = (d = b) => d) => c) => a", | ||
| "(a = (b = { yield: 1, get yield() { return 1; }, set yield(v) { }, yield() { } }) => b) => a", | ||
| "(a = (b = ({ yield: c } = { })) => b) => a", | ||
| "(a = (b = c.yield) => b) => a", | ||
| "(a = (b = function yield() { }) => b) => a", | ||
| "({ x = 1 }, a = (b = (c = 1) => c) => b) => a", | ||
| "[a = (b = (c = 1) => c)] = []", | ||
| ]; | ||
|
|
||
| for (const [before, after, strict] of generators) { | ||
| // `yield` is a reserved word in strict mode code. | ||
| if (strict) | ||
| continue; | ||
| for (const expression of valid) | ||
| shouldParse(before + expression + after); | ||
| } | ||
|
|
||
| // Outside a generator `yield` is an identifier in sloppy mode, at every level. | ||
| for (const [expression, , neverValid] of invalid) { | ||
| if (neverValid) | ||
| continue; | ||
| shouldParse(`(function () { (${expression}); })`); | ||
| shouldParse(`(function* () { () => { (${expression}); }; })`); | ||
| shouldParse(`(function* () { function f() { (${expression}); } })`); | ||
| shouldThrowSyntaxError(`(function () { "use strict"; (${expression}); })`); | ||
| } | ||
|
|
||
| // The arrow functions do what they say. One with `yield` as an identifier in its parameters still has its item in the source | ||
| // provider cache. Only a generator does not use it. | ||
| { | ||
| const f = (a = (yield) => yield + 1) => a; | ||
| shouldBe(f()(41), 42, "yield as a parameter name"); | ||
|
|
||
| function* inArrowBody() { | ||
| return () => (a = (yield) => yield * 2) => a; | ||
| } | ||
| shouldBe(inArrowBody().next().value()()(21), 42, "yield as a parameter name in the body of an arrow function in a generator"); | ||
|
|
||
| const nest = (a = (yield = 1) => (b = (c = yield + 1) => c) => b) => a; | ||
| shouldBe(nest()()()(), 2, "yield as a parameter name and in a default value"); | ||
|
|
||
| function yieldAsVariable() { | ||
| var yield = 5; | ||
| return ((a = (b = yield) => b * 2) => a)()(); | ||
| } | ||
| shouldBe(yieldAsVariable(), 10, "yield as a variable in a default value"); | ||
|
|
||
| // "{ x = 1 }" is not an expression, so the nested arrow functions are first parsed in the scope that is not a generator. | ||
| const captures = `(function* (p) { | ||
| let local = 10; | ||
| const first = yield; | ||
| return ({ x = 1 }, a = (b = (c = x + local + p + first) => c) => b) => a; | ||
| })`; | ||
| for (const dropCache of [false, true]) { | ||
| const generator = (0, eval)(captures)(100); | ||
| generator.next(); | ||
| const arrow = generator.next(1000).value; | ||
| // A full collection drops the source provider cache. An arrow function is compiled when it is first called, and | ||
| // parses its text again then. | ||
| if (dropCache) | ||
| fullGC(); | ||
| shouldBe(arrow({ })()(), 1111, "captured variables"); | ||
| shouldBe(arrow({ x: 2 })()(), 1112, "captured variables"); | ||
| } | ||
|
|
||
| // A generator still takes a valid arrow function from the cache: one with `yield` as a property name only, and one with | ||
| // no `yield` at all. This shows in what the default value reads. It reads N of the generator, and the body declares an N | ||
| // of its own. With the item the generator knows that its N is captured. When the parser has to parse the arrow function | ||
| // to get there, the generator does not know it (a bug of that parse), and N is not defined. | ||
| if (jscOptions().useSourceProviderCache) { | ||
| function* yieldAsPropertyName() { | ||
| let N = 40; | ||
| return ({ x = 1 }, a = ({ yield: b } = { yield: N }) => { var N = 7; return b; }) => a; | ||
| } | ||
| shouldBe(yieldAsPropertyName().next().value({ })(), 40, "yield as a property name in a pattern"); | ||
|
|
||
| function* noYield() { | ||
| let N = 40; | ||
| return ({ x = 1 }, a = (b = N + x) => { var N = 7; return b; }) => a; | ||
| } | ||
| shouldBe(noYield().next().value({ })(), 41, "a default value that reads a variable that the body declares again"); | ||
| } | ||
|
|
||
| class Base { | ||
| name() { return "base"; } | ||
| } | ||
| class Derived extends Base { | ||
| *method() { | ||
| return ({ x = 1 }, a = (b = (c = [this.field, arguments[0], super.name(), x]) => c) => b) => a; | ||
| } | ||
| name() { return "derived"; } | ||
| } | ||
| const derived = new Derived; | ||
| derived.field = "field"; | ||
| shouldBe(JSON.stringify(derived.method("argument").next().value({ })()()), `["field","argument","base",1]`, "this, arguments and super"); | ||
|
|
||
| function* withEval(p) { | ||
| return ({ x = 1 }, a = (b = (c = eval("x + p")) => c) => b) => a; | ||
| } | ||
| shouldBe(withEval(41).next().value({ })()(), 42, "eval"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.