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]); - } - } - } } ///