Skip to content

jslexer: stop regexp literals from spanning lines - #1392

Open
simpleqt wants to merge 1 commit into
python-babel:masterfrom
simpleqt:fix/jslexer-regex-literal-newline
Open

simpleqt wants to merge 1 commit into
python-babel:masterfrom
simpleqt:fix/jslexer-regex-literal-newline

Conversation

@simpleqt

@simpleqt simpleqt commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fix jslexer regexp literals spanning lines (issue #640)

The tokenizer matched a JS regexp literal with re.DOTALL, so a match attempt could run past line terminators and swallow everything up to the next / on a later line. That silently dropped every gettext message between such literals — exactly the symptom in #640, where the message between />/g and /'/g disappears.

A JS regexp literal cannot contain any of the ECMAScript line terminators — U+000A, U+000D, U+2028 or U+2029 — and a backslash does not continue one onto the next line, so dropping DOTALL and excluding all four terminators from both the ordinary and the escaped character classes bounds a failed match to a single line. With the source from #640, for LF, CR, CRLF, U+2028 and U+2029 alike:

  • before: extracts ['First message', 'Third message'] (matches the report's POT output)
  • after: extracts ['First message', 'Second message', 'Third message']

Thanks to @carlitogwapito, who measured that CR, U+2028 and U+2029 still matched — in the ordinary path and immediately after a backslash — and supplied failing cases for them.

The new test_regexp_literal_cannot_span_lines and test_regexp_literal_cannot_continue_after_a_backslash pin the tokenizer behaviour for all four terminators, test_ordinary_regexp_literals_still_match keeps escaped slashes and flags working, and test_regexp_literals_do_not_swallow_messages covers both regexp shapes from #640 at the extraction level, parametrised over the terminators.

One existing assertion changes meaning: test_jsx_extraction's source also contains regexp literals, and the buggy lexer dropped messages from it in non-JSX mode, which is what the old != assertion was locking in. With extraction fixed, non-JSX now yields the same messages as JSX, so the assertion becomes == (kept as if/else so both parametrised cases stay visible in the report).

Prior art: #791 rewrites this pattern to handle / inside character classes, but it keeps DOTALL (its character classes also match newlines), so it does not address the newline case in #640. The two fixes are independent; mine only changes line-terminator handling.

I used an AI assistant (ZCode) to help locate the swallowing behavior in the tokenizer and to draft the tests; I verified the before/after behavior above myself and wrote the fix.

Copilot AI balanced review requested due to automatic review settings October 16, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@carlitogwapito

Copy link
Copy Markdown

A small line-terminator test, if it's useful.

The LF cases pass with this change, but the regexp rule still matches across CR, U+2028 and U+2029, including immediately after a backslash. The test covers all four terminators plus a normal-regexp control.

On Windows/Python 3.14.4, six cases still fail on this head; both LF cases and the control pass. The full Babel/CLDR suite wasn't run.

Would you consider excluding those other ECMAScript line terminators from both the ordinary and backslash paths too?

@simpleqt
simpleqt force-pushed the fix/jslexer-regex-literal-newline branch from c6e18a1 to 2ef3aef Compare October 23, 2026 13:10
@simpleqt

Copy link
Copy Markdown
Contributor Author

All four terminators are handled now (2ef3aef) - thank you for the failing cases, they were exactly right and I had only ever tested the LF path.

What changed, following your split:

  • ordinary path: [^/\\\n] → [^/\\\r\n\u2028\u2029]
  • backslash path: \\. → \\[^\r\n\u2028\u2029], so \ followed by a line terminator no longer continues the literal (it is a syntax error in a regexp literal, so the tokenizer must not treat it as one)

Both of your failing shapes are now red on the old head and green on this one: 6 failed / 10 passed on c6e18a1 for \r, \u2028, \u2029 (LF and CRLF already passed), 16 passed with the fix. The CRLF case needed no separate handling - the literal can't contain the \n either.

Tests added, mirroring your test:

  • test_regexp_literal_cannot_span_lines and test_regexp_literal_cannot_continue_after_a_backslash, both parametrized over \n, \r, \r\n, \u2028, \u2029; the second one puts the terminator directly after a backslash
  • test_ordinary_regexp_literals_still_match keeps the control case (/a\/b+c/gi) green
  • test_regexp_literals_do_not_swallow_messages is now parametrized over the same terminators

Verification: pytest tests/messages/test_jslexer.py tests/messages/test_js_extract.py → 36 passed; ruff check at the pre-commit pin (0.14.10) → clean. I did not run the wider suite to a meaningful result in this checkout - tests/messages needs the compiled CLDR data (a bare clone fails test_plurals at collection with UnknownLocaleError), so CI is the authority there.

I amended the single commit rather than adding a fixup, so the branch stays one commit; the PR description now covers all four terminators and credits your measurement.

@akx akx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase and drop the specific jslexer tests. It's enough to test the extraction result.

A regexp literal cannot contain an ECMAScript line terminator, and a
backslash cannot escape one either, so the fallback regexp rule must not
match across a line break; it previously swallowed the rest of the source
and silently dropped every gettext() call it contained (issue python-babel#640).

Per review, drop the jslexer-level token assertion tests; the extraction
result is what matters.
@simpleqt
simpleqt force-pushed the fix/jslexer-regex-literal-newline branch from 2ef3aef to 33328b9 Compare November 1, 2026 10:11
@simpleqt

simpleqt commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current master (ecf1da8) and dropped the jslexer-internal tests as you asked.

  • All 11 tests this PR added to tests/messages/test_jslexer.py are gone - they asserted on the token stream, and that file is now byte-identical to master. The new module-level pytest import went with them.
  • Kept test_js_extract.py::test_regexp_literals_do_not_swallow_messages, which is the extraction-result assertion (five line terminators: \n, \r, \r\n, U+2028, U+2029, all expecting ["First message", "Second message", "Third message"]).
  • One hunk I did keep and want to flag, because it is not a jslexer-internal test: test_jsx_extraction[False] on master asserts messages != EXPECTED_JSX_MESSAGES, i.e. it pins the buggy behaviour. This PR changes that comparison to ==. Without it the suite is red after the rebase, so I kept it, but it is your call - say so and I will drop it instead.

The production change is unchanged. It is still needed after the rebase: #1288 had already fixed the // and <!-- rules, so regex_re was the last rule able to swallow a line terminator.

Verified: extraction tests 129 passed, 1 skipped (124 before); the three JS files 57 passed (52 before); full suite 7784 passed, 9 skipped, 2 xfailed; ruff check clean. Red/green holds - with jslexer.py reverted to master and the tests in place, 6 fail (the five terminator parameters plus test_jsx_extraction[False]), and all pass with the fix.

@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.56%. Comparing base (ecf1da8) to head (33328b9).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1392   +/-   ##
=======================================
  Coverage   93.56%   93.56%           
=======================================
  Files          27       27           
  Lines        4943     4943           
=======================================
  Hits         4625     4625           
  Misses        318      318           
Flag Coverage Δ
macos-14-3.10 92.65% <100.00%> (ø)
macos-14-3.11 92.59% <100.00%> (ø)
macos-14-3.12 92.79% <100.00%> (ø)
macos-14-3.13 92.79% <100.00%> (ø)
macos-14-3.14 92.77% <100.00%> (ø)
macos-14-3.15 92.77% <100.00%> (ø)
macos-14-3.8 92.53% <100.00%> (ø)
macos-14-3.9 92.59% <100.00%> (ø)
macos-14-pypy3.10 92.65% <100.00%> (ø)
ubuntu-24.04-3.10 92.67% <100.00%> (ø)
ubuntu-24.04-3.11 92.61% <100.00%> (ø)
ubuntu-24.04-3.12 92.81% <100.00%> (ø)
ubuntu-24.04-3.13 92.81% <100.00%> (ø)
ubuntu-24.04-3.14 92.79% <100.00%> (ø)
ubuntu-24.04-3.15 92.79% <100.00%> (ø)
ubuntu-24.04-3.8 92.55% <100.00%> (ø)
ubuntu-24.04-3.9 92.61% <100.00%> (ø)
ubuntu-24.04-pypy3.10 92.67% <100.00%> (ø)
windows-2022-3.10 92.66% <100.00%> (ø)
windows-2022-3.11 92.60% <100.00%> (ø)
windows-2022-3.12 92.81% <100.00%> (ø)
windows-2022-3.13 92.81% <100.00%> (ø)
windows-2022-3.14 92.79% <100.00%> (ø)
windows-2022-3.15 92.79% <100.00%> (ø)
windows-2022-3.8 92.64% <100.00%> (ø)
windows-2022-3.9 92.60% <100.00%> (ø)
windows-2022-pypy3.10 92.66% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed

codspeed Bot commented Nov 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 88 untouched benchmarks


Comparing simpleqt:fix/jslexer-regex-literal-newline (33328b9) with master (ecf1da8)

Open in CodSpeed

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants