Draft
Conversation
Add a depth-first search helper on RazorProjectEngineTestBase for finding the first node of a specific type in an IR tree. This replaces brittle .Children.OfType<T>().Single() LINQ chains in tests with a robust recursive search that works regardless of IR tree shape changes from the deferred lowering pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce new IR node types that represent elements and attributes whose final form (plain HTML vs. bound tag helper) is not yet determined at lowering time. These are the data types that enable separating lowering from tag helper resolution: - ElementOrTagHelperIntermediateNode: an element that may or may not be a tag helper. Stores all syntax-tree-derived information (tag name, attributes, spans, component hints) directly on the node so the resolution phase never needs syntax tree access. - MarkupOrTagHelperAttributeIntermediateNode: an attribute whose final form is deferred. Stores pre-lowered fallback forms for both the merged-value path (FallbackNode) and the full-attribute path (FallbackAttributeNode) to avoid re-accessing the syntax tree. - MarkupOrTagHelperAttributeValueIntermediateNode: a literal attribute value that resolves to either CSharpIntermediateToken (bound non-string), HtmlContentIntermediateNode (bound string), or HtmlAttributeValueIntermediateNode (unbound). - CSharpOrTagHelperExpressionAttributeValueIntermediateNode: a dynamic/expression attribute value (e.g. Value="@expr") that resolves differently based on tag helper binding. - ElementBoundaryMarker: sentinel nodes (StartTagEnd, BodyEnd) that prevent HtmlContent merging across element boundaries during deferred lowering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of immediately resolving elements to their final HTML or tag helper form, the lowering phase now produces deferred ElementOrTagHelperIntermediateNode and MarkupOrTagHelperAttributeIntermediateNode nodes. This separates syntax-tree-to-IR lowering from tag-helper-binding resolution. Key changes: - Remove VisitMarkupTagHelperElement/StartTag/Attribute methods that previously built TagHelperIntermediateNode directly during lowering. That responsibility moves to the resolution phase. - All elements that could potentially be tag helpers are now lowered as ElementOrTagHelperIntermediateNode with pre-computed attribute metadata (structure, source spans, attribute data) and pre-lowered HTML fallback forms stored on the deferred nodes. - Add ElementBoundaryMarker nodes to prevent HtmlContent merging across start tag / body / end tag boundaries. - Extract helpers: LowerSyntaxNode(), InferAttributeStructure(), LowerAttributeAsHtml() and make LooksLikeAComponentName() internal static so the resolution phase can reuse them. The lowering phase no longer needs to know about tag helper binding. It only needs to know the set of potential tag helper tag names (from discovery) to decide whether to create deferred nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a new compiler phase that runs after lowering and before the tag helper rewrite phase. It resolves the deferred ElementOrTagHelperIntermediateNode and attribute nodes produced by lowering into their final forms: - Elements that match tag helpers become TagHelperIntermediateNode with fully resolved attributes and binding information. - Elements that don't match are unwrapped to plain HTML markup nodes, using the pre-lowered fallback forms stored during lowering. The phase works entirely with IR nodes — it never accesses the syntax tree. All information needed for resolution (tag names, attribute metadata, source spans, fallback forms) was pre-computed and stored on the deferred nodes during lowering. Key responsibilities: - Tag helper binding via ITagHelperBinder - Attribute conversion (bound properties, directive attributes, minimized attributes, splat, key, ref, rendermode, formname) - Diagnostic production (RZ1034 malformed, RZ1035 missing angle bracket, RZ1042 void elements, RZ2011 inconsistent TagStructure, RZ10012 component name mismatch) - Special handling for end-tag-only elements, escaped elements, self-closing and void elements Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Insert DefaultTagHelperResolutionPhase into the pipeline between lowering and the tag helper rewrite phase. The new phase order is: Discovery -> Lowering -> Resolution -> Rewrite -> ... Previously, lowering ran after the rewrite phase and consumed the rewritten syntax tree. Now lowering runs first on the original syntax tree, and resolution handles the tag helper binding on IR nodes. Pipeline changes: - RazorProjectEngine: insert resolution phase after lowering - SourceGeneratorProjectEngine: track resolution phase index instead of rewrite phase index, and re-run from discovery+1 (including lowering) when tag helpers change, since lowering now produces deferred nodes that depend on discovery results Downstream fixes: - ComponentLoweringPass: transfer diagnostics from TagHelperIntermediateNode to ComponentChildContentIntermediateNode during child content rewriting, since diagnostics are now produced by the resolution phase and stored on the tag helper node - ComponentTypeArgumentIntermediateNode: handle HtmlContent form of type argument values produced by the deferred lowering pipeline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt tests to the new compiler pipeline where resolution runs after lowering: - NodeWriter tests (DesignTime, Runtime, TagHelperHtmlAttribute): update ExecutePhasesThrough to target DefaultTagHelperResolutionPhase instead of IRazorIntermediateNodeLoweringPhase, since code generation tests need fully resolved IR. Use FindDescendant<T>() helper instead of brittle .Children.OfType<T>().Single() chains. - DefaultRazorTagHelperBinderPhaseTest: refactor from syntax-tree-based assertions to IR-based assertions. The binder phase now feeds into lowering+resolution, so tests verify the final TagHelperIntermediateNode output rather than the rewritten syntax tree. Add FindTagHelperNodes() and CollectTagHelperNodes() helpers for recursive IR traversal. - RazorProjectEngineTest: update phase ordering assertion to reflect the new pipeline: Discovery -> Lowering -> Resolution -> Rewrite. - DefaultRazorIntermediateNodeLoweringPhaseIntegrationTest: update phase endpoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mechanical baseline updates resulting from the lowering/resolution pipeline change. The deferred pipeline produces slightly different IR in a few systematic ways: - Source span adjustments: some spans shift by small amounts because the new pipeline computes attribute boundaries differently (e.g. including/excluding whitespace around tag names). - LazyIntermediateToken -> IntermediateToken: some token nodes in tag helper attribute values change type because the resolution phase produces them directly rather than deferring to lazy evaluation. - HtmlContent span changes: whitespace-only content nodes between tag helper attributes may lose source span information when produced as fallback during resolution. No codegen (.codegen.cs, .codegen.html), mapping (.mappings.txt), or diagnostic (.diagnostics.txt) baselines are changed — only .ir.txt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add end-to-end compilation tests that exercise real-world Blazor component patterns through the source generator pipeline. These validate that the deferred tag helper lowering correctly handles component nesting, implicit context variables, and generic type inference scenarios that have been problematic: - Router with CascadingAuthenticationState wrapper (the pattern from the default Blazor template that triggered CS0103 errors) - @Bind with generic components (CS7036 missing argument errors) - RenderFragment<T> implicit context variables in nested wrappers - AuthorizeView with nested ChildContent and RenderFragment - Various combinations of component nesting depth These tests compile actual C# to verify no compiler errors are produced, catching regressions that IR-level tests would miss. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
|
Finally a good candidate to try out some new Copilot PR review tooling! It reckons there is a regression:
Test failures seem to indicate it's on to something. I'm a couple of commits into my own review, but pausing here for the evening... or maybe the weekend :P |
Bug 1 - ComponentBindLoweringPass crash: TryExtractEventNodeStaticText and ExtractEventNodeExpression access node.Children[0] without bounds checking. The deferred pipeline can produce TagHelperDirectiveAttributeParameterIntermediateNode with zero children (e.g., empty @Bind:event=""), causing IndexOutOfRangeException. Fix: add node.Children.Count > 0 guard. Bug 2 - Missing RZ10012 in miscellaneous files: When no tag helpers are discovered (tagHelperContext is null or empty), the early return path calls UnwrapAllElements but did not produce RZ10012 diagnostics for component-like elements. This caused CSharpAndRazor_MiscellaneousFile diagnostic tests to fail. Fix: UnwrapAllElements now accepts DocumentIntermediateNode and produces RZ10012 diagnostics, with a local LooksLikeAComponentName helper. Bug 3 - Formatting regression (FormatNestedComponents2_Range): RZ10012 diagnostic source spans covered the entire element (start tag to end tag) via elementNode.Source, instead of just the start tag. RazorFormattingService aborts range formatting when any diagnostic overlaps the formatting range. For multi-line elements like <ChildContent>...</ChildContent>, the full-element span overlapped interior content, preventing formatting. Fix: add StartTagSpan property to ElementOrTagHelperIntermediateNode, populate it during lowering from BuildSourceSpanFromNode(node.StartTag), and use it for RZ10012 diagnostics. Source generator pipeline fix: SourceGeneratorProjectEngine split point changed from Resolution phase to Rewrite phase. The source generator processes files twice (declaration + generation), and the split must include the Rewrite phase in the tag-helper processing stage to produce correct output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ToddGrun
reviewed
Mar 20, 2026
|
|
||
| private ElementBoundaryMarker() { } | ||
|
|
||
| public override IntermediateNodeCollection Children { get => field ??= []; } |
Contributor
ToddGrun
reviewed
Mar 20, 2026
|
|
||
| protected static AttributeStructure InferAttributeStructure(MarkupAttributeBlockSyntax node) | ||
| { | ||
| if (node.EqualsToken.Kind == SyntaxKind.None && node.Value == null) |
Contributor
Contributor
There was a problem hiding this comment.
never mind, it looks like this method was removed in commit 7
ComponentFileKindVisitor.VisitMarkupAttributeBlock was missing zero-length value handling that the base LoweringVisitor had. When ValueChanged=> (empty unquoted value) was lowered, ValueSourceSpan was null, causing the resolution phase to create a CSharp token without a source mapping. WriteCSharpToken then skipped the mapping entirely, which made TryGetPositionInfoAsync return Razor instead of CSharp for the cursor position. Fix: Add the same else/else-if branches from the base class to handle zero-length values. For unbound HTML attributes (e.g. onsubmit=''), pass null to CreateEmptyHtmlContent in the resolution phase so the source span does not propagate to the fallback IR (matching the old pipeline output). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR introduces deferred tag helper lowering — a new compiler architecture that separates syntax-tree-to-IR lowering from tag helper binding resolution.
Motivation
Previously, the lowering phase needed to know whether an element was a tag helper during lowering, which required running the tag helper rewrite phase first on the syntax tree. This tightly coupled lowering to the rewritten syntax tree and made it difficult to reason about the pipeline.
The new architecture runs lowering before resolution: the lowering phase produces deferred \ElementOrTagHelperIntermediateNode\ and attribute nodes that store all necessary information (tag names, attribute metadata, source spans, pre-lowered HTML fallback forms). A new \DefaultTagHelperResolutionPhase\ then resolves these deferred nodes into their final forms using only the IR — no syntax tree access needed.
Pipeline Change
\
Before: Discovery → Rewrite → Lowering → ...
After: Discovery → Lowering → Resolution → Rewrite → ...
\\
Commits
This PR is structured as a series of logical commits for easier review:
What doesn't change