Skip to content

Resolve consecutive WithoutEndTag tag helpers as siblings - #13211

Closed
chsienki wants to merge 1 commit into
dotnet:mainfrom
chsienki:chsienki/fix-deferred-taghelper-consecutive-withoutendtag
Closed

Resolve consecutive WithoutEndTag tag helpers as siblings#13211
chsienki wants to merge 1 commit into
dotnet:mainfrom
chsienki:chsienki/fix-deferred-taghelper-consecutive-withoutendtag

Conversation

@chsienki

@chsienki chsienki commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Consecutive tag helpers declared with TagStructure.WithoutEndTag and written without a self-closing slash (e.g. <meta-description><meta-keywords><head-custom>) only bind the first helper. The rest are emitted as literal, unprocessed markup.

This is a regression from the deferred tag helper lowering work (#12957), which moved tag helper resolution after IR lowering. It was reported downstream as dotnet/razor#13212, where a GrandNode app rendered a blank page (Uncaught ReferenceError: Vue is not defined) because the <head> script-registration tag helpers silently stopped running and the Vue bundle was never emitted.

Root cause

The HTML parser nests consecutive unclosed tags, so <a><b><c> parses as a > b > c. When DefaultTagHelperResolutionPhase.ResolveElement binds a as a StartTagOnly helper, it promotes a's parser-nested body children to be siblings inserted after a. But the element walker ResolveElements iterates children in reverse, so it has already moved past that position and never resolves the promoted siblings. They remain UnresolvedElementIntermediateNodes and are later unwrapped to literal HTML.

The ConvertToPlainElement path already re-resolves its promoted siblings; the direct ResolveElement StartTagOnly path was missing the equivalent step.

Fix

After promoting the StartTagOnly element's children to siblings, resolve them in place (in reverse, since resolving a promoted StartTagOnly sibling can itself insert further siblings). This mirrors the existing pattern in ConvertToPlainElementAndResolve.

Testing

Two new integration tests in TagHelpersIntegrationTest:

  • ConsecutiveWithoutEndTagTagHelpers_AllBind — three consecutive WithoutEndTag helpers all bind.
  • MixedNestedStartTagOnlyAndHtmlTagHelpers_AllResolveCorrectly — a tangled mix of a nestable helper, consecutive WithoutEndTag helpers, a normal helper, and real HTML (<section>, <div>); asserts every helper binds in document order and real markup is preserved.

Both fail without the fix (only the first helper binds) and pass with it. The existing tag helper suites remain green (Language ~TagHelper: 953 pass; MVC extensions codegen/baseline: 178 pass).

Note: this regression is also present in the shipping .NET 10 GA Razor compiler, so a servicing backport to release/10.0.3xx (and any 11 preview line) is likely warranted.

When a StartTagOnly (TagStructure.WithoutEndTag) tag helper is bound, its
body children -- which the HTML parser nested underneath it because the tag
was left unclosed (`<a><b><c>` parses as a > b > c) -- are promoted to be
siblings of the tag helper. The element walker iterates children in reverse,
so it never revisits these newly inserted positions; the promoted siblings
were left as unresolved elements and later unwrapped to literal markup. As a
result only the first of a run of consecutive WithoutEndTag helpers bound,
and the rest were emitted as plain text.

Resolve the promoted siblings in place after the promotion, mirroring the
re-resolution already performed on the ConvertToPlainElement path. Resolution
runs in reverse because resolving a promoted StartTagOnly sibling can itself
insert further siblings after it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4a482bf-ffc0-4a98-a673-863b5e84c6d8
@chsienki
chsienki requested a review from a team as a code owner August 5, 2026 21:36
@davidwengier

Copy link
Copy Markdown
Member

Wrong repo :P

@chsienki

chsienki commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Wrong repo :P

picard facepalm.gif

@chsienki chsienki closed this Aug 5, 2026
@chsienki

chsienki commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Reopened against the correct repo: dotnet/roslyn#84771 (Razor now lives in dotnet/roslyn).

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.

2 participants