Skip to content

fix(undefined-object): track Liquid declarations inside HTML comments#1197

Open
SinhSinhAn wants to merge 2 commits intoShopify:mainfrom
SinhSinhAn:fix/undefined-object-html-comment-assign
Open

fix(undefined-object): track Liquid declarations inside HTML comments#1197
SinhSinhAn wants to merge 2 commits intoShopify:mainfrom
SinhSinhAn:fix/undefined-object-html-comment-assign

Conversation

@SinhSinhAn
Copy link
Copy Markdown
Contributor

What is this PR?

Fix #1099 (filed by @charlespwd). The reproduction:

<!-- {% assign foo = 10 %} -->
{{ foo }}

was producing Unknown object 'foo' used even though Liquid runs inside HTML comments at runtime — the browser only strips <!-- ... --> after Liquid has already executed.

Why was this broken?

The HTML grammar stores HtmlComment.body as opaque text:

// stage-2-ast.ts
export interface HtmlComment extends ASTNode<NodeTypes.HtmlComment> {
  body: string;  // raw text, not parsed
}

The check infrastructure walks the AST via visitLiquid, so any LiquidTag nested inside an HtmlComment.body is invisible to every visitor. UndefinedObject never sees the {% assign foo %}, never records foo as defined, and the later {{ foo }} is flagged.

This is technically a parser-layer asymmetry (Liquid is parsed inside attribute values and inside liquidRawTag bodies, but not inside HTML comment bodies), but fixing it at the grammar level is invasive: it would change HtmlComment.body from string to a structured array, break the prettier-plugin-liquid HtmlComment printer at printer-liquid-html.ts:301-325, and ripple through every downstream consumer that reads node.body as a string. That feels like a design call for the maintainers, not a drive-by.

How this PR fixes it

Scope the fix to the UndefinedObject check itself. When the visitor encounters an HtmlComment, the check re-parses node.body with the existing toLiquidAST helper and walks the sub-AST for the four tag types whose declarations are visible outside the enclosing block:

  • assign
  • capture
  • increment
  • decrement

Each declaration is registered with scope.start = comment.position.end so the variable becomes visible from the end of the comment onward, matching the existing position-based scoping rules used elsewhere in the check. References that appear before the comment continue to be flagged correctly.

Block-local declarations (for, tablerow, form, paginate, layout none) are intentionally skipped:

  • Their scope cannot escape the block, so they cannot leak past -->
  • References to them inside the comment are themselves unreachable to the visitor, so there is nothing to track

If the comment body is malformed Liquid (or just plain HTML text with no tags), parsing throws and the check silently skips the comment rather than failing the entire run.

Trade-offs vs. fixing the grammar

The deep fix (parse Liquid inside the HTML comment grammar rule, change HtmlComment.body to a structured array) would also benefit other checks like UnknownFilter, DeprecatedFilter, and UnusedAssign. I'd be happy to tackle that as a follow-up if you'd prefer the broader fix; the current PR is shaped to land the user-visible win quickly without touching the AST shape or the prettier printer.

Test plan

Added 8 regression tests under a Liquid inside HTML comments (issue #1099) describe block in index.spec.ts:

  • Assign inside an HTML comment is recognized after the comment closes (the issue's exact reproduction)
  • Capture inside an HTML comment is recognized
  • Increment + decrement in the same comment are both recognized
  • Multiple assigns within a single comment are all tracked
  • References before the comment are still flagged (scope-start semantics preserved)
  • Genuinely undefined variables are still flagged when a comment exists
  • Nested assigns inside a comment-wrapped {% if %} block are tracked through both branches
  • Malformed Liquid in a comment body does not crash the check

Full regression run: 954 / 954 tests pass across theme-check-common. TypeScript build clean.

Closes #1099

`<!-- {% assign foo = 10 %} -->` followed by `{{ foo }}` triggered an
"Unknown object 'foo' used" warning even though Liquid is executed
inside HTML comments at runtime (the browser strips `<!-- ... -->`
after Liquid has already run).

The HTML grammar stores `HtmlComment.body` as opaque text, so the
visitor that powers UndefinedObject never reaches Liquid nodes nested
inside a comment. Rather than refactor the grammar/AST/printer
(which would be a breaking change for downstream consumers), the
check now re-parses each `HtmlComment.body` with `toLiquidAST` and
walks the resulting sub-AST for the four tag types that declare
file-scoped variables: `assign`, `capture`, `increment`, `decrement`.

Each declaration is registered with `scope.start = comment.position.end`
so the variable becomes visible from the end of the comment onward,
matching the existing position-based scoping rules. References that
appear before the comment continue to be flagged.

Block-local declarations (`for`, `tablerow`, `form`, `paginate`,
`layout none`) are intentionally skipped because their scope cannot
escape the block, and any references to them inside the comment are
themselves unreachable to the visitor.

Closes Shopify#1099
@SinhSinhAn SinhSinhAn requested a review from a team as a code owner April 27, 2026 04:45
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.

Liquid in HTML comment is ignored for UnknownObject purposes

1 participant