From 3d1cdadbdfd5ab38962afbfb2e6ba4ad92898477 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 10:17:08 -0700 Subject: [PATCH 01/14] Adds SyntaxToken factory method overloads Provides additional overloads for the `SyntaxToken` factory methods in `SyntaxFactory`. These new overloads allow specifying the parent syntax node, position, and index, offering greater flexibility when creating syntax tokens. In addition, use one of the new SyntaxFactory.Token overloads in ClassifiedSpanVisitor rather than constructor a SyntaxToken directly. --- .../src/Language/ClassifiedSpanVisitor.cs | 4 +-- .../src/Language/Syntax/SyntaxFactory.cs | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs index b8f38697657..a65f8861de4 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -73,7 +73,7 @@ public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) if (comment.IsMissing) { // We need to generate a classified span at this position. So insert a marker in its place. - comment = new(razorCommentSyntax, Syntax.InternalSyntax.SyntaxFactory.Token(SyntaxKind.Marker, string.Empty), razorCommentSyntax.StartCommentStar.EndPosition, index: 0); + comment = SyntaxFactory.Token(SyntaxKind.Marker, parent: node, position: node.StartCommentStar.EndPosition); } WriteSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs index 34f99b4ccc4..51506537b4d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxFactory.cs @@ -10,19 +10,29 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax; internal static partial class SyntaxFactory { public static SyntaxToken Token(SyntaxKind kind, params RazorDiagnostic[] diagnostics) - { - return Token(kind, content: string.Empty, diagnostics: diagnostics); - } + => Token(kind, content: string.Empty, parent: null, position: 0, index: 0, diagnostics: diagnostics); public static SyntaxToken Token(SyntaxKind kind, string content, params RazorDiagnostic[] diagnostics) - { - return new SyntaxToken(parent: null, InternalSyntax.SyntaxFactory.Token(kind, content, diagnostics), position: 0, index: 0); - } + => Token(kind, content, parent: null, position: 0, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, SyntaxNode? parent, int position, params RazorDiagnostic[] diagnostics) + => Token(kind, string.Empty, parent, position, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, string content, SyntaxNode? parent, int position, params RazorDiagnostic[] diagnostics) + => Token(kind, content, parent, position, index: 0, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, SyntaxNode? parent, int position, int index, params RazorDiagnostic[] diagnostics) + => Token(kind, string.Empty, parent, position, index, diagnostics); + + public static SyntaxToken Token( + SyntaxKind kind, string content, SyntaxNode? parent, int position, int index, params RazorDiagnostic[] diagnostics) + => new(parent, InternalSyntax.SyntaxFactory.Token(kind, content, diagnostics), position, index); internal static SyntaxToken MissingToken(SyntaxKind kind, params RazorDiagnostic[] diagnostics) - { - return new SyntaxToken(parent: null, InternalSyntax.SyntaxFactory.MissingToken(kind, diagnostics), position: 0, index: 0); - } + => new(parent: null, InternalSyntax.SyntaxFactory.MissingToken(kind, diagnostics), position: 0, index: 0); public static SyntaxList List() where TNode : SyntaxNode From d6dcfe96655578a9bc06f41b66779bd6c6170eb7 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 10:21:55 -0700 Subject: [PATCH 02/14] Simplifies ClassifiedSpanVisitor Refactors ClassifiedSpanVisitor to use a `BlockSaver` ref struct to manage block scopes, improving readability and reducing code duplication. Removes unnecessary base visit action delegates. --- .../src/Language/ClassifiedSpanVisitor.cs | 185 +++++++++++------- 1 file changed, 115 insertions(+), 70 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs index a65f8861de4..b785477f845 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; @@ -17,18 +16,6 @@ internal class ClassifiedSpanVisitor : SyntaxWalker private readonly RazorSourceDocument _source; private readonly ImmutableArray.Builder _spans; - private readonly Action _baseVisitCSharpCodeBlock; - private readonly Action _baseVisitCSharpStatement; - private readonly Action _baseVisitCSharpExplicitExpression; - private readonly Action _baseVisitCSharpImplicitExpression; - private readonly Action _baseVisitRazorDirective; - private readonly Action _baseVisitCSharpTemplateBlock; - private readonly Action _baseVisitMarkupBlock; - private readonly Action _baseVisitMarkupTagHelperAttributeValue; - private readonly Action _baseVisitMarkupTagHelperElement; - private readonly Action _baseVisitMarkupCommentBlock; - private readonly Action _baseVisitMarkupDynamicAttributeValue; - private BlockKindInternal _currentBlockKind; private SyntaxNode? _currentBlock; @@ -37,18 +24,6 @@ private ClassifiedSpanVisitor(RazorSourceDocument source, ImmutableArray VisitRoot(RazorSyntaxTree s public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { - WriteBlock(node, BlockKindInternal.Comment, razorCommentSyntax => + using (CommentBlock(node)) { - WriteSpan(razorCommentSyntax.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); - WriteSpan(razorCommentSyntax.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + WriteSpan(node.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + WriteSpan(node.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + + var comment = node.Comment; - var comment = razorCommentSyntax.Comment; if (comment.IsMissing) { // We need to generate a classified span at this position. So insert a marker in its place. @@ -78,56 +54,75 @@ public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) WriteSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); - WriteSpan(razorCommentSyntax.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - WriteSpan(razorCommentSyntax.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); - }); + WriteSpan(node.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + WriteSpan(node.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + } } public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) { - if (node.Parent is CSharpStatementBodySyntax || - node.Parent is CSharpExplicitExpressionBodySyntax || - node.Parent is CSharpImplicitExpressionBodySyntax || - node.Parent is RazorDirectiveBodySyntax || - (_currentBlockKind == BlockKindInternal.Directive && - node.Children.Count == 1 && - node.Children[0] is CSharpStatementLiteralSyntax)) + if (node.Parent is CSharpStatementBodySyntax or + CSharpExplicitExpressionBodySyntax or + CSharpImplicitExpressionBodySyntax or + RazorDirectiveBodySyntax || + (_currentBlockKind == BlockKindInternal.Directive && node.Children is [CSharpStatementLiteralSyntax])) { base.VisitCSharpCodeBlock(node); return; } - WriteBlock(node, BlockKindInternal.Statement, _baseVisitCSharpCodeBlock); + using (StatementBlock(node)) + { + base.VisitCSharpCodeBlock(node); + } } public override void VisitCSharpStatement(CSharpStatementSyntax node) { - WriteBlock(node, BlockKindInternal.Statement, _baseVisitCSharpStatement); + using (StatementBlock(node)) + { + base.VisitCSharpStatement(node); + } } public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) { - WriteBlock(node, BlockKindInternal.Expression, _baseVisitCSharpExplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpExplicitExpression(node); + } } public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) { - WriteBlock(node, BlockKindInternal.Expression, _baseVisitCSharpImplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpImplicitExpression(node); + } } public override void VisitRazorDirective(RazorDirectiveSyntax node) { - WriteBlock(node, BlockKindInternal.Directive, _baseVisitRazorDirective); + using (DirectiveBlock(node)) + { + base.VisitRazorDirective(node); + } } public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) { - WriteBlock(node, BlockKindInternal.Template, _baseVisitCSharpTemplateBlock); + using (TemplateBlock(node)) + { + base.VisitCSharpTemplateBlock(node); + } } public override void VisitMarkupBlock(MarkupBlockSyntax node) { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupBlock); + using (MarkupBlock(node)) + { + base.VisitMarkupBlock(node); + } } public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node) @@ -135,52 +130,63 @@ public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttribute // We don't generate a classified span when the attribute value is a simple literal value. // This is done so we maintain the classified spans generated in 2.x which // used ConditionalAttributeCollapser (combines markup literal attribute values into one span with no block parent). - if (node.Children.Count > 1 || - (node.Children.Count == 1 && node.Children[0] is MarkupDynamicAttributeValueSyntax)) + if (!IsSimpleLiteralValue(node)) { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupTagHelperAttributeValue); + base.VisitMarkupTagHelperAttributeValue(node); return; } - base.VisitMarkupTagHelperAttributeValue(node); + using (MarkupBlock(node)) + { + base.VisitMarkupTagHelperAttributeValue(node); + } + + static bool IsSimpleLiteralValue(MarkupTagHelperAttributeValueSyntax node) + { + return node.Children is [MarkupDynamicAttributeValueSyntax] or { Count: > 1 }; + } } public override void VisitMarkupStartTag(MarkupStartTagSyntax node) { - WriteBlock(node, BlockKindInternal.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node, includeEditHandler: true); foreach (var child in children) { Visit(child); } - }); + } } public override void VisitMarkupEndTag(MarkupEndTagSyntax node) { - WriteBlock(node, BlockKindInternal.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node, includeEditHandler: true); + foreach (var child in children) { Visit(child); } - }); + } } public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) { - WriteBlock(node, BlockKindInternal.Tag, _baseVisitMarkupTagHelperElement); + using (TagBlock(node)) + { + base.VisitMarkupTagHelperElement(node); + } } public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) { foreach (var child in node.Attributes) { - if (child is MarkupTagHelperAttributeSyntax || - child is MarkupTagHelperDirectiveAttributeSyntax || - child is MarkupMinimizedTagHelperDirectiveAttributeSyntax) + if (child is MarkupTagHelperAttributeSyntax or + MarkupTagHelperDirectiveAttributeSyntax or + MarkupMinimizedTagHelperDirectiveAttributeSyntax) { Visit(child); } @@ -194,14 +200,15 @@ public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) { - WriteBlock(node, BlockKindInternal.Markup, n => + using (MarkupBlock(node)) { var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new SyntaxTokenList(node.EqualsToken), chunkGenerator: null); var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); + Visit(mergedAttributePrefix); Visit(node.Value); Visit(node.ValueSuffix); - }); + } } public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) @@ -224,21 +231,27 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) { - WriteBlock(node, BlockKindInternal.Markup, n => + using (MarkupBlock(node)) { var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name); Visit(mergedAttributePrefix); - }); + } } public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) { - WriteBlock(node, BlockKindInternal.HtmlComment, _baseVisitMarkupCommentBlock); + using (HtmlCommentBlock(node)) + { + base.VisitMarkupCommentBlock(node); + } } public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) { - WriteBlock(node, BlockKindInternal.Markup, _baseVisitMarkupDynamicAttributeValue); + using (MarkupBlock(node)) + { + base.VisitMarkupDynamicAttributeValue(node); + } } public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) @@ -307,18 +320,50 @@ public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralS base.VisitMarkupEphemeralTextLiteral(node); } - private void WriteBlock(TNode node, BlockKindInternal kind, Action handler) where TNode : SyntaxNode + private BlockSaver CommentBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Comment); + + private BlockSaver DirectiveBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Directive); + + private BlockSaver ExpressionBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Expression); + + private BlockSaver HtmlCommentBlock(SyntaxNode node) + => Block(node, BlockKindInternal.HtmlComment); + + private BlockSaver MarkupBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Markup); + + private BlockSaver StatementBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Statement); + + private BlockSaver TagBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Tag); + + private BlockSaver TemplateBlock(SyntaxNode node) + => Block(node, BlockKindInternal.Template); + + private BlockSaver Block(SyntaxNode node, BlockKindInternal kind) { - var previousBlock = _currentBlock; - var previousKind = _currentBlockKind; + var saver = new BlockSaver(this); _currentBlock = node; _currentBlockKind = kind; - handler(node); + return saver; + } + + private readonly ref struct BlockSaver(ClassifiedSpanVisitor visitor) + { + private readonly SyntaxNode? _previousBlock = visitor._currentBlock; + private readonly BlockKindInternal _previousKind = visitor._currentBlockKind; - _currentBlock = previousBlock; - _currentBlockKind = previousKind; + public void Dispose() + { + visitor._currentBlock = _previousBlock; + visitor._currentBlockKind = _previousKind; + } } private void WriteSpan(SyntaxNode node, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) From 7beb9bdf8b794db1a7513d4898982cfd952d6a69 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 10:33:52 -0700 Subject: [PATCH 03/14] Simplifies span writing in classifier Refactors span writing logic in the `ClassifiedSpanVisitor` to improve readability and eliminate redundant null checks. Retrieves `AcceptedCharacters` directly within the method, using the node's edit handler or defaulting to `Any`. Also adds an assertion to validate current block during span writing. --- .../src/Language/ClassifiedSpanVisitor.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs index b785477f845..c1f867ea9cb 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; @@ -366,49 +367,40 @@ public void Dispose() } } - private void WriteSpan(SyntaxNode node, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) + private void WriteSpan(SyntaxNode node, SpanKindInternal kind) { if (node.IsMissing) { return; } + Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a node."); + var spanSource = node.GetSourceSpan(_source); var blockSource = _currentBlock.GetSourceSpan(_source); - if (!acceptedCharacters.HasValue) - { - acceptedCharacters = AcceptedCharactersInternal.Any; - var context = node.GetEditHandler(); - if (context != null) - { - acceptedCharacters = context.AcceptedCharacters; - } - } - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters.Value); + var acceptedCharacters = node.GetEditHandler() is { } context + ? context.AcceptedCharacters + : AcceptedCharactersInternal.Any; + + var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters); + _spans.Add(span); } - private void WriteSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) + private void WriteSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) { if (token.IsMissing) { return; } + Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a token."); + var spanSource = token.GetSourceSpan(_source); var blockSource = _currentBlock.GetSourceSpan(_source); - if (!acceptedCharacters.HasValue) - { - acceptedCharacters = AcceptedCharactersInternal.Any; - var context = token.GetEditHandler(); - if (context != null) - { - acceptedCharacters = context.AcceptedCharacters; - } - } + var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters); - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters.Value); _spans.Add(span); } From a6a95d9bc6f64d9d202e296c75d8a148dfd89836 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 10:59:16 -0700 Subject: [PATCH 04/14] Improves classified span generation performance Changes span creation to only compute the current block's span when it is first needed. This avoids redundant computations, improving performance. --- .../src/Language/ClassifiedSpanVisitor.cs | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs index c1f867ea9cb..dd40e05b155 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Immutable; @@ -17,8 +17,9 @@ internal class ClassifiedSpanVisitor : SyntaxWalker private readonly RazorSourceDocument _source; private readonly ImmutableArray.Builder _spans; - private BlockKindInternal _currentBlockKind; private SyntaxNode? _currentBlock; + private SourceSpan? _currentBlockSpan; + private BlockKindInternal _currentBlockKind; private ClassifiedSpanVisitor(RazorSourceDocument source, ImmutableArray.Builder spans) { @@ -42,8 +43,8 @@ public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { using (CommentBlock(node)) { - WriteSpan(node.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); - WriteSpan(node.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + AddSpan(node.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + AddSpan(node.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); var comment = node.Comment; @@ -53,10 +54,10 @@ public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) comment = SyntaxFactory.Token(SyntaxKind.Marker, parent: node, position: node.StartCommentStar.EndPosition); } - WriteSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); + AddSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); - WriteSpan(node.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - WriteSpan(node.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + AddSpan(node.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + AddSpan(node.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); } } @@ -257,49 +258,49 @@ public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValu public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) { - WriteSpan(node, SpanKindInternal.MetaCode); + AddSpan(node, SpanKindInternal.MetaCode); base.VisitRazorMetaCode(node); } public override void VisitCSharpTransition(CSharpTransitionSyntax node) { - WriteSpan(node, SpanKindInternal.Transition); + AddSpan(node, SpanKindInternal.Transition); base.VisitCSharpTransition(node); } public override void VisitMarkupTransition(MarkupTransitionSyntax node) { - WriteSpan(node, SpanKindInternal.Transition); + AddSpan(node, SpanKindInternal.Transition); base.VisitMarkupTransition(node); } public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) { - WriteSpan(node, SpanKindInternal.Code); + AddSpan(node, SpanKindInternal.Code); base.VisitCSharpStatementLiteral(node); } public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) { - WriteSpan(node, SpanKindInternal.Code); + AddSpan(node, SpanKindInternal.Code); base.VisitCSharpExpressionLiteral(node); } public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) { - WriteSpan(node, SpanKindInternal.Code); + AddSpan(node, SpanKindInternal.Code); base.VisitCSharpEphemeralTextLiteral(node); } public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) { - WriteSpan(node, SpanKindInternal.None); + AddSpan(node, SpanKindInternal.None); base.VisitUnclassifiedTextLiteral(node); } public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) { - WriteSpan(node, SpanKindInternal.Markup); + AddSpan(node, SpanKindInternal.Markup); base.VisitMarkupLiteralAttributeValue(node); } @@ -311,13 +312,13 @@ public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) return; } - WriteSpan(node, SpanKindInternal.Markup); + AddSpan(node, SpanKindInternal.Markup); base.VisitMarkupTextLiteral(node); } public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) { - WriteSpan(node, SpanKindInternal.Markup); + AddSpan(node, SpanKindInternal.Markup); base.VisitMarkupEphemeralTextLiteral(node); } @@ -352,22 +353,31 @@ private BlockSaver Block(SyntaxNode node, BlockKindInternal kind) _currentBlock = node; _currentBlockKind = kind; + // This is a new block, so we reset the current block span. + // It will be computed when the first span is written. + _currentBlockSpan = null; + return saver; } private readonly ref struct BlockSaver(ClassifiedSpanVisitor visitor) { private readonly SyntaxNode? _previousBlock = visitor._currentBlock; + private readonly SourceSpan? _previousBlockSpan = visitor._currentBlockSpan; private readonly BlockKindInternal _previousKind = visitor._currentBlockKind; public void Dispose() { visitor._currentBlock = _previousBlock; + visitor._currentBlockSpan = _previousBlockSpan; visitor._currentBlockKind = _previousKind; } } - private void WriteSpan(SyntaxNode node, SpanKindInternal kind) + private SourceSpan CurrentBlockSpan + => _currentBlockSpan ??= _currentBlock.AssumeNotNull().GetSourceSpan(_source); + + private void AddSpan(SyntaxNode node, SpanKindInternal kind) { if (node.IsMissing) { @@ -376,19 +386,16 @@ private void WriteSpan(SyntaxNode node, SpanKindInternal kind) Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a node."); - var spanSource = node.GetSourceSpan(_source); - var blockSource = _currentBlock.GetSourceSpan(_source); + var nodeSpan = node.GetSourceSpan(_source); var acceptedCharacters = node.GetEditHandler() is { } context ? context.AcceptedCharacters : AcceptedCharactersInternal.Any; - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters); - - _spans.Add(span); + AddSpan(nodeSpan, kind, acceptedCharacters); } - private void WriteSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) + private void AddSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) { if (token.IsMissing) { @@ -397,13 +404,14 @@ private void WriteSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharact Debug.Assert(_currentBlock != null, "Current block should not be null when writing a span for a token."); - var spanSource = token.GetSourceSpan(_source); - var blockSource = _currentBlock.GetSourceSpan(_source); - var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters); + var tokenSpan = token.GetSourceSpan(_source); - _spans.Add(span); + AddSpan(tokenSpan, kind, acceptedCharacters); } + private void AddSpan(SourceSpan span, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) + => _spans.Add(new(span, CurrentBlockSpan, kind, _currentBlockKind, acceptedCharacters)); + private sealed class Policy : IPooledObjectPolicy.Builder> { public static readonly Policy Instance = new(); From 2d90c0cb5757b732ffc3911d2dd1e474aa7edf97 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 12:41:30 -0700 Subject: [PATCH 05/14] Improves attribute markup span creation Uses a `SourceSpanComputer` to efficiently create markup spans for attributes, avoiding unnecessary string allocations and improving performance of the `ClassifiedSpanVisitor`. --- .../src/Language/ClassifiedSpanVisitor.cs | 25 +++- .../src/Language/SourceSpanComputer.cs | 117 ++++++++++++++++++ 2 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs index dd40e05b155..e5e094f8f46 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs @@ -204,10 +204,19 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) { using (MarkupBlock(node)) { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new SyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); + // For attributes, we add a single span from the start of the name prefix to the end of the value prefix. + var spanComputer = new SourceSpanComputer(_source); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + spanComputer.Add(node.NameSuffix); + spanComputer.Add(node.EqualsToken); + spanComputer.Add(node.ValuePrefix); - Visit(mergedAttributePrefix); + var sourceSpan = spanComputer.ToSourceSpan(); + + AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); + + // Visit the value and value suffix separately. Visit(node.Value); Visit(node.ValueSuffix); } @@ -235,8 +244,14 @@ public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttribute { using (MarkupBlock(node)) { - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name); - Visit(mergedAttributePrefix); + // For minimized attributes, we add a single span for the attribute name along with the name prefix. + var spanComputer = new SourceSpanComputer(_source); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + + var sourceSpan = spanComputer.ToSourceSpan(); + + AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs new file mode 100644 index 00000000000..87f4eec304d --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax; + +namespace Microsoft.AspNetCore.Razor.Language; + +/// +/// Helper that can be used to efficiently build up a from a set of syntax tokens. +/// +internal ref struct SourceSpanComputer(RazorSourceDocument source) +{ + private readonly RazorSourceDocument _source = source; + + private SyntaxToken _firstToken; + private SyntaxToken _lastToken; + + public void Add(SyntaxToken token) + { + if (token.Kind == SyntaxKind.None) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = token; + } + + _lastToken = token; + } + + public void Add(SyntaxTokenList tokenList) + { + if (tokenList.Count == 0) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = tokenList[0]; + } + + _lastToken = tokenList[^1]; + } + + public void Add(SyntaxTokenList? tokenList) + { + if (tokenList is not [_, ..] tokens) + { + return; + } + + if (_firstToken.Kind == SyntaxKind.None) + { + _firstToken = tokens[0]; + } + + _lastToken = tokens[^1]; + } + + public void Add(CSharpEphemeralTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(CSharpExpressionLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(CSharpStatementLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(MarkupEphemeralTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(MarkupTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public void Add(UnclassifiedTextLiteralSyntax? literal) + { + Add(literal?.LiteralTokens); + } + + public readonly SourceSpan ToSourceSpan() + { + if (_firstToken.Kind == SyntaxKind.None) + { + return default; + } + + Debug.Assert(_lastToken.Kind != SyntaxKind.None, "Last token should not be None when first token is set."); + + var start = _firstToken.Span.Start; + var end = _lastToken.Span.End; + + Debug.Assert(start <= end, "Start position should not be greater than end position."); + + var length = end - start; + + var text = _source.Text; + var startLinePosition = text.Lines.GetLinePosition(start); + var endLinePosition = text.Lines.GetLinePosition(end); + var lineCount = endLinePosition.Line - startLinePosition.Line; + + return new SourceSpan(_source.FilePath, absoluteIndex: start, startLinePosition.Line, startLinePosition.Character, length, lineCount, endLinePosition.Character); + } +} From 3d71e4c307f0f9d72e06fea0fabdc3b594f7a814 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 12:44:59 -0700 Subject: [PATCH 06/14] Moves ClassifiedSpanVisitor to Legacy namespace Moves the `ClassifiedSpanVisitor` to the `Legacy` namespace since it is only accessed by code in that namespace. This change is part of an effort to better organize the Razor compiler's internal structure. It specifically isolates legacy code. --- .../src/Language/{ => Legacy}/ClassifiedSpanVisitor.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/{ => Legacy}/ClassifiedSpanVisitor.cs (99%) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs similarity index 99% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs index e5e094f8f46..dcbd5637525 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs @@ -3,14 +3,13 @@ using System.Collections.Immutable; using System.Diagnostics; -using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Razor.Language; +namespace Microsoft.AspNetCore.Razor.Language.Legacy; -internal class ClassifiedSpanVisitor : SyntaxWalker +internal sealed class ClassifiedSpanVisitor : SyntaxWalker { private static readonly ObjectPool.Builder> Pool = DefaultPool.Create(Policy.Instance, size: 5); From e48227cc20f95fac755da5f0a7ce8e282465e7f1 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 12:58:47 -0700 Subject: [PATCH 07/14] Pools ClassifiedSpanVisitor instances to reduce allocations Replaces the pooled `ImmutableArray.Builder` with a pooled `ClassifiedSpanVisitor` to avoid allocating a new visitor instance each time. The `ImmutableArray.Builder` is still effectively pooled, since it is owned by `ClassifiedSpanVisitor`. This change reduces overall memory allocations and improves performance. --- .../Language/Legacy/ClassifiedSpanVisitor.cs | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs index dcbd5637525..2a8eefbaca6 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs @@ -11,33 +11,40 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy; internal sealed class ClassifiedSpanVisitor : SyntaxWalker { - private static readonly ObjectPool.Builder> Pool = DefaultPool.Create(Policy.Instance, size: 5); + private static readonly ObjectPool Pool = DefaultPool.Create(Policy.Instance, size: 5); - private readonly RazorSourceDocument _source; private readonly ImmutableArray.Builder _spans; + private RazorSourceDocument _source = null!; private SyntaxNode? _currentBlock; private SourceSpan? _currentBlockSpan; private BlockKindInternal _currentBlockKind; - private ClassifiedSpanVisitor(RazorSourceDocument source, ImmutableArray.Builder spans) + private ClassifiedSpanVisitor() { - _source = source; - _spans = spans; + _spans = ImmutableArray.CreateBuilder(); + _source = null!; + } + private void Initialize(RazorSourceDocument source) + { + _source = source; _currentBlockKind = BlockKindInternal.Markup; } public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) { - using var _ = Pool.GetPooledObject(out var builder); + using var _ = Pool.GetPooledObject(out var visitor); - var visitor = new ClassifiedSpanVisitor(syntaxTree.Source, builder); + visitor.Initialize(syntaxTree.Source); visitor.Visit(syntaxTree.Root); - return builder.ToImmutableAndClear(); + return visitor.GetSpansAndClear(); } + private ImmutableArray GetSpansAndClear() + => _spans.ToImmutableAndClear(); + public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { using (CommentBlock(node)) @@ -426,7 +433,23 @@ private void AddSpan(SyntaxToken token, SpanKindInternal kind, AcceptedCharacter private void AddSpan(SourceSpan span, SpanKindInternal kind, AcceptedCharactersInternal acceptedCharacters) => _spans.Add(new(span, CurrentBlockSpan, kind, _currentBlockKind, acceptedCharacters)); - private sealed class Policy : IPooledObjectPolicy.Builder> + private void Reset() + { + _spans.Clear(); + + if (_spans.Capacity > Policy.MaximumObjectSize) + { + // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger + _spans.Capacity = 0; + } + + _source = null!; + _currentBlock = null!; + _currentBlockSpan = null; + _currentBlockKind = BlockKindInternal.Markup; + } + + private sealed class Policy : IPooledObjectPolicy { public static readonly Policy Instance = new(); @@ -438,17 +461,11 @@ private Policy() { } - public ImmutableArray.Builder Create() => ImmutableArray.CreateBuilder(); + public ClassifiedSpanVisitor Create() => new(); - public bool Return(ImmutableArray.Builder builder) + public bool Return(ClassifiedSpanVisitor visitor) { - builder.Clear(); - - if (builder.Capacity > MaximumObjectSize) - { - // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger - builder.Capacity = 0; - } + visitor.Reset(); return true; } From 6a37ec77e9f8a8d5c8fe2aace367f1dfef9a16c5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 13:06:24 -0700 Subject: [PATCH 08/14] Uses ArgHelper in legacy RazorSyntaxTreeExtensions Replaces explicit null checks with `ArgHelper.ThrowIfNull` in `RazorSyntaxTreeExtensions` for conciseness. --- .../src/Language/Legacy/RazorSyntaxTreeExtensions.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs index 67b03324925..016dcc9e5bc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RazorSyntaxTreeExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Immutable; namespace Microsoft.AspNetCore.Razor.Language.Legacy; @@ -10,20 +9,14 @@ internal static class RazorSyntaxTreeExtensions { public static ImmutableArray GetClassifiedSpans(this RazorSyntaxTree syntaxTree) { - if (syntaxTree == null) - { - throw new ArgumentNullException(nameof(syntaxTree)); - } + ArgHelper.ThrowIfNull(syntaxTree); return ClassifiedSpanVisitor.VisitRoot(syntaxTree); } public static ImmutableArray GetTagHelperSpans(this RazorSyntaxTree syntaxTree) { - if (syntaxTree == null) - { - throw new ArgumentNullException(nameof(syntaxTree)); - } + ArgHelper.ThrowIfNull(syntaxTree); return TagHelperSpanVisitor.VisitRoot(syntaxTree); } From 3fba16d4513d364ff1aef0e9dd73bb729c75fcf2 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 13:15:32 -0700 Subject: [PATCH 09/14] Moves TagHelperSpanVisitor to Legacy namespace Moves the `TagHelperSpanVisitor` to the `Legacy` namespace since it is only accessed by code in that namespace. This change is part of an effort to better organize the Razor compiler's internal structure. It specifically isolates legacy code. --- .../src/Language/{ => Legacy}/TagHelperSpanVisitor.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/{ => Legacy}/TagHelperSpanVisitor.cs (89%) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs similarity index 89% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs index f812a1c240a..f680a1a00d9 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/TagHelperSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/TagHelperSpanVisitor.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; -using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -namespace Microsoft.AspNetCore.Razor.Language; +namespace Microsoft.AspNetCore.Razor.Language.Legacy; -internal class TagHelperSpanVisitor : SyntaxWalker +internal sealed class TagHelperSpanVisitor : SyntaxWalker { private readonly RazorSourceDocument _source; private readonly ImmutableArray.Builder _spans; From a71697fe3f3191387b69c6991939a7c006e58867 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 13:50:18 -0700 Subject: [PATCH 10/14] Improves Razor formatting span handling Refactors formatting visitor to use a ref struct SpanComputer for generating text spans, improving efficiency. Consolidates span creation logic and simplifies block management with a BlockSaver struct for cleaner code. --- .../Language/Legacy/ClassifiedSpanVisitor.cs | 8 +- ...{SourceSpanComputer.cs => SpanComputer.cs} | 31 ++- .../Formatting/FormattingVisitor.cs | 247 ++++++++++++------ 3 files changed, 191 insertions(+), 95 deletions(-) rename src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/{SourceSpanComputer.cs => SpanComputer.cs} (73%) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs index 2a8eefbaca6..c3cc20f56b6 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/ClassifiedSpanVisitor.cs @@ -211,14 +211,14 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) using (MarkupBlock(node)) { // For attributes, we add a single span from the start of the name prefix to the end of the value prefix. - var spanComputer = new SourceSpanComputer(_source); + var spanComputer = new SpanComputer(); spanComputer.Add(node.NamePrefix); spanComputer.Add(node.Name); spanComputer.Add(node.NameSuffix); spanComputer.Add(node.EqualsToken); spanComputer.Add(node.ValuePrefix); - var sourceSpan = spanComputer.ToSourceSpan(); + var sourceSpan = spanComputer.ToSourceSpan(_source); AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); @@ -251,11 +251,11 @@ public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttribute using (MarkupBlock(node)) { // For minimized attributes, we add a single span for the attribute name along with the name prefix. - var spanComputer = new SourceSpanComputer(_source); + var spanComputer = new SpanComputer(); spanComputer.Add(node.NamePrefix); spanComputer.Add(node.Name); - var sourceSpan = spanComputer.ToSourceSpan(); + var sourceSpan = spanComputer.ToSourceSpan(_source); AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs similarity index 73% rename from src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs rename to src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs index 87f4eec304d..b450434c1bc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpanComputer.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SpanComputer.cs @@ -3,16 +3,16 @@ using System.Diagnostics; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.Language; /// -/// Helper that can be used to efficiently build up a from a set of syntax tokens. +/// Helper that can be used to efficiently build up a or +/// from a set of syntax tokens. /// -internal ref struct SourceSpanComputer(RazorSourceDocument source) +internal ref struct SpanComputer() { - private readonly RazorSourceDocument _source = source; - private SyntaxToken _firstToken; private SyntaxToken _lastToken; @@ -91,7 +91,7 @@ public void Add(UnclassifiedTextLiteralSyntax? literal) Add(literal?.LiteralTokens); } - public readonly SourceSpan ToSourceSpan() + public readonly SourceSpan ToSourceSpan(RazorSourceDocument source) { if (_firstToken.Kind == SyntaxKind.None) { @@ -107,11 +107,28 @@ public readonly SourceSpan ToSourceSpan() var length = end - start; - var text = _source.Text; + var text = source.Text; var startLinePosition = text.Lines.GetLinePosition(start); var endLinePosition = text.Lines.GetLinePosition(end); var lineCount = endLinePosition.Line - startLinePosition.Line; - return new SourceSpan(_source.FilePath, absoluteIndex: start, startLinePosition.Line, startLinePosition.Character, length, lineCount, endLinePosition.Character); + return new SourceSpan(source.FilePath, absoluteIndex: start, startLinePosition.Line, startLinePosition.Character, length, lineCount, endLinePosition.Character); + } + + public readonly TextSpan ToTextSpan() + { + if (_firstToken.Kind == SyntaxKind.None) + { + return default; + } + + Debug.Assert(_lastToken.Kind != SyntaxKind.None, "Last token should not be None when first token is set."); + + var start = _firstToken.Span.Start; + var end = _lastToken.Span.End; + + Debug.Assert(start <= end, "Start position should not be greater than end position."); + + return TextSpan.FromBounds(start, end); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs index 315f8d2916f..219672af014 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs @@ -7,19 +7,13 @@ using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Text; -using RazorSyntaxToken = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxToken; -using RazorSyntaxTokenList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxTokenList; -using RazorSyntaxWalker = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxWalker; - namespace Microsoft.CodeAnalysis.Razor.Formatting; -// There is already RazorSyntaxNode so not following that pattern for this alias -using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; +using Microsoft.AspNetCore.Razor.Language.Syntax; -internal class FormattingVisitor : RazorSyntaxWalker +internal class FormattingVisitor : SyntaxWalker { private const string HtmlTag = "html"; @@ -43,24 +37,23 @@ public FormattingVisitor(bool inGlobalNamespace) public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Comment, razorCommentSyntax => + using (CommentBlock(node)) { // We only want to move the start of the comment into the right spot, so we only // create spans for the start. // The body of the comment, including whitespace before the "*@" is left exactly // as the user has it in the file. - WriteSpan(razorCommentSyntax.StartCommentTransition, FormattingSpanKind.Transition); - WriteSpan(razorCommentSyntax.StartCommentStar, FormattingSpanKind.MetaCode); - }); + AddSpan(node.StartCommentTransition, FormattingSpanKind.Transition); + AddSpan(node.StartCommentStar, FormattingSpanKind.MetaCode); + } } public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) { - if (node.Parent is CSharpStatementBodySyntax || - node.Parent is CSharpImplicitExpressionBodySyntax || - node.Parent is RazorDirectiveBodySyntax || - (_currentBlockKind == FormattingBlockKind.Directive && - node.Parent?.Parent is RazorDirectiveBodySyntax)) + if (node.Parent is CSharpStatementBodySyntax or + CSharpImplicitExpressionBodySyntax or + RazorDirectiveBodySyntax || + (_currentBlockKind == FormattingBlockKind.Directive && node.Parent?.Parent is RazorDirectiveBodySyntax)) { // If we get here, it means we don't want this code block to be considered significant. // Without this, we would have double indentation in places where @@ -97,37 +90,58 @@ node.Parent is RazorDirectiveBodySyntax || return; } - WriteBlock(node, FormattingBlockKind.Statement, base.VisitCSharpCodeBlock); + using (StatementBlock(node)) + { + base.VisitCSharpCodeBlock(node); + } } public override void VisitCSharpStatement(CSharpStatementSyntax node) { - WriteBlock(node, FormattingBlockKind.Statement, base.VisitCSharpStatement); + using (StatementBlock(node)) + { + base.VisitCSharpStatement(node); + } } public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) { - WriteBlock(node, FormattingBlockKind.Expression, base.VisitCSharpExplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpExplicitExpression(node); + } } public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) { - WriteBlock(node, FormattingBlockKind.Expression, base.VisitCSharpImplicitExpression); + using (ExpressionBlock(node)) + { + base.VisitCSharpImplicitExpression(node); + } } public override void VisitRazorDirective(RazorDirectiveSyntax node) { - WriteBlock(node, FormattingBlockKind.Directive, base.VisitRazorDirective); + using (DirectiveBlock(node)) + { + base.VisitRazorDirective(node); + } } public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Template, base.VisitCSharpTemplateBlock); + using (TemplateBlock(node)) + { + base.VisitCSharpTemplateBlock(node); + } } public override void VisitMarkupBlock(MarkupBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupBlock); + using (MarkupBlock(node)) + { + base.VisitMarkupBlock(node); + } } public override void VisitMarkupElement(MarkupElementSyntax node) @@ -160,19 +174,20 @@ public override void VisitMarkupElement(MarkupElementSyntax node) public override void VisitMarkupStartTag(MarkupStartTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node); + foreach (var child in children) { Visit(child); } - }); + } } public override void VisitMarkupEndTag(MarkupEndTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node); @@ -180,7 +195,7 @@ public override void VisitMarkupEndTag(MarkupEndTagSyntax node) { Visit(child); } - }); + } } public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) @@ -299,48 +314,68 @@ static bool HasUnspecifiedCascadingTypeParameter(MarkupTagHelperElementSyntax no public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - foreach (var child in n.LegacyChildren) + foreach (var child in node.LegacyChildren) { Visit(child); } - }); + } } public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - foreach (var child in n.LegacyChildren) + foreach (var child in node.LegacyChildren) { Visit(child); } - }); + } } public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, n => + using (MarkupBlock(node)) { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new RazorSyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); - Visit(mergedAttributePrefix); + // For attributes, we add a single span from the start of the name prefix to the end of the value prefix. + var spanComputer = new SpanComputer(); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + spanComputer.Add(node.NameSuffix); + spanComputer.Add(node.EqualsToken); + spanComputer.Add(node.ValuePrefix); + + var textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + + // Visit the value and value suffix separately. Visit(node.Value); Visit(node.ValueSuffix); - }); + } } public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) { - WriteBlock(node, FormattingBlockKind.Tag, n => + using (TagBlock(node)) { - var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new RazorSyntaxTokenList(node.EqualsToken), chunkGenerator: null); - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); - Visit(mergedAttributePrefix); + // For attributes, we add a single span from the start of the name prefix to the end of the value prefix. + var spanComputer = new SpanComputer(); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + spanComputer.Add(node.NameSuffix); + spanComputer.Add(node.EqualsToken); + spanComputer.Add(node.ValuePrefix); + + var textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + + // Visit the value and value suffix separately. Visit(node.Value); Visit(node.ValueSuffix); - }); + } } public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node) @@ -358,26 +393,41 @@ public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinim public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, n => + using (MarkupBlock(node)) { - var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name); - Visit(mergedAttributePrefix); - }); + // For minimized attributes, we add a single span for the attribute name along with the name prefix. + var spanComputer = new SpanComputer(); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + + var textSpan = spanComputer.ToTextSpan(); + + AddSpan(textSpan, FormattingSpanKind.Markup); + } } public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) { - WriteBlock(node, FormattingBlockKind.HtmlComment, base.VisitMarkupCommentBlock); + using (HtmlCommentBlock(node)) + { + base.VisitMarkupCommentBlock(node); + } } public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupDynamicAttributeValue); + using (MarkupBlock(node)) + { + base.VisitMarkupDynamicAttributeValue(node); + } } public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node) { - WriteBlock(node, FormattingBlockKind.Markup, base.VisitMarkupTagHelperAttributeValue); + using (MarkupBlock(node)) + { + base.VisitMarkupTagHelperAttributeValue(node); + } } public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) @@ -385,11 +435,11 @@ public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) if (node.Parent is MarkupTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo.Bound: true }) { // For @bind attributes we want to pretend that we're in a Html context, so write this span as markup - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); } else { - WriteSpan(node, FormattingSpanKind.MetaCode); + AddSpan(node, FormattingSpanKind.MetaCode); } base.VisitRazorMetaCode(node); @@ -397,13 +447,13 @@ public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) public override void VisitCSharpTransition(CSharpTransitionSyntax node) { - WriteSpan(node, FormattingSpanKind.Transition); + AddSpan(node, FormattingSpanKind.Transition); base.VisitCSharpTransition(node); } public override void VisitMarkupTransition(MarkupTransitionSyntax node) { - WriteSpan(node, FormattingSpanKind.Transition); + AddSpan(node, FormattingSpanKind.Transition); base.VisitMarkupTransition(node); } @@ -420,7 +470,7 @@ public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax no // being "inside" the block. if (node.LiteralTokens is not [{ Kind: SyntaxKind.Marker }]) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); } base.VisitCSharpStatementLiteral(node); @@ -428,25 +478,25 @@ public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax no public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); base.VisitCSharpExpressionLiteral(node); } public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Code); + AddSpan(node, FormattingSpanKind.Code); base.VisitCSharpEphemeralTextLiteral(node); } public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.None); + AddSpan(node, FormattingSpanKind.None); base.VisitUnclassifiedTextLiteral(node); } public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) { - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupLiteralAttributeValue(node); } @@ -458,56 +508,85 @@ public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) return; } - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupTextLiteral(node); } public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) { - WriteSpan(node, FormattingSpanKind.Markup); + AddSpan(node, FormattingSpanKind.Markup); base.VisitMarkupEphemeralTextLiteral(node); } - private void WriteBlock(TNode node, FormattingBlockKind kind, Action handler) where TNode : SyntaxNode + private BlockSaver CommentBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Comment); + + private BlockSaver DirectiveBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Directive); + + private BlockSaver ExpressionBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Expression); + + private BlockSaver HtmlCommentBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.HtmlComment); + + private BlockSaver MarkupBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Markup); + + private BlockSaver StatementBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Statement); + + private BlockSaver TagBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Tag); + + private BlockSaver TemplateBlock(SyntaxNode node) + => Block(node, FormattingBlockKind.Template); + + private BlockSaver Block(SyntaxNode node, FormattingBlockKind kind) { - var previousBlock = _currentBlock; - var previousKind = _currentBlockKind; + var saver = new BlockSaver(this); _currentBlock = node; _currentBlockKind = kind; - handler(node); + return saver; + } - _currentBlock = previousBlock; - _currentBlockKind = previousKind; + private readonly ref struct BlockSaver(FormattingVisitor visitor) + { + private readonly SyntaxNode? _previousBlock = visitor._currentBlock; + private readonly FormattingBlockKind _previousKind = visitor._currentBlockKind; + + public void Dispose() + { + visitor._currentBlock = _previousBlock; + visitor._currentBlockKind = _previousKind; + } } - private void WriteSpan(SyntaxNode node, FormattingSpanKind kind) + private void AddSpan(SyntaxNode node, FormattingSpanKind kind) { if (node.IsMissing) { return; } - Assumes.NotNull(_currentBlock); + AddSpan(node.Span, kind); + } - var span = new FormattingSpan( - node.Span, - _currentBlock.Span, - kind, - _currentBlockKind, - _currentRazorIndentationLevel, - _currentHtmlIndentationLevel, - isInGlobalNamespace: _inGlobalNamespace, - isInClassBody: _isInClassBody, - _currentComponentIndentationLevel); + private void AddSpan(SyntaxToken token, FormattingSpanKind kind) + { + if (token.IsMissing) + { + return; + } - _spans.Add(span); + AddSpan(token.Span, kind); } - private void WriteSpan(RazorSyntaxToken token, FormattingSpanKind kind) + private void AddSpan(TextSpan textSpan, FormattingSpanKind kind) { - if (token.IsMissing) + if (textSpan.IsEmpty) { return; } @@ -515,7 +594,7 @@ private void WriteSpan(RazorSyntaxToken token, FormattingSpanKind kind) Assumes.NotNull(_currentBlock); var span = new FormattingSpan( - token.Span, + textSpan, _currentBlock.Span, kind, _currentBlockKind, From d7a4515f92075a5a08594e69367066a1b2822235 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 13:56:06 -0700 Subject: [PATCH 11/14] Make SyntaxUtilities.MergeTextLiterals private This method is no longer used outside of SyntaxUtilities. --- .../src/Language/Syntax/SyntaxUtilities.cs | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs index ed227f7b2d4..0efeb668a9e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/SyntaxUtilities.cs @@ -9,39 +9,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax; internal static class SyntaxUtilities { - public static MarkupTextLiteralSyntax MergeTextLiterals(params ReadOnlySpan literals) - { - SyntaxNode? parent = null; - var position = 0; - var seenFirstLiteral = false; - - using PooledArrayBuilder builder = []; - - foreach (var literal in literals) - { - if (literal == null) - { - continue; - } - - if (!seenFirstLiteral) - { - // Set the parent and position of the merged literal to the value of the first non-null literal. - parent = literal.Parent; - position = literal.Position; - seenFirstLiteral = true; - } - - builder.AddRange(literal.LiteralTokens); - } - - return (MarkupTextLiteralSyntax)InternalSyntax.SyntaxFactory - .MarkupTextLiteral( - literalTokens: builder.ToGreenListNode().ToGreenList(), - chunkGenerator: null) - .CreateRed(parent, position); - } - internal static SyntaxList GetRewrittenMarkupStartTagChildren( MarkupStartTagSyntax node, bool includeEditHandler = false) { @@ -120,6 +87,39 @@ void AddLiteralIsIfNeeded() } } + private static MarkupTextLiteralSyntax MergeTextLiterals(params ReadOnlySpan literals) + { + SyntaxNode? parent = null; + var position = 0; + var seenFirstLiteral = false; + + using PooledArrayBuilder builder = []; + + foreach (var literal in literals) + { + if (literal == null) + { + continue; + } + + if (!seenFirstLiteral) + { + // Set the parent and position of the merged literal to the value of the first non-null literal. + parent = literal.Parent; + position = literal.Position; + seenFirstLiteral = true; + } + + builder.AddRange(literal.LiteralTokens); + } + + return (MarkupTextLiteralSyntax)InternalSyntax.SyntaxFactory + .MarkupTextLiteral( + literalTokens: builder.ToGreenListNode().ToGreenList(), + chunkGenerator: null) + .CreateRed(parent, position); + } + internal static SyntaxList GetRewrittenMarkupEndTagChildren( MarkupEndTagSyntax node, bool includeEditHandler = false) { From d51064bf70fdfcb031619a86fdc25ddd7b590053 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 14:02:44 -0700 Subject: [PATCH 12/14] Uses pooled array builder for formatting spans Improves performance by utilizing a pooled array builder to avoid allocations when collecting formatting spans. This change reduces memory pressure and improves the efficiency of the formatting process. --- .../Formatting/FormattingContext.cs | 27 ++++++++++--------- .../Formatting/FormattingVisitor.cs | 16 +++++++---- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs index 2f2ac128c9b..31de89f1551 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; @@ -19,7 +20,7 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal sealed class FormattingContext { - private IReadOnlyList? _formattingSpans; + private ImmutableArray? _formattingSpans; private IReadOnlyDictionary? _indentations; private readonly bool _useNewFormattingEngine; @@ -143,25 +144,27 @@ public IReadOnlyDictionary GetIndentations() return _indentations; } - private IReadOnlyList GetFormattingSpans() + private ImmutableArray GetFormattingSpans() { - if (_formattingSpans is null) + return _formattingSpans ??= ComputeFormattingSpans(CodeDocument); + + static ImmutableArray ComputeFormattingSpans(RazorCodeDocument codeDocument) { - var syntaxTree = CodeDocument.GetRequiredSyntaxTree(); - var inGlobalNamespace = CodeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace) && + var syntaxTree = codeDocument.GetRequiredSyntaxTree(); + var inGlobalNamespace = codeDocument.TryGetNamespace(fallbackToRootNamespace: true, out var @namespace) && string.IsNullOrEmpty(@namespace); - _formattingSpans = GetFormattingSpans(syntaxTree, inGlobalNamespace: inGlobalNamespace); - } - return _formattingSpans; + return GetFormattingSpans(syntaxTree, inGlobalNamespace: inGlobalNamespace); + } } - private static IReadOnlyList GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace) + private static ImmutableArray GetFormattingSpans(RazorSyntaxTree syntaxTree, bool inGlobalNamespace) { - var visitor = new FormattingVisitor(inGlobalNamespace: inGlobalNamespace); - visitor.Visit(syntaxTree.Root); + using var _ = ArrayBuilderPool.GetPooledObject(out var formattingSpans); + + FormattingVisitor.VisitRoot(syntaxTree, formattingSpans, inGlobalNamespace); - return visitor.FormattingSpans; + return formattingSpans.ToImmutableAndClear(); } /// diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs index 219672af014..bab1ab25072 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingVisitor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Language; @@ -13,12 +14,12 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.AspNetCore.Razor.Language.Syntax; -internal class FormattingVisitor : SyntaxWalker +internal sealed class FormattingVisitor : SyntaxWalker { private const string HtmlTag = "html"; + private readonly ImmutableArray.Builder _spans; private readonly bool _inGlobalNamespace; - private readonly List _spans; private FormattingBlockKind _currentBlockKind; private SyntaxNode? _currentBlock; private int _currentHtmlIndentationLevel = 0; @@ -26,14 +27,19 @@ internal class FormattingVisitor : SyntaxWalker private int _currentComponentIndentationLevel = 0; private bool _isInClassBody = false; - public FormattingVisitor(bool inGlobalNamespace) + private FormattingVisitor(ImmutableArray.Builder spans, bool inGlobalNamespace) { _inGlobalNamespace = inGlobalNamespace; - _spans = new List(); + _spans = spans; _currentBlockKind = FormattingBlockKind.Markup; } - public IReadOnlyList FormattingSpans => _spans; + public static void VisitRoot( + RazorSyntaxTree syntaxTree, ImmutableArray.Builder spans, bool inGlobalNamespace) + { + var visitor = new FormattingVisitor(spans, inGlobalNamespace); + visitor.Visit(syntaxTree.Root); + } public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { From a8dcf4a43603014bb1d4175b0a16f17f223dd3c5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 14:25:03 -0700 Subject: [PATCH 13/14] Improves tag helper span computation performance Utilizes a pooled array builder when computing tag helper spans, reducing memory allocations. Avoids calling into the legacy `GetTagHelperSpans` method, further improving performance by avoiding allocations of `TagHelperSpanVisitor`. In addition, returns `SourceSpans` rather than `TagHelperSpanInternal` since tooling doesn't use the tag helper binding info. --- .../RazorCodeDocumentExtensions.CachedData.cs | 26 ++++++++++++++++--- .../Extensions/RazorCodeDocumentExtensions.cs | 8 +++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs index 5ca184053bf..5a0a26323e2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs @@ -7,6 +7,8 @@ using System.Runtime.CompilerServices; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.Threading; @@ -40,7 +42,7 @@ private sealed class CachedData(RazorCodeDocument codeDocument) private readonly SemaphoreSlim _stateLock = new(initialCount: 1); private SyntaxTree? _syntaxTree; private ImmutableArray? _classifiedSpans; - private ImmutableArray? _tagHelperSpans; + private ImmutableArray? _tagHelperSpans; public SyntaxTree GetOrParseCSharpSyntaxTree(CancellationToken cancellationToken) { @@ -74,7 +76,7 @@ public ImmutableArray GetOrComputeClassifiedSpans(Cancel } } - public ImmutableArray GetOrComputeTagHelperSpans(CancellationToken cancellationToken) + public ImmutableArray GetOrComputeTagHelperSpans(CancellationToken cancellationToken) { if (_tagHelperSpans is { } tagHelperSpans) { @@ -83,7 +85,25 @@ public ImmutableArray GetOrComputeTagHelperSpans(Cancella using (_stateLock.DisposableWait(cancellationToken)) { - return _tagHelperSpans ??= _codeDocument.GetRequiredSyntaxTree().GetTagHelperSpans(); + return _tagHelperSpans ??= ComputeTagHelperSpans(_codeDocument.GetRequiredSyntaxTree()); + } + + static ImmutableArray ComputeTagHelperSpans(RazorSyntaxTree syntaxTree) + { + using var builder = new PooledArrayBuilder(); + + foreach (var node in syntaxTree.Root.DescendantNodes()) + { + if (node is not MarkupTagHelperElementSyntax tagHelperElement || + tagHelperElement.TagHelperInfo is null) + { + continue; + } + + builder.Add(tagHelperElement.GetSourceSpan(syntaxTree.Source)); + } + + return builder.ToImmutableAndClear(); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs index 8a459a1ed8d..b05f204d3da 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs @@ -153,12 +153,12 @@ public static RazorLanguageKind GetLanguageKind(this RazorCodeDocument codeDocum private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeClassifiedSpans(CancellationToken.None); - private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) + private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeTagHelperSpans(CancellationToken.None); private static RazorLanguageKind GetLanguageKindCore( ImmutableArray classifiedSpans, - ImmutableArray tagHelperSpans, + ImmutableArray tagHelperSpans, int hostDocumentIndex, int hostDocumentLength, bool rightAssociative) @@ -206,10 +206,8 @@ private static RazorLanguageKind GetLanguageKindCore( } } - foreach (var tagHelperSpan in tagHelperSpans) + foreach (var span in tagHelperSpans) { - var span = tagHelperSpan.Span; - if (span.AbsoluteIndex <= hostDocumentIndex) { var end = span.AbsoluteIndex + span.Length; From 5059a2645c875a265d5e1df95047fbbb3511eade Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 20 Jun 2025 14:47:31 -0700 Subject: [PATCH 14/14] Refactors classified span computation Refactors the way classified spans are computed for Razor code documents intooling. This change replaces the usage of legacy compiler APIs with a new `ClassifiedSpanVisitor`. The new implementation is streamlined to only produce the information needed for the `GetLanguageKind()` API, improving performance and reducing dependencies. This also includes changing the `ClassifiedSpanInternal` to `ClassifiedSpan` and removing the dependency on `Microsoft.AspNetCore.Razor.Language.Legacy`. --- .../RazorCodeDocumentExtensions.CachedData.cs | 7 +- .../Extensions/RazorCodeDocumentExtensions.cs | 16 +- ...rCodeDocumentExtensions_ClassifiedSpans.cs | 486 ++++++++++++++++++ 3 files changed, 497 insertions(+), 12 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs index 5a0a26323e2..7bfd9bc5895 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.CachedData.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; -using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; @@ -41,7 +40,7 @@ private sealed class CachedData(RazorCodeDocument codeDocument) private readonly SemaphoreSlim _stateLock = new(initialCount: 1); private SyntaxTree? _syntaxTree; - private ImmutableArray? _classifiedSpans; + private ImmutableArray? _classifiedSpans; private ImmutableArray? _tagHelperSpans; public SyntaxTree GetOrParseCSharpSyntaxTree(CancellationToken cancellationToken) @@ -63,7 +62,7 @@ static SyntaxTree ParseSyntaxTree(RazorCodeDocument codeDocument, CancellationTo } } - public ImmutableArray GetOrComputeClassifiedSpans(CancellationToken cancellationToken) + public ImmutableArray GetOrComputeClassifiedSpans(CancellationToken cancellationToken) { if (_classifiedSpans is { } classifiedSpans) { @@ -72,7 +71,7 @@ public ImmutableArray GetOrComputeClassifiedSpans(Cancel using (_stateLock.DisposableWait(cancellationToken)) { - return _classifiedSpans ??= _codeDocument.GetRequiredSyntaxTree().GetClassifiedSpans(); + return _classifiedSpans ??= ClassifiedSpanVisitor.VisitRoot(_codeDocument.GetRequiredSyntaxTree()); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs index b05f204d3da..ea1abdf6280 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Intermediate; -using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -150,14 +150,14 @@ public static RazorLanguageKind GetLanguageKind(this RazorCodeDocument codeDocum return GetLanguageKindCore(classifiedSpans, tagHelperSpans, hostDocumentIndex, documentLength, rightAssociative); } - private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) + private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeClassifiedSpans(CancellationToken.None); private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) => GetCachedData(document).GetOrComputeTagHelperSpans(CancellationToken.None); private static RazorLanguageKind GetLanguageKindCore( - ImmutableArray classifiedSpans, + ImmutableArray classifiedSpans, ImmutableArray tagHelperSpans, int hostDocumentIndex, int hostDocumentLength, @@ -178,7 +178,7 @@ private static RazorLanguageKind GetLanguageKindCore( { // We're at an edge. - if (classifiedSpan.SpanKind is SpanKindInternal.MetaCode or SpanKindInternal.Transition) + if (classifiedSpan.Kind is SpanKind.MetaCode or SpanKind.Transition) { // If we're on an edge of a transition of some kind (MetaCode representing an open or closing piece of syntax such as <|, // and Transition representing an explicit transition to/from razor syntax, such as @|), prefer to classify to the span @@ -236,13 +236,13 @@ private static RazorLanguageKind GetLanguageKindCore( // Default to Razor return RazorLanguageKind.Razor; - static RazorLanguageKind GetLanguageFromClassifiedSpan(ClassifiedSpanInternal classifiedSpan) + static RazorLanguageKind GetLanguageFromClassifiedSpan(ClassifiedSpan classifiedSpan) { // Overlaps with request - return classifiedSpan.SpanKind switch + return classifiedSpan.Kind switch { - SpanKindInternal.Markup => RazorLanguageKind.Html, - SpanKindInternal.Code => RazorLanguageKind.CSharp, + SpanKind.Markup => RazorLanguageKind.Html, + SpanKind.Code => RazorLanguageKind.CSharp, // Content type was non-C# or Html or we couldn't find a classified span overlapping the request position. // All other classified span kinds default back to Razor diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs new file mode 100644 index 00000000000..c03724adf70 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions_ClassifiedSpans.cs @@ -0,0 +1,486 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.Razor.Language; + +internal static partial class RazorCodeDocumentExtensions +{ + // This is modified copy of the ClassifiedSpanVisitor from the legacy compiler APIs. + // It is streamlined to only produce the information needed for tooling's + // GetLanguageKind() API. + // + // Note that the legacy ClassifiedSpanVisitor will be removed with the legacy editor. + + private enum SpanKind + { + Transition, + MetaCode, + Comment, + Code, + Markup, + None + } + + private record struct ClassifiedSpan(SourceSpan Span, SpanKind Kind); + + private sealed class ClassifiedSpanVisitor : SyntaxWalker + { + private enum BlockKind + { + // Code + Statement, + Directive, + Expression, + + // Markup + Markup, + Template, + + // Special + Comment, + Tag, + HtmlComment + } + + private static readonly ObjectPool s_pool = DefaultPool.Create(Policy.Instance, size: 5); + + private readonly ImmutableArray.Builder _spans; + + private RazorSourceDocument _source; + private BlockKind _currentBlockKind; + + private ClassifiedSpanVisitor() + { + _spans = ImmutableArray.CreateBuilder(); + _source = null!; + } + + private void Initialize(RazorSourceDocument source) + { + _source = source; + _currentBlockKind = BlockKind.Markup; + } + + public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) + { + using var _ = s_pool.GetPooledObject(out var visitor); + + visitor.Initialize(syntaxTree.Source); + visitor.Visit(syntaxTree.Root); + + return visitor.GetSpansAndClear(); + } + + private ImmutableArray GetSpansAndClear() + => _spans.ToImmutableAndClear(); + + public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) + { + using (CommentBlock()) + { + AddSpan(node.StartCommentTransition, SpanKind.Transition); + AddSpan(node.StartCommentStar, SpanKind.MetaCode); + + var comment = node.Comment; + + if (comment.IsMissing) + { + // We need to generate a classified span at this position. So insert a marker in its place. + comment = SyntaxFactory.Token(SyntaxKind.Marker, parent: node, position: node.StartCommentStar.EndPosition); + } + + AddSpan(comment, SpanKind.Comment); + + AddSpan(node.EndCommentStar, SpanKind.MetaCode); + AddSpan(node.EndCommentTransition, SpanKind.Transition); + } + } + + public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) + { + if (node.Parent is CSharpStatementBodySyntax or + CSharpExplicitExpressionBodySyntax or + CSharpImplicitExpressionBodySyntax or + RazorDirectiveBodySyntax || + (_currentBlockKind == BlockKind.Directive && node.Children is [CSharpStatementLiteralSyntax])) + { + base.VisitCSharpCodeBlock(node); + return; + } + + using (StatementBlock()) + { + base.VisitCSharpCodeBlock(node); + } + } + + public override void VisitCSharpStatement(CSharpStatementSyntax node) + { + using (StatementBlock()) + { + base.VisitCSharpStatement(node); + } + } + + public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) + { + using (ExpressionBlock()) + { + base.VisitCSharpExplicitExpression(node); + } + } + + public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) + { + using (ExpressionBlock()) + { + base.VisitCSharpImplicitExpression(node); + } + } + + public override void VisitRazorDirective(RazorDirectiveSyntax node) + { + using (DirectiveBlock()) + { + base.VisitRazorDirective(node); + } + } + + public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) + { + using (TemplateBlock()) + { + base.VisitCSharpTemplateBlock(node); + } + } + + public override void VisitMarkupBlock(MarkupBlockSyntax node) + { + using (MarkupBlock()) + { + base.VisitMarkupBlock(node); + } + } + + public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node) + { + // We don't generate a classified span when the attribute value is a simple literal value. + // This is done so we maintain the classified spans generated in 2.x which + // used ConditionalAttributeCollapser (combines markup literal attribute values into one span with no block parent). + if (!IsSimpleLiteralValue(node)) + { + base.VisitMarkupTagHelperAttributeValue(node); + return; + } + + using (MarkupBlock()) + { + base.VisitMarkupTagHelperAttributeValue(node); + } + + static bool IsSimpleLiteralValue(MarkupTagHelperAttributeValueSyntax node) + { + return node.Children is [MarkupDynamicAttributeValueSyntax] or { Count: > 1 }; + } + } + + public override void VisitMarkupStartTag(MarkupStartTagSyntax node) + { + using (TagBlock()) + { + var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node, includeEditHandler: true); + foreach (var child in children) + { + Visit(child); + } + } + } + + public override void VisitMarkupEndTag(MarkupEndTagSyntax node) + { + using (TagBlock()) + { + var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node, includeEditHandler: true); + + foreach (var child in children) + { + Visit(child); + } + } + } + + public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) + { + using (TagBlock()) + { + base.VisitMarkupTagHelperElement(node); + } + } + + public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) + { + foreach (var child in node.Attributes) + { + if (child is MarkupTagHelperAttributeSyntax or + MarkupTagHelperDirectiveAttributeSyntax or + MarkupMinimizedTagHelperDirectiveAttributeSyntax) + { + Visit(child); + } + } + } + + public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node) + { + // We don't want to generate a classified span for a tag helper end tag. Do nothing. + } + + public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) + { + using (MarkupBlock()) + { + // For attributes, we add a single span from the start of the name prefix to the end of the value prefix. + var spanComputer = new SpanComputer(); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + spanComputer.Add(node.NameSuffix); + spanComputer.Add(node.EqualsToken); + spanComputer.Add(node.ValuePrefix); + + var sourceSpan = spanComputer.ToSourceSpan(_source); + + AddSpan(sourceSpan, SpanKind.Markup); + + // Visit the value and value suffix separately. + Visit(node.Value); + Visit(node.ValueSuffix); + } + } + + public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) + { + Visit(node.Value); + } + + public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node) + { + Visit(node.Transition); + Visit(node.Colon); + Visit(node.Value); + } + + public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinimizedTagHelperDirectiveAttributeSyntax node) + { + Visit(node.Transition); + Visit(node.Colon); + } + + public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) + { + using (MarkupBlock()) + { + // For minimized attributes, we add a single span for the attribute name along with the name prefix. + var spanComputer = new SpanComputer(); + spanComputer.Add(node.NamePrefix); + spanComputer.Add(node.Name); + + var sourceSpan = spanComputer.ToSourceSpan(_source); + + AddSpan(sourceSpan, SpanKind.Markup); + } + } + + public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) + { + using (HtmlCommentBlock()) + { + base.VisitMarkupCommentBlock(node); + } + } + + public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) + { + using (MarkupBlock()) + { + base.VisitMarkupDynamicAttributeValue(node); + } + } + + public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) + { + AddSpan(node, SpanKind.MetaCode); + base.VisitRazorMetaCode(node); + } + + public override void VisitCSharpTransition(CSharpTransitionSyntax node) + { + AddSpan(node, SpanKind.Transition); + base.VisitCSharpTransition(node); + } + + public override void VisitMarkupTransition(MarkupTransitionSyntax node) + { + AddSpan(node, SpanKind.Transition); + base.VisitMarkupTransition(node); + } + + public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) + { + AddSpan(node, SpanKind.Code); + base.VisitCSharpStatementLiteral(node); + } + + public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) + { + AddSpan(node, SpanKind.Code); + base.VisitCSharpExpressionLiteral(node); + } + + public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) + { + AddSpan(node, SpanKind.Code); + base.VisitCSharpEphemeralTextLiteral(node); + } + + public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) + { + AddSpan(node, SpanKind.None); + base.VisitUnclassifiedTextLiteral(node); + } + + public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) + { + AddSpan(node, SpanKind.Markup); + base.VisitMarkupLiteralAttributeValue(node); + } + + public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) + { + if (node.Parent is MarkupLiteralAttributeValueSyntax) + { + base.VisitMarkupTextLiteral(node); + return; + } + + AddSpan(node, SpanKind.Markup); + base.VisitMarkupTextLiteral(node); + } + + public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) + { + AddSpan(node, SpanKind.Markup); + base.VisitMarkupEphemeralTextLiteral(node); + } + + private BlockSaver CommentBlock() + => Block(BlockKind.Comment); + + private BlockSaver DirectiveBlock() + => Block(BlockKind.Directive); + + private BlockSaver ExpressionBlock() + => Block(BlockKind.Expression); + + private BlockSaver HtmlCommentBlock() + => Block(BlockKind.HtmlComment); + + private BlockSaver MarkupBlock() + => Block(BlockKind.Markup); + + private BlockSaver StatementBlock() + => Block(BlockKind.Statement); + + private BlockSaver TagBlock() + => Block(BlockKind.Tag); + + private BlockSaver TemplateBlock() + => Block(BlockKind.Template); + + private BlockSaver Block(BlockKind kind) + { + var saver = new BlockSaver(this); + + _currentBlockKind = kind; + + return saver; + } + + private readonly ref struct BlockSaver(ClassifiedSpanVisitor visitor) + { + private readonly BlockKind _previousKind = visitor._currentBlockKind; + + public void Dispose() + { + visitor._currentBlockKind = _previousKind; + } + } + + private void AddSpan(SyntaxNode node, SpanKind kind) + { + if (node.IsMissing) + { + return; + } + + var nodeSpan = node.GetSourceSpan(_source); + + AddSpan(nodeSpan, kind); + } + + private void AddSpan(SyntaxToken token, SpanKind kind) + { + if (token.IsMissing) + { + return; + } + + var tokenSpan = token.GetSourceSpan(_source); + + AddSpan(tokenSpan, kind); + } + + private void AddSpan(SourceSpan span, SpanKind kind) + => _spans.Add(new(span, kind)); + + private void Reset() + { + _spans.Clear(); + + if (_spans.Capacity > Policy.MaximumObjectSize) + { + // Differs from ArrayBuilderPool.Policy's behavior as we allow our array to grow significantly larger + _spans.Capacity = 0; + } + + _source = null!; + _currentBlockKind = BlockKind.Markup; + } + + private sealed class Policy : IPooledObjectPolicy + { + public static readonly Policy Instance = new(); + + // Significantly larger than DefaultPool.MaximumObjectSize as there shouldn't be much concurrency + // of these arrays (we limit the number of pooled items to 5) and they are commonly large + public const int MaximumObjectSize = DefaultPool.MaximumObjectSize * 32; + + private Policy() + { + } + + public ClassifiedSpanVisitor Create() => new(); + + public bool Return(ClassifiedSpanVisitor visitor) + { + visitor.Reset(); + + return true; + } + } + } +}