Skip to content

fix(parser): inject params into one-line non-async arrow scenarios - #5680

Open
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5679-oneline-arrow-params
Open

fix(parser): inject params into one-line non-async arrow scenarios#5680
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5679-oneline-arrow-params

Conversation

@mirao

@mirao mirao commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5679

Problem

Scenario('...', ({ I }) => { if (true) { I.say('hello') } }) injects nothing — I, page objects and current are all undefined, and the test dies with TypeError: Cannot read properties of undefined.

parse-function@5.6.10 decides whether its input is an ES6 object method with:

const isMethod = /^\*?.+\([\S\W]*\)\s*{/i.test(result.value)
if (!(isFunction || isAsyncFn || isAsyncArrow) && isMethod) result.value = `{ ${result.value} }`

The greedy .+ combined with [\S\W]* matches any source containing a ) { sequence, so a non-async arrow whose body holds an if, for, while or switch is wrapped in braces as a fake object method and acorn throws. getParams() logs the SyntaxError and returns undefined.

Async arrows escape via the library's own isAsyncArrow check. In plain JavaScript a multi-line arrow escapes too, because . does not cross a newline. That second escape does not exist under TypeScripttsx/esbuild emit every function on one line, so fn.toString() returns the one-line form however the source was written. In a TypeScript suite every non-async scenario with destructured params and a conditional fails, which is why this surfaced as a 4.0 regression.

Fix

normalizeArrowFn() asks acorn whether the source really is an ArrowFunctionExpression and, if so, hands parse-function async <source> so it takes the isAsyncArrow branch instead of the method-wrapping one.

Anything acorn does not confirm as an arrow — class methods, generators, function expressions, strings, unparseable input — is returned untouched, so only input that already fails is affected. The prefix cannot change a parameter list, and default values stay correct because their offsets are sliced from the same prefixed string.

ecmaVersion becomes a shared const so the parse and the arrow check cannot drift apart. Its value is unchanged, so no syntax gains or loses parseability.

Verification

Check Result
New tests with lib/parser.js reverted 4 failing
New tests with the fix 12 passing (8 pre-existing + 4 new)
Differential vs. the old parser, 47 function shapes, both getParams and getParamsToString 0 regressions, 15 fixes
npm run test:unit 773 passed, 0 failed
npm run test:runner 276 passed, 0 failed
Repro from the issue, incl. Data().Scenario(({ current }) => ...) 4 passed, params injected

The differential corpus covered class methods, async/static methods, getters, generators and async generators, function expressions, expression-body arrows, rest params, default values (numeric, string, call expressions), nested arrows, strings containing =>, sinon proxies, string inputs and garbage input.

Not included

A one-line arrow whose body uses ES2021+ syntax (??=, ||=, numeric separators, class fields) still gets no params, because ecmaVersion: 11 pins the parser to ES2020. That is a separate, pre-existing defect with the same symptom — unrelated to the isMethod misdetection and unchanged by this PR. Raising it to 'latest' fixes it and was green on both suites, but it is deliberately left out to keep this PR to the reported bug. Happy to add it here or in a follow-up if maintainers prefer.

🤖 Generated with Claude Code

parse-function@5.6.10 decides whether its input is an ES6 object method with
`/^\*?.+\([\S\W]*\)\s*{/`. The greedy `.+` combined with `[\S\W]*` matches any
source containing a `) {` sequence, so a non-async arrow whose body holds an
`if`, `for`, `while` or `switch` gets wrapped in braces as a fake object method
and acorn throws. getParams() then returns undefined and nothing is injected —
`I`, page objects and `current` are all undefined when the test runs.

Async arrows escape through the library's own isAsyncArrow check, and in plain
JavaScript a multi-line arrow escapes as well, because `.` does not cross a
newline so the greedy `.+` cannot reach past the first line. That second escape
does not exist under TypeScript: tsx/esbuild emit every function on one line, so
fn.toString() returns the one-line form however the source was written, and
every non-async scenario with destructured params and a conditional fails.

normalizeArrowFn() asks acorn whether the source really is an
ArrowFunctionExpression and, if so, hands parse-function `async <source>` so it
takes the isAsyncArrow branch. Anything acorn does not confirm as an arrow —
class methods, generators, function expressions, strings, unparseable input — is
returned untouched, so only input that fails today is affected. The prefix cannot
change a parameter list, and default values stay correct because their offsets
are sliced from the same prefixed string.

ecmaVersion becomes a shared const so the parse and the arrow check cannot drift
apart; its value is unchanged, so no syntax gains or loses parseability.

Fixes codeceptjs#5679

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Since 4.0: Scenario with a non-async one-line arrow gets no injected parameters (parse-function isMethod misdetection)

1 participant