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

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?

The tokenizer matched a JS regexp literal with re.DOTALL, so an
unterminated match could run past line terminators and swallow the
rest of the file. A literal like /'/ inside a ternary made
extract_javascript drop every gettext message that followed it
(issue python-babel#640).

A JS regexp literal cannot contain an ECMAScript line terminator - CR,
LF, U+2028 or U+2029 - and a backslash does not continue one onto the
next line, so drop DOTALL and exclude all four terminators from the
ordinary and the escaped character classes. This restores extraction of
the messages after such a literal; test_jsx_extraction now expects the
same messages with and without the jsx option.

Signed-off-by: simpleqt <2015900818@qq.com>
@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
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants