diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs
index b24e3d4b48a..c2e9f7ebdbe 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs
@@ -53,7 +53,7 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
if (tagHelperContext == null || tagHelperContext.TagHelpers is [])
{
- // No tag helpers discovered - unwrap all ElementOrTagHelper nodes to their fallback.
+ // No tag helpers discovered - unwrap all UnresolvedElement nodes to their fallback.
UnwrapAllElements(documentNode, documentNode);
// Still need to set referenced tag helpers for downstream phases.
@@ -174,6 +174,49 @@ private void ResolveElement(
return;
}
+ // Build the tag helper node (binding validation + node creation + diagnostics + body).
+ var (tagHelperNode, bodyNode) = BuildTagHelperNode(elementNode, binding, tagName, prefix, usedHelpers, in context);
+
+ // Resolve any body children that are still UnresolvedElementIntermediateNode.
+ ResolveBodyChildren(bodyNode, binder, prefix, usedHelpers, in context, tagHelperNode);
+
+ // Check AllowedChildren constraints (RZ2009, RZ2010).
+ ValidateAllowedChildren(tagHelperNode, bodyNode, binding, prefix);
+
+ // Replace the UnresolvedElement with the TagHelperIntermediateNode.
+ parent.Children[index] = tagHelperNode;
+
+ // For StartTagOnly elements, body content from the original element
+ // belongs to the parent, not the tag helper. Promote it.
+ if (tagHelperNode.TagMode == TagMode.StartTagOnly)
+ {
+ var startTagEndIdx = elementNode.StartTagEndIndex;
+ var bodyEndIdx = elementNode.BodyEndIndex;
+
+ if (startTagEndIdx >= 0 && bodyEndIdx >= 0)
+ {
+ var insertIdx = index + 1;
+ for (var i = startTagEndIdx; i < bodyEndIdx; i++)
+ {
+ parent.Children.Insert(insertIdx++, elementNode.Children[i]);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Creates a from a confirmed tag helper binding,
+ /// adds all binding-level diagnostics, and builds the body node by delegating to the resolver.
+ /// Covers the "tag helper binding and validation" and "element construction" split points.
+ ///
+ private (TagHelperIntermediateNode TagHelperNode, TagHelperBodyIntermediateNode BodyNode) BuildTagHelperNode(
+ UnresolvedElementIntermediateNode elementNode,
+ TagHelperBinding binding,
+ string tagName,
+ string prefix,
+ TagHelperCollection.Builder usedHelpers,
+ in ResolutionContext context)
+ {
// It IS a tag helper. Track the used helpers.
usedHelpers.AddRange(binding.TagHelpers);
@@ -216,15 +259,33 @@ private void ResolveElement(
// Build body and attributes.
var bodyNode = new TagHelperBodyIntermediateNode();
-
_resolver.BuildTagHelper(tagHelperNode, bodyNode, elementNode, binding, context.SourceDocument, in context);
- // After building the tag helper, resolve any body children that are still
- // UnresolvedElementIntermediateNode. Pass the tagHelperNode as parent so the
- // binder can see the parent tag name. This is needed for:
- // - Components: child content matching (e.g., Found/NotFound inside Router)
- // - Legacy tag helpers: RequireParentTag matching (e.g.,
inside | )
- var tagHelperParentForBody = tagHelperNode;
+ return (tagHelperNode, bodyNode);
+ }
+
+ ///
+ /// Resolves body children of a newly built tag helper node.
+ /// Iterates over children in reverse order, recursively
+ /// resolving any entries with the
+ /// tag helper as the parent context. Covers the "child attribute processing" split point.
+ ///
+ ///
+ /// Passing is critical so the binder can see the parent tag
+ /// name. This is needed for:
+ ///
+ /// - Components: child content matching (e.g., Found/NotFound inside Router)
+ /// - Legacy tag helpers: RequireParentTag matching (e.g., <td> inside <tr>)
+ ///
+ ///
+ private void ResolveBodyChildren(
+ TagHelperBodyIntermediateNode bodyNode,
+ TagHelperBinder binder,
+ string prefix,
+ TagHelperCollection.Builder usedHelpers,
+ in ResolutionContext context,
+ TagHelperIntermediateNode tagHelperParent)
+ {
for (var i = bodyNode.Children.Count - 1; i >= 0; i--)
{
var bodyChild = bodyNode.Children[i];
@@ -238,7 +299,7 @@ private void ResolveElement(
// would descend into the element's children and prematurely resolve them
// without knowing the parent tag helper (e.g., Found/NotFound inside Router
// need to know Router is their parent to be matched as child content).
- ResolveElement(bodyNode, i, bodyElementNode, binder, prefix, usedHelpers, in context, tagHelperParentForBody);
+ ResolveElement(bodyNode, i, bodyElementNode, binder, prefix, usedHelpers, in context, tagHelperParent);
}
else
{
@@ -252,29 +313,6 @@ private void ResolveElement(
// start tag on the tracker stack). For matched pairs like ,
// the rewriter handles them normally. The rewriter (which still runs after this phase)
// will emit RZ1033 for orphan end tags.
-
- // Check AllowedChildren constraints (RZ2009, RZ2010).
- ValidateAllowedChildren(tagHelperNode, bodyNode, binding, prefix);
-
- // Replace the ElementOrTagHelper with the TagHelperIntermediateNode.
- parent.Children[index] = tagHelperNode;
-
- // For StartTagOnly elements, body content from the original element
- // belongs to the parent, not the tag helper. Promote it.
- if (tagHelperNode.TagMode == TagMode.StartTagOnly)
- {
- var startTagEndIdx = elementNode.StartTagEndIndex;
- var bodyEndIdx = elementNode.BodyEndIndex;
-
- if (startTagEndIdx >= 0 && bodyEndIdx >= 0)
- {
- var insertIdx = index + 1;
- for (var i = startTagEndIdx; i < bodyEndIdx; i++)
- {
- parent.Children.Insert(insertIdx++, elementNode.Children[i]);
- }
- }
- }
}
///