Skip to content

Commit 2d90c0c

Browse files
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`.
1 parent a6a95d9 commit 2d90c0c

File tree

2 files changed

+137
-5
lines changed

2 files changed

+137
-5
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/ClassifiedSpanVisitor.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,19 @@ public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
204204
{
205205
using (MarkupBlock(node))
206206
{
207-
var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new SyntaxTokenList(node.EqualsToken), chunkGenerator: null);
208-
var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix);
207+
// For attributes, we add a single span from the start of the name prefix to the end of the value prefix.
208+
var spanComputer = new SourceSpanComputer(_source);
209+
spanComputer.Add(node.NamePrefix);
210+
spanComputer.Add(node.Name);
211+
spanComputer.Add(node.NameSuffix);
212+
spanComputer.Add(node.EqualsToken);
213+
spanComputer.Add(node.ValuePrefix);
209214

210-
Visit(mergedAttributePrefix);
215+
var sourceSpan = spanComputer.ToSourceSpan();
216+
217+
AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any);
218+
219+
// Visit the value and value suffix separately.
211220
Visit(node.Value);
212221
Visit(node.ValueSuffix);
213222
}
@@ -235,8 +244,14 @@ public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttribute
235244
{
236245
using (MarkupBlock(node))
237246
{
238-
var mergedAttributePrefix = SyntaxUtilities.MergeTextLiterals(node.NamePrefix, node.Name);
239-
Visit(mergedAttributePrefix);
247+
// For minimized attributes, we add a single span for the attribute name along with the name prefix.
248+
var spanComputer = new SourceSpanComputer(_source);
249+
spanComputer.Add(node.NamePrefix);
250+
spanComputer.Add(node.Name);
251+
252+
var sourceSpan = spanComputer.ToSourceSpan();
253+
254+
AddSpan(sourceSpan, SpanKindInternal.Markup, AcceptedCharactersInternal.Any);
240255
}
241256
}
242257

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using Microsoft.AspNetCore.Razor.Language.Syntax;
6+
7+
namespace Microsoft.AspNetCore.Razor.Language;
8+
9+
/// <summary>
10+
/// Helper that can be used to efficiently build up a <see cref="SourceSpan"/> from a set of syntax tokens.
11+
/// </summary>
12+
internal ref struct SourceSpanComputer(RazorSourceDocument source)
13+
{
14+
private readonly RazorSourceDocument _source = source;
15+
16+
private SyntaxToken _firstToken;
17+
private SyntaxToken _lastToken;
18+
19+
public void Add(SyntaxToken token)
20+
{
21+
if (token.Kind == SyntaxKind.None)
22+
{
23+
return;
24+
}
25+
26+
if (_firstToken.Kind == SyntaxKind.None)
27+
{
28+
_firstToken = token;
29+
}
30+
31+
_lastToken = token;
32+
}
33+
34+
public void Add(SyntaxTokenList tokenList)
35+
{
36+
if (tokenList.Count == 0)
37+
{
38+
return;
39+
}
40+
41+
if (_firstToken.Kind == SyntaxKind.None)
42+
{
43+
_firstToken = tokenList[0];
44+
}
45+
46+
_lastToken = tokenList[^1];
47+
}
48+
49+
public void Add(SyntaxTokenList? tokenList)
50+
{
51+
if (tokenList is not [_, ..] tokens)
52+
{
53+
return;
54+
}
55+
56+
if (_firstToken.Kind == SyntaxKind.None)
57+
{
58+
_firstToken = tokens[0];
59+
}
60+
61+
_lastToken = tokens[^1];
62+
}
63+
64+
public void Add(CSharpEphemeralTextLiteralSyntax? literal)
65+
{
66+
Add(literal?.LiteralTokens);
67+
}
68+
69+
public void Add(CSharpExpressionLiteralSyntax? literal)
70+
{
71+
Add(literal?.LiteralTokens);
72+
}
73+
74+
public void Add(CSharpStatementLiteralSyntax? literal)
75+
{
76+
Add(literal?.LiteralTokens);
77+
}
78+
79+
public void Add(MarkupEphemeralTextLiteralSyntax? literal)
80+
{
81+
Add(literal?.LiteralTokens);
82+
}
83+
84+
public void Add(MarkupTextLiteralSyntax? literal)
85+
{
86+
Add(literal?.LiteralTokens);
87+
}
88+
89+
public void Add(UnclassifiedTextLiteralSyntax? literal)
90+
{
91+
Add(literal?.LiteralTokens);
92+
}
93+
94+
public readonly SourceSpan ToSourceSpan()
95+
{
96+
if (_firstToken.Kind == SyntaxKind.None)
97+
{
98+
return default;
99+
}
100+
101+
Debug.Assert(_lastToken.Kind != SyntaxKind.None, "Last token should not be None when first token is set.");
102+
103+
var start = _firstToken.Span.Start;
104+
var end = _lastToken.Span.End;
105+
106+
Debug.Assert(start <= end, "Start position should not be greater than end position.");
107+
108+
var length = end - start;
109+
110+
var text = _source.Text;
111+
var startLinePosition = text.Lines.GetLinePosition(start);
112+
var endLinePosition = text.Lines.GetLinePosition(end);
113+
var lineCount = endLinePosition.Line - startLinePosition.Line;
114+
115+
return new SourceSpan(_source.FilePath, absoluteIndex: start, startLinePosition.Line, startLinePosition.Character, length, lineCount, endLinePosition.Character);
116+
}
117+
}

0 commit comments

Comments
 (0)