From 35fd2ef99f1eddb6680f6209904b0efbae6d23b2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 3 Apr 2026 01:00:37 +0000
Subject: [PATCH 1/2] Initial plan
From bd52e5d0897500e0155e0e782b2e7c498da10d0a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 3 Apr 2026 01:15:22 +0000
Subject: [PATCH 2/2] refactor: break up ResolveElement into BuildTagHelperNode
and ResolveBodyChildren
Split the large ResolveElement method in DefaultTagHelperResolutionPhase into
three logical pieces as suggested in the issue:
1. ResolveElement (orchestrator) - handles early returns and delegates
2. BuildTagHelperNode - binding validation, node creation, diagnostics, body build
3. ResolveBodyChildren - resolves ElementOrTagHelperIntermediateNode body children
Pure refactoring, no behavioral changes. All tests pass.
Agent-Logs-Url: https://github.com/dotnet/razor/sessions/4e076cca-3076-4662-90a3-f99959390685
Co-authored-by: chsienki <16246502+chsienki@users.noreply.github.com>
---
.../DefaultTagHelperResolutionPhase.cs | 100 ++++++++++++------
1 file changed, 69 insertions(+), 31 deletions(-)
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 74daa974dad..0bfe9e9a706 100644
--- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs
+++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultTagHelperResolutionPhase.cs
@@ -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 ElementOrTagHelperIntermediateNode.
+ ResolveBodyChildren(bodyNode, binder, prefix, usedHelpers, in context, tagHelperNode);
+
+ // 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]);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 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(
+ ElementOrTagHelperIntermediateNode 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
- // ElementOrTagHelperIntermediateNode. 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]);
- }
- }
- }
}
///