From 478bf45be830c56aa828753876fca00a0fb7f59c Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Thu, 6 Aug 2026 10:57:09 +0200 Subject: [PATCH] fix(parser): inject params into one-line non-async arrow scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 --- lib/parser.js | 16 ++++++++++++++-- test/unit/parser_test.js | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index ea74ad0fc..6a6ead165 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -4,7 +4,8 @@ function _interopDefault(ex) { import * as acorn from 'acorn' import parseFunctionModule from 'parse-function' const parseFunction = _interopDefault(parseFunctionModule) -const parser = parseFunction({ parse: acorn.parse, ecmaVersion: 11, plugins: ['objectRestSpread'] }) +const ecmaVersion = 11 +const parser = parseFunction({ parse: acorn.parse, ecmaVersion, plugins: ['objectRestSpread'] }) import output from './output.js' parser.use(destructuredArgs) @@ -17,7 +18,7 @@ export const getParamsToString = function (fn) { function getParams(fn, { warnOnLegacyFormat = false } = {}) { if (fn.isSinonProxy) return [] try { - const reflected = parser.parse(fn) + const reflected = parser.parse(normalizeArrowFn(fn)) if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) { output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up') } @@ -38,6 +39,17 @@ function getParams(fn, { warnOnLegacyFormat = false } = {}) { export { getParams } +function normalizeArrowFn(fn) { + const code = (typeof fn === 'function' ? fn.toString() : String(fn)).trim() + if (!code.includes('=>') || code.startsWith('async')) return fn + try { + if (acorn.parseExpressionAt(code, 0, { ecmaVersion }).type !== 'ArrowFunctionExpression') return fn + } catch { + return fn + } + return `async ${code}` +} + function destructuredArgs() { return (node, result) => { result.destructuredArgs = result.destructuredArgs || [] diff --git a/test/unit/parser_test.js b/test/unit/parser_test.js index 32e39b5d5..78d712cde 100644 --- a/test/unit/parser_test.js +++ b/test/unit/parser_test.js @@ -40,5 +40,19 @@ describe('parser', () => { it('should get params for class method with destructured args', () => { expect(getParams(obj.method5)).to.eql(['locator', 'sec']) }) + + // prettier-ignore + const fixturesOneLineArrows = [ + ['destructured args and a condition', ({ locator, sec }) => { if (true) { return locator } }, ['locator', 'sec']], + ['a single arg and a condition', locator => { if (true) { return locator } }, ['locator']], + ['multiple args and a loop', (locator, sec) => { for (;;) { return locator || sec } }, ['locator', 'sec']], + ['a nested arrow function', ({ locator, sec }) => { [locator].forEach((l) => { if (l) { return sec } }) }, ['locator', 'sec']], + ] + + fixturesOneLineArrows.forEach(([title, fn, params]) => { + it(`should get params for one-line arrow function with ${title}`, () => { + expect(getParams(fn)).to.eql(params) + }) + }) }) })