Repository navigation
Remove Hybrid Tag - #1309
Remove Hybrid Tag#1309
Conversation
Ruby Liquid has no block form for section, but the parser treated it as a
hybrid tag and scanned forward for a matching endsection on every section
tag (both in documents and in {% liquid %} bodies). Files with many section
tags parsed in quadratic time.
Make section a plain TagKind.Tag and remove the hybrid tag kind. Theme
Check still reports a stray endsection as "Unknown tag 'endsection'" by
mapping the parser error.
The previous commit made a stray endsection throw "Attempting to close LiquidTag 'section'". In a full Theme Check run that turns the file's AST into an error, so the only offense was LiquidHTMLSyntaxError and every other check skipped the file. Only tags with a body (Block/Raw) have end tags, so only those now throw the structural close errors. endsection parses as an unknown tag like any other end* name, and LiquidSyntaxError reports it as "Unknown tag 'endsection'".
laxRecoverTagMarkup only recovers Tag/Block kinds, so section (previously
Hybrid) was never recovered. Now that section is a Tag it would be, and
{% section 'x' junk %} would recover and render, while Ruby's section tag
raises in every error mode. Exclude it explicitly to keep the previous
behavior.
The previous change stopped throwing structural close errors for every
registered tag without a body, so stray {% endrender %}, {% endecho %},
{% endsections %}, etc. silently parsed as unknown tags instead of failing
as they do on main. Only endsection needs the exception. Restore the
existing behavior for everything else and add a regression test.
75220dc to
334cac7
Compare
charlespwd
left a comment
There was a problem hiding this comment.
I think we're almost there but I don't think we want to leave anything that leaves a hint that this was ever a thing. We never merged hybrid. New agents should not have to wonder about endsection ever being a thing
| /** | ||
| * Whether a stray `{% end<name> %}` for a registered tag is a structural parse | ||
| * error. `endsection` is the exception: `section` is standalone, and Ruby | ||
| * Liquid reports `{% endsection %}` as an unknown tag (the legacy block form | ||
| * was removed), so it parses as an unknown tag and Theme Check reports it. | ||
| */ | ||
| export function isStructuralEndTag(innerName: string, def: { kind: string } | undefined): boolean { | ||
| return def !== undefined && innerName !== 'section'; | ||
| } |
There was a problem hiding this comment.
I feel like that's "legacy" noise. It should just throw. This was never a thing. We want something like "design by omission" here.
the innerName !== 'section' is sus.
There was a problem hiding this comment.
Addressed in f7d0355 — removed the section exception from isStructuralEndTag, so endsection now throws through the same structural end-tag path as the other registered tags.
| // Ruby's `section` tag has no lax parse: it raises on anything but | ||
| // `section '[type]'` in every error mode, so don't recover it here. | ||
| if (!def || tagName === 'section' || (def.kind !== TagKind.Tag && def.kind !== TagKind.Block)) { |
There was a problem hiding this comment.
feels off to have tagName == 'seciton' here
There was a problem hiding this comment.
Addressed in f7d0355 — removed the tagName === "section" special case from lax recovery.
karreiro
left a comment
There was a problem hiding this comment.
Thank you, @stephanie-shopify!
I agree with the point about removing hybrid support entirely. Apart from that, the PR looks good to me, and with that change applied we'll be ready to proceed with the merge 🚀
| ### D12: `section` is a standalone tag | ||
|
|
||
| When parser encounters `{% section %}`, it scans forward in the token array for `{% endsection %}`. The array is materialized, so scanning is a cheap index walk. | ||
| `section` is a plain `TagKind.Tag`. Ruby Liquid has no block form, so `{% endsection %}` is a structural error. |
There was a problem hiding this comment.
Does it even need mentioning ?
There was a problem hiding this comment.
Good call — removed the section-specific decision note in 947a33a.
| ### 6.4 Section Tag | ||
|
|
||
| **Detection:** When parser encounters `{% section %}`, it scans forward in the token array for `{% endsection %}`, respecting nesting. If found, parse as block. Otherwise, standalone. | ||
|
|
||
| **Markup for both forms:** `SectionMarkup { name: LiquidString, args: LiquidNamedArgument[] }`. Same regardless of form. The difference is whether `children` is populated and `blockEndPosition` exists. | ||
| `section` is standalone only (`{% section 'name' %}`), producing `SectionMarkup { name: LiquidString, args: LiquidNamedArgument[] }`. See D12. |
There was a problem hiding this comment.
This is no longer special I think? Does it need special handling?
There was a problem hiding this comment.
Yep, no longer special. Removed this section-specific deep dive and renumbered the following sections in 947a33a.
charlespwd
left a comment
There was a problem hiding this comment.
Minor doc nit but we're good to merge after :)
What are you adding in this PR?
Parses
sectionas a standalone tag and removes the hybrid tag kind.The hand-written parser treated
sectionas a hybrid tag: every{% section %}scanned forward through the rest of the document (or the rest of a{% liquid %}body) looking for a matching{% endsection %}. Files with manysectiontags parsed in quadratic time.Ruby Liquid has no block form for
section(the parity corpus already records{% section 'foo' %}{% endsection %}asUnknown tag 'endsection'), so the hybrid path wasn't modelling real Liquid. Its only consumer was Theme Check, which used the block form to report that error.Changes:
sectionTagis nowTagKind.Tag. RemovedTagKind.Hybrid,TagDefinitionHybrid,liquid-hybrid.tsandparseLineHybridTag.{% endsection %}now parses as an unknown tag, andLiquidSyntaxErrorreports it asUnknown tag 'endsection'(same message as before). Stray{% endrender %},{% endsections %}, etc. are unchanged.laxRecoverTagMarkuponly recoversTag/Blockkinds, so hybridsectionwas never recovered. It now excludessectionexplicitly to keep that behavior (Ruby'ssectionraises in every error mode).Intentional behavior differences, all on Liquid that Ruby already rejects:
{% endsection %}used to be a parse error, so every other check skipped the file. Now it's an unknown-tag offense and the other checks still run.endsectionline inside{% liquid %}reportsUnexpected end tag 'endsection' in {% liquid %} block, same as a strayendif.endsectioninside an HTML element (<p>{% section 'x' %}y{% endsection %}</p>) reportsLiquidHTMLSyntaxError. That's the existing behavior for any unknownend*tag inside HTML (<p>{% endfoo %}</p>does the same onmain).Parse time for N consecutive
sectiontags:{% section 'x' %}before{% liquid %}lines beforeWhat's next? Any followup issues?
None.
Tophatting
Before/after harness (theme-check CLI + Prettier + parser timing,
mainbuild vs this branch):sectionusage (incl.{% liquid %}and nested inif), missing section files (MissingTemplate), bad markup and each stray-endsectioncase. The only theme-check diffs are the intentional ones listed above. Prettier output also changes for those stray-endsectioninputs. Most changes are cosmetic, but<p>{% section 'x' %}y{% endsection %}</p>gets mangled (see review thread onast.test.ts)..liquidfile.sectiontags: 7.3s → 26ms (document), 1.0s → 26ms ({% liquid %}).Manual (VS Code extension, Debug Node Extension):
sectiontags. Cmd-click on the section name opens the section file.MissingTemplateon missing sections.Unknown tag 'endsection'on strayendsection.{% section %}references.Tests:
vitestacross liquid-html-parser, theme-check-common, prettier-plugin-liquid, theme-graph and theme-language-server-common all pass (6714 tests, rebased on2066abfe).Before you deploy
changeset