Skip to content

Commit 478bf45

Browse files
Jaromir Obrclaude
andcommitted
fix(parser): inject params into one-line non-async arrow scenarios
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 #5679 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8b91815 commit 478bf45

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

lib/parser.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ function _interopDefault(ex) {
44
import * as acorn from 'acorn'
55
import parseFunctionModule from 'parse-function'
66
const parseFunction = _interopDefault(parseFunctionModule)
7-
const parser = parseFunction({ parse: acorn.parse, ecmaVersion: 11, plugins: ['objectRestSpread'] })
7+
const ecmaVersion = 11
8+
const parser = parseFunction({ parse: acorn.parse, ecmaVersion, plugins: ['objectRestSpread'] })
89
import output from './output.js'
910

1011
parser.use(destructuredArgs)
@@ -17,7 +18,7 @@ export const getParamsToString = function (fn) {
1718
function getParams(fn, { warnOnLegacyFormat = false } = {}) {
1819
if (fn.isSinonProxy) return []
1920
try {
20-
const reflected = parser.parse(fn)
21+
const reflected = parser.parse(normalizeArrowFn(fn))
2122
if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
2223
output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
2324
}
@@ -38,6 +39,17 @@ function getParams(fn, { warnOnLegacyFormat = false } = {}) {
3839

3940
export { getParams }
4041

42+
function normalizeArrowFn(fn) {
43+
const code = (typeof fn === 'function' ? fn.toString() : String(fn)).trim()
44+
if (!code.includes('=>') || code.startsWith('async')) return fn
45+
try {
46+
if (acorn.parseExpressionAt(code, 0, { ecmaVersion }).type !== 'ArrowFunctionExpression') return fn
47+
} catch {
48+
return fn
49+
}
50+
return `async ${code}`
51+
}
52+
4153
function destructuredArgs() {
4254
return (node, result) => {
4355
result.destructuredArgs = result.destructuredArgs || []

test/unit/parser_test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,19 @@ describe('parser', () => {
4040
it('should get params for class method with destructured args', () => {
4141
expect(getParams(obj.method5)).to.eql(['locator', 'sec'])
4242
})
43+
44+
// prettier-ignore
45+
const fixturesOneLineArrows = [
46+
['destructured args and a condition', ({ locator, sec }) => { if (true) { return locator } }, ['locator', 'sec']],
47+
['a single arg and a condition', locator => { if (true) { return locator } }, ['locator']],
48+
['multiple args and a loop', (locator, sec) => { for (;;) { return locator || sec } }, ['locator', 'sec']],
49+
['a nested arrow function', ({ locator, sec }) => { [locator].forEach((l) => { if (l) { return sec } }) }, ['locator', 'sec']],
50+
]
51+
52+
fixturesOneLineArrows.forEach(([title, fn, params]) => {
53+
it(`should get params for one-line arrow function with ${title}`, () => {
54+
expect(getParams(fn)).to.eql(params)
55+
})
56+
})
4357
})
4458
})

0 commit comments

Comments
 (0)