-
Notifications
You must be signed in to change notification settings - Fork 240
Deferred tag helper lowering: separate resolution from lowering phase #12957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a961766
2ddcd5f
d9ca32c
cb887d3
cfc9961
73c7041
31d1a11
c0e2886
c97248b
4e7d84d
a4d38f2
85e82bb
515e993
b7f3e89
cfa7a66
a15bd9f
711771a
182a23d
4a7f192
2f4392a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.Language.Intermediate; | ||
|
|
||
| /// <summary> | ||
| /// A unresolved intermediate node representing a dynamic/expression attribute value (e.g. the | ||
| /// <c>@someExpr</c> in <c>Value="@someExpr"</c>) whose final IR form depends on whether | ||
| /// the containing attribute is a bound tag helper property. Produced by initial lowering when | ||
| /// inside an <see cref="ElementOrTagHelperIntermediateNode"/>. | ||
| /// | ||
| /// <para>The resolution phase converts this to:</para> | ||
| /// <list type="bullet"> | ||
| /// <item>Direct <see cref="CSharpIntermediateToken"/> children (for bound non-string tag helper properties)</item> | ||
| /// <item>A <see cref="CSharpExpressionAttributeValueIntermediateNode"/> or | ||
| /// <see cref="CSharpCodeAttributeValueIntermediateNode"/> (for unbound/plain HTML attributes)</item> | ||
| /// </list> | ||
| /// </summary> | ||
| internal sealed class CSharpOrTagHelperExpressionAttributeValueIntermediateNode : IntermediateNode | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Razor used to do all sorts of tree mutation, so properties (conceptually anyway) made sense, but we generally don't anymore. I'd love to do a pass to change it to make them properly immutable that takes all the data in ctors at some point. |
||
| { | ||
| /// <summary>The whitespace/text prefix before the expression.</summary> | ||
| public string Prefix { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Whether the dynamic value contains a top-level expression (implicit or explicit). | ||
| /// When true, the non-tag-helper path produces <see cref="CSharpExpressionAttributeValueIntermediateNode"/>; | ||
| /// when false, it produces <see cref="CSharpCodeAttributeValueIntermediateNode"/>. | ||
| /// </summary> | ||
| public bool ContainsExpression { get; set; } | ||
|
|
||
| public override IntermediateNodeCollection Children { get => field ??= []; } | ||
|
|
||
| public override void Accept(IntermediateNodeVisitor visitor) | ||
| { | ||
| visitor.VisitDefault(this); | ||
| } | ||
|
|
||
| public override void FormatNode(IntermediateNodeFormatter formatter) | ||
| { | ||
| formatter.WriteChildren(Children); | ||
| formatter.WriteProperty(nameof(Prefix), Prefix); | ||
| formatter.WriteProperty(nameof(ContainsExpression), ContainsExpression.ToString()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.Language.Intermediate; | ||
|
|
||
| /// <summary> | ||
| /// An intermediate node that represents an HTML element which may or may not be a tag helper. | ||
| /// All syntax-tree-derived information is stored directly on this node during lowering, | ||
| /// so the resolution phase does not need access to the syntax tree. | ||
| /// </summary> | ||
| internal sealed class ElementOrTagHelperIntermediateNode : IntermediateNode | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this idea.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also like this idea, but I'll do it in a follow up so as not to churn this PR any further.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow-up issue: #12995 |
||
| { | ||
| public string TagName { get; set; } = string.Empty; | ||
| public bool IsComponent { get; set; } | ||
|
|
||
| /// <summary>Whether the element is escaped with ! (e.g., <!input>).</summary> | ||
| public bool IsEscaped { get; set; } | ||
|
|
||
| /// <summary>Whether the start tag is self-closing (ends with />).</summary> | ||
| public bool IsSelfClosing { get; set; } | ||
|
|
||
| /// <summary>Whether the element has an end tag.</summary> | ||
| public bool HasEndTag { get; set; } | ||
|
|
||
| /// <summary>The end tag name (may differ in case from TagName for case-mismatch diagnostics).</summary> | ||
| public string? EndTagName { get; set; } | ||
|
|
||
| /// <summary>Source span of the end tag (for case-mismatch diagnostics).</summary> | ||
| public SourceSpan? EndTagSpan { get; set; } | ||
|
|
||
| /// <summary>Whether the element is a void element (e.g., input, br).</summary> | ||
| public bool IsVoidElement { get; set; } | ||
|
|
||
| /// <summary>Source span of the start tag's name (for component StartTagSpan).</summary> | ||
| public SourceSpan? StartTagNameSpan { get; set; } | ||
|
|
||
| /// <summary>Source span of the entire start tag (e.g., <Component attr="val">), used for diagnostics.</summary> | ||
| public SourceSpan? StartTagSpan { get; set; } | ||
|
|
||
| /// <summary>Attribute name/value pairs for tag helper binding.</summary> | ||
| public ImmutableArray<KeyValuePair<string, string>> AttributeData { get; set; } | ||
|
|
||
| /// <summary>Whether the start tag is missing its close angle (>).</summary> | ||
| public bool HasMissingCloseAngle { get; set; } | ||
|
|
||
| /// <summary>Whether the element has C# expression or code children in the start-tag region (e.g., <div @expr>).</summary> | ||
| public bool HasDynamicExpressionChild { get; set; } | ||
|
|
||
| /// <summary>Whether the end tag is missing its close angle (>).</summary> | ||
| public bool HasMissingEndCloseAngle { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The index in <see cref="Children"/> where body children begin (one past the last | ||
| /// start-tag child). Children in <c>[0, StartTagEndIndex)</c> are start-tag children. | ||
| /// Set during lowering; -1 if not computed. | ||
| /// </summary> | ||
| public int StartTagEndIndex { get; set; } = -1; | ||
|
|
||
| /// <summary> | ||
| /// The index in <see cref="Children"/> where end-tag children begin (one past the last | ||
| /// body child). Children in <c>[StartTagEndIndex, BodyEndIndex)</c> are body children. | ||
| /// Set during lowering; -1 if not computed. | ||
| /// </summary> | ||
| public int BodyEndIndex { get; set; } = -1; | ||
|
|
||
| public override IntermediateNodeCollection Children { get => field ??= []; } | ||
|
|
||
| public override void Accept(IntermediateNodeVisitor visitor) | ||
| { | ||
| visitor.VisitDefault(this); | ||
| } | ||
|
|
||
| public override void FormatNode(IntermediateNodeFormatter formatter) | ||
| { | ||
| formatter.WriteContent(TagName); | ||
| formatter.WriteProperty(nameof(TagName), TagName); | ||
| formatter.WriteProperty(nameof(IsComponent), IsComponent.ToString()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.Language.Intermediate; | ||
|
|
||
| /// <summary> | ||
| /// An intermediate node representing an attribute whose final form (plain HTML attribute | ||
| /// vs tag helper bound attribute) is not yet determined. Pre-lowered fallback forms are | ||
| /// stored so the resolution phase does not need syntax tree access. | ||
| /// </summary> | ||
| internal sealed class MarkupOrTagHelperAttributeIntermediateNode : IntermediateNode | ||
| { | ||
| /// <summary>The attribute name (e.g., "Value", "@bind-Value", "@onclick").</summary> | ||
| public string AttributeName { get; set; } = string.Empty; | ||
|
|
||
| /// <summary>Whether this is a minimized attribute (no value).</summary> | ||
| public bool IsMinimized { get; set; } | ||
|
|
||
| /// <summary>The raw value content of the attribute (for all-literal values only).</summary> | ||
| public string? ValueContent { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Source span covering the attribute value content (between quotes). | ||
| /// Computed from syntax tree positions during lowering so the resolution phase | ||
| /// can use it without needing syntax tree access. | ||
| /// </summary> | ||
| public SourceSpan? ValueSourceSpan { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The attribute structure (quote style). Pre-computed during initial lowering | ||
| /// so the resolution phase does not need to inspect syntax. | ||
| /// </summary> | ||
| public AttributeStructure AttributeStructure { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Source span of the attribute name only (not the whole attribute). | ||
| /// Pre-computed during initial lowering for tag helper property OriginalAttributeSpan. | ||
| /// </summary> | ||
| public SourceSpan? AttributeNameSpan { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Pre-lowered form of this attribute for when the element IS a tag helper but this | ||
| /// attribute is unbound (doesn't match any tag helper property). The tag helper runtime | ||
| /// represents unbound HTML attributes as <see cref="TagHelperHtmlAttributeIntermediateNode"/> | ||
| /// wrapping a structured <see cref="HtmlAttributeIntermediateNode"/> with merged value tokens. | ||
| /// Produced during lowering via <c>VisitAttributeValue</c>, which merges adjacent literal | ||
| /// tokens into a single <see cref="HtmlContentIntermediateNode"/>. | ||
| /// </summary> | ||
| public IntermediateNode? AsTagHelperAttribute { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Pre-lowered form of this attribute for when the element is NOT a tag helper and needs | ||
| /// to be unwrapped back to plain HTML markup. Plain HTML elements represent attributes with | ||
| /// individual value tokens preserving the original parse structure. Produced during lowering | ||
| /// via <c>LowerAttributeAsHtml</c>, which preserves individual tokens without merging. | ||
| /// Used by <c>ConvertToMarkupElement</c> and <c>UnwrapElement</c> to restore the element | ||
| /// to its non-tag-helper form. | ||
| /// </summary> | ||
| public IntermediateNode? AsMarkupAttribute { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The <see cref="HtmlAttributeIntermediateNode"/> child containing unresolved attribute | ||
| /// value children. Set during lowering to avoid linear scans in the resolution phase. | ||
| /// </summary> | ||
| public HtmlAttributeIntermediateNode? HtmlAttributeNode { get; set; } | ||
|
|
||
| public override IntermediateNodeCollection Children { get => field ??= []; } | ||
|
|
||
| public override void Accept(IntermediateNodeVisitor visitor) | ||
| { | ||
| visitor.VisitDefault(this); | ||
| } | ||
|
|
||
| public override void FormatNode(IntermediateNodeFormatter formatter) | ||
| { | ||
| formatter.WriteContent(AttributeName); | ||
| formatter.WriteProperty(nameof(AttributeName), AttributeName); | ||
| formatter.WriteProperty(nameof(IsMinimized), IsMinimized.ToString()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.Language.Intermediate; | ||
|
|
||
| /// <summary> | ||
| /// A unresolved intermediate node representing a literal attribute value (e.g. the <c>123</c> in | ||
| /// <c>IntProperty="123"</c>) whose final IR form depends on whether the containing attribute | ||
| /// is a bound tag helper property. Produced by initial lowering when inside an | ||
| /// <see cref="ElementOrTagHelperIntermediateNode"/>. | ||
| /// | ||
| /// <para>The resolution phase converts this to:</para> | ||
| /// <list type="bullet"> | ||
| /// <item>A <see cref="CSharpIntermediateToken"/> (for bound non-string tag helper properties)</item> | ||
| /// <item>An <see cref="HtmlContentIntermediateNode"/> (for bound string tag helper properties)</item> | ||
| /// <item>An <see cref="HtmlAttributeValueIntermediateNode"/> (for unbound/plain HTML attributes)</item> | ||
| /// </list> | ||
| /// </summary> | ||
| internal sealed class MarkupOrTagHelperAttributeValueIntermediateNode : IntermediateNode | ||
| { | ||
| /// <summary>The whitespace/text prefix before the value (from parser splitting on spaces).</summary> | ||
| public string Prefix { get; set; } = string.Empty; | ||
|
|
||
| public override IntermediateNodeCollection Children { get => field ??= []; } | ||
|
|
||
| public override void Accept(IntermediateNodeVisitor visitor) | ||
| { | ||
| visitor.VisitDefault(this); | ||
| } | ||
|
|
||
| public override void FormatNode(IntermediateNodeFormatter formatter) | ||
| { | ||
| formatter.WriteChildren(Children); | ||
| formatter.WriteProperty(nameof(Prefix), Prefix); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super nit: An