Skip to content

fix: unpaired apostrophe in JavaScript comments#370

Open
rtritto wants to merge 6 commits into
bgub:mainfrom
rtritto:fix-unpaired-apostrophe
Open

fix: unpaired apostrophe in JavaScript comments#370
rtritto wants to merge 6 commits into
bgub:mainfrom
rtritto:fix-unpaired-apostrophe

Conversation

@rtritto

@rtritto rtritto commented Jun 4, 2026

Copy link
Copy Markdown

Fix #337

Copilot AI review requested due to automatic review settings June 4, 2026 18: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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the Eta template parser to recognize and skip JavaScript-style single-line (// ...) comments while scanning template code blocks, preventing premature tag-closing detection inside line comments.

Changes:

  • Extend the close-token regex to detect // comment starts.
  • Add logic to advance the parser index to the next line terminator when // is encountered.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/parse.ts
@rtritto
rtritto requested a review from Copilot June 4, 2026 18:11

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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread src/parse.ts Outdated
Comment thread src/parse.ts Outdated
Comment thread src/parse.ts Outdated
@rtritto
rtritto requested a review from Copilot June 4, 2026 18:27

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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@jayaddison

Copy link
Copy Markdown

@rtritto I think we'll want test coverage in parse.spec.ts to demonstrate the problem and confirm the fix.

Also: I am worried about edge cases such as open-close syntax elements (such as the apostrophe, double-quote, and so on) that themselves contain comments -- e.g. a string such as 'this is the start of the string and there is a comment /* here */ within it' -- or variants of that.

I'll try to find some time to explore other examples that might cause the parser to misinterpret the input; trying to find those should allow us to make the parser more robust.

@rtritto

rtritto commented Jun 5, 2026

Copy link
Copy Markdown
Author

@jayaddison add some test on parse.spec.ts

@jayaddison

Copy link
Copy Markdown

Sorry for not getting to this sooner; I am beginning to examine the code for similar problems now. Thank you for adding some test coverage.

Comment thread test/parse.spec.ts Outdated
const buff = eta.parse("hi <% // comment with unpaired apostrophe' \n %>");
expect(buff).toEqual([
"hi ",
{ val: "// comment with unpaired apostrophe' \n ", t: "e" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@bgub I'm learning the codebase and parsing behaviour in order to provide feedback to @rtritto about this pull request.

From what I've understood so far: a parse result with t: "e" indicates a token that has type exec / evaluate. I think that means that the comment itself will be evaluated by the JavaScript engine.

That's probably fine, although I wonder: could we omit the parsed comments entirely, and skip evaluation of them? (saving a few evaluations, potentially for large/descriptive text such as license templates or lengthy explanatory comments?)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I now think that this would only affect comments inside template blocks - e.g. within <%...%> segments.

I wouldn't expect many or extensive comments within those, generally -- so perhaps there is not much likely performance improvement (in typical usage) available here.

Comment thread test/parse.spec.ts
});

it("works with unpaired apostrophe in multiline comment", () => {
const buff = eta.parse("hi <% /* comment with unpaired apostrophe' */ %>");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@rtritto your fix works for double-quotes in addition to apostrophes, and I think that might be worth demonstrating too.

Another set of scenarios I'll look into is cases where there are mixed // and /* comments, including interleaving of those (for example -- start /* // middle */ end).

@jayaddison

Copy link
Copy Markdown

NB: I think that this changeset also resolves #336 (a closed bug, although replicable against v4.6.0 during my testing).

Comment thread src/parse.ts

const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;

const lineTerminatorReg = /(?:\r\n|[\n\r\u2028\u2029])/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This makes me think that we might be missing the U+2028 / U+2029 line terminators from the end-quote regex patterns (singleQuoteReg, doubleQuoteReg). I haven't explored this yet but would like to.

Comment thread src/parse.ts

const doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;

const lineTerminatorReg = /(?:\r\n|[\n\r\u2028\u2029])/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The regex itself seems correct to me. There might be a way to use the /.*/ pattern as an optimisation later; that should match on everything except for line terminators -- which are exactly the set of things this regex searches for.

The regex consumes CRLF when it is found, which is potentially helpful, because it'll move the search needle entirely past the linebreak to the following line's content.. although makes the code slightly less platform-agnostic.

Comment thread test/parse.spec.ts
});

it("works with unpaired apostrophe in single-line comment", () => {
const buff = eta.parse("hi <% // comment with unpaired apostrophe' \n %>");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One more request for a test case: please could we add a (negative) test case where a single-line comment extends all the way to the end of the template?

In other words, something like:

const buff = eta.parse("hi <% // comment with unpaired apostrophe'");

This should raise an unclosed tag parse error exception.

The reason I'm asking: I think it might be possible to break out of one of the loops as a potential optimisation, at a later date. But let's add test coverage on the scenario to confirm the behaviour, before doing that.

Comment thread test/parse.spec.ts Outdated
const buff = eta.parse("hi <% /* comment with unpaired apostrophe' */ %>");
expect(buff).toEqual([
"hi ",
{ val: "/* comment with unpaired apostrophe' */ ", t: "e" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NB: the parsed val result seems to be trimmed of surrounding whitespace:

Suggested change
{ val: "/* comment with unpaired apostrophe' */ ", t: "e" },
{ val: "/* comment with unpaired apostrophe' */", t: "e" },

(this affects the previous test case expectations too)

@jayaddison

Copy link
Copy Markdown

I've been thinking about why the string-and-comment parsing logic was introduced/updated, particularly after looking at commit 78c256c.

I think it's so that templates can contain strings/comments that themselves include the same symbols used for template tags (e.g. <% and %>).

So: some test coverage for situations like that could also make sense; it's not necessarily directly related to the fix here, so we should probably do that in a separate pull request.

cc @bgub, partly to check if my theory about the parsing logic is anywhere near accurate

@jayaddison jayaddison 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.

I think that this fixes the bug, so from that point of view I'm ready to approve it.

However: my memory is that parsing of source code syntax is rarely safe using regular expressions, because most languages have context-sensitive syntax trees, where the behaviour (mode/context) of the parser changes as it reads and interprets certain tokens. Comments and string literals are two of those.

The eta parser does produce a context-type indicator (e.g. i, e, ...) along with each value, but without knowing that it is/uses a generalised JavaScript parser, I don't have strong confidence that all language syntax quirks are captured -- and that could mean that there are ways to confuse/bypass the regex patterns and inject or obscure the behaviour of code.

So I am prepared to approve this fix in-the-small (after the test cases are updated/expanded), but I would personally remain cautious about using it in important production code.

It is possible that there exist collections of JavaScript and other language code that are designed to be adversarial and break parsers/interpreters in unexpected ways -- I don't know of any of those, but ideally I would suggest testing a parser against a large-ish collection of cases like that (sometimes called adversarial datasets because they intentionally go against-the-grain of the language/implementers, to try to find bugs).

@jayaddison

Copy link
Copy Markdown

(I have not used any AI/LLMs for this review; I know that my terminology/language is not perfect, and I may have misunderstood some aspects of parsing, and also aspects of eta -- I've tried my best)

@bgub bgub left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for addressing #337. The core approach fixes the reported unpaired-apostrophe case, but I don't think this is ready to merge yet.

Please address these before merging:

  1. The two new tests currently fail. Eta trims whitespace immediately before the closing tag, but the assertions expect that whitespace to remain in val. Please correct the expectations and make sure the full suite passes.

  2. The new // regex alternative runs before the configured close-tag alternative. This regresses custom closing delimiters beginning with // when they are adjacent to code. For example, this works on main but throws unclosed tag on this branch:

new Eta({ tags: ["{{", "//"] }).parse("{{= it.x//")

The same issue occurs with a delimiter such as //}}. Please preserve the existing custom-delimiter behavior and add a regression test.

Please also add passing coverage for the line-comment termination cases this implementation explicitly handles—at minimum \r, \r\n, U+2028, and U+2029—plus a close-tag-looking sequence inside a single-line comment.

For reference, I ran pnpm lint and pnpm build successfully. pnpm test failed only on the two newly added assertions (107 tests passed). Performance looks neutral for ordinary templates and slightly better for comment-heavy templates, so performance is not a blocker.

@rtritto

rtritto commented Jul 14, 2026

Copy link
Copy Markdown
Author

@bgub done

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.

Unpaired apostrophe in JavaScript comments causes "Unexpected token '%'" error in template parsing

4 participants