Skip to content

Commit 13534f7

Browse files
authored
Make keyword state in parser immutable (#8234)
* Make keyword state in parser immutable Splitting this simpler refactoring out from a larger immutability refactoring: there is no good reason for the keywords used in a given parse context to be mutable. They're used immutably in every real context. As part of this, I've added a reference to System.Collections.Immutable to the language dll, and we will make more use of the immutable collections in future refactorings.
1 parent 211ab2f commit 13534f7

File tree

5 files changed

+108
-120
lines changed

5 files changed

+108
-120
lines changed

src/Compiler/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@
2929
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="$(Tooling_MicrosoftCodeAnalysisNetAnalyzersPackageVersion)" NoWarn="NU1608" />
3030
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="$(Tooling_MicrosoftCodeAnalysisBannedApiAnalyzersPackageVersion)" />
3131
<PackageVersion Include="Roslyn.Diagnostics.Analyzers" Version="$(Tooling_RoslynDiagnosticsAnalyzersPackageVersion)" />
32+
<PackageVersion Include="System.Collections.Immutable" Version="$(SystemCollectionsImmutablePackageVersion)" />
3233
</ItemGroup>
3334
</Project>

src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/CSharpCodeParser.cs

Lines changed: 96 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Collections.Immutable;
89
using System.Diagnostics;
910
using System.Linq;
1011
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
@@ -48,8 +49,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
4849
builder.Description = Resources.TagHelperPrefixDirective_Description;
4950
});
5051

51-
internal static ISet<string> DefaultKeywords = new HashSet<string>()
52-
{
52+
internal static ImmutableHashSet<string> DefaultKeywords = ImmutableHashSet.Create(
5353
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
5454
SyntaxConstants.CSharp.AddTagHelperKeyword,
5555
SyntaxConstants.CSharp.RemoveTagHelperKeyword,
@@ -65,12 +65,12 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
6565
"namespace",
6666
"class",
6767
"where"
68-
};
68+
);
6969

70-
private readonly ISet<string> CurrentKeywords = new HashSet<string>(DefaultKeywords);
70+
private readonly ImmutableHashSet<string> CurrentKeywords;
7171

72-
private readonly Dictionary<CSharpKeyword, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>> _keywordParserMap = new Dictionary<CSharpKeyword, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>>();
73-
private readonly Dictionary<string, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>> _directiveParserMap = new Dictionary<string, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>>(StringComparer.Ordinal);
72+
private readonly ImmutableDictionary<CSharpKeyword, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>> _keywordParserMap;
73+
private readonly ImmutableDictionary<string, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>> _directiveParserMap;
7474

7575
public CSharpCodeParser(ParserContext context)
7676
: this(directives: Enumerable.Empty<DirectiveDescriptor>(), context: context)
@@ -90,15 +90,103 @@ public CSharpCodeParser(IEnumerable<DirectiveDescriptor> directives, ParserConte
9090
throw new ArgumentNullException(nameof(context));
9191
}
9292

93-
Keywords = new HashSet<string>();
93+
var keywordsBuilder = ImmutableHashSet<string>.Empty.ToBuilder();
94+
var keywordParserMapBuilder = ImmutableDictionary<CSharpKeyword, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>>.Empty.ToBuilder();
95+
var currentKeywordsBuilder = DefaultKeywords.ToBuilder();
96+
var directiveParserMapBuilder = ImmutableDictionary.CreateBuilder<string, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>>(StringComparer.Ordinal);
97+
9498
SetupKeywordParsers();
9599
SetupExpressionParsers();
96100
SetupDirectiveParsers(directives);
101+
102+
Keywords = keywordsBuilder.ToImmutable();
103+
CurrentKeywords = currentKeywordsBuilder.ToImmutable();
104+
_keywordParserMap = keywordParserMapBuilder.ToImmutable();
105+
_directiveParserMap = directiveParserMapBuilder.ToImmutable();
106+
107+
void SetupKeywordParsers()
108+
{
109+
MapKeywords(ParseConditionalBlock, topLevel: true, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock);
110+
MapKeywords(ParseCaseStatement, topLevel: false, CSharpKeyword.Case, CSharpKeyword.Default);
111+
MapKeywords(ParseIfStatement, topLevel: true, CSharpKeyword.If);
112+
MapKeywords(ParseTryStatement, topLevel: true, CSharpKeyword.Try);
113+
MapKeywords(ParseDoStatement, topLevel: true, CSharpKeyword.Do);
114+
MapKeywords(ParseUsingKeyword, topLevel: true, CSharpKeyword.Using);
115+
}
116+
117+
void MapKeywords(
118+
Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler,
119+
bool topLevel,
120+
params CSharpKeyword[] keywords)
121+
{
122+
foreach (var keyword in keywords)
123+
{
124+
keywordParserMapBuilder.Add(keyword, handler);
125+
if (topLevel)
126+
{
127+
keywordsBuilder.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
128+
}
129+
}
130+
}
131+
132+
void SetupExpressionParsers()
133+
{
134+
keywordParserMapBuilder.Add(CSharpKeyword.Await, ParseAwaitExpression);
135+
}
136+
137+
void SetupDirectiveParsers(IEnumerable<DirectiveDescriptor> directiveDescriptors)
138+
{
139+
foreach (var directiveDescriptor in directiveDescriptors)
140+
{
141+
currentKeywordsBuilder.Add(directiveDescriptor.Directive);
142+
MapDirectives((builder, transition) => ParseExtensibleDirective(builder, transition, directiveDescriptor), directiveParserMapBuilder, keywordsBuilder, context, directiveDescriptor.Directive);
143+
}
144+
145+
MapDirectives(ParseTagHelperPrefixDirective, directiveParserMapBuilder, keywordsBuilder, context, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
146+
MapDirectives(ParseAddTagHelperDirective, directiveParserMapBuilder, keywordsBuilder, context, SyntaxConstants.CSharp.AddTagHelperKeyword);
147+
MapDirectives(ParseRemoveTagHelperDirective, directiveParserMapBuilder, keywordsBuilder, context, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
148+
149+
// If there wasn't any extensible directives relating to the reserved directives then map them.
150+
if (!directiveParserMapBuilder.ContainsKey("class"))
151+
{
152+
MapDirectives(ParseReservedDirective, directiveParserMapBuilder, keywordsBuilder, context, "class");
153+
}
154+
155+
if (!directiveParserMapBuilder.ContainsKey("namespace"))
156+
{
157+
MapDirectives(ParseReservedDirective, directiveParserMapBuilder, keywordsBuilder, context, "namespace");
158+
}
159+
}
160+
161+
static void MapDirectives(
162+
Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler,
163+
ImmutableDictionary<string, Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax>>.Builder directiveParserMap,
164+
ImmutableHashSet<string>.Builder keywords,
165+
ParserContext context,
166+
params string[] directives)
167+
{
168+
foreach (var directive in directives)
169+
{
170+
if (directiveParserMap.ContainsKey(directive))
171+
{
172+
// It is possible for the list to contain duplicates in cases when the project is misconfigured.
173+
// In those cases, we shouldn't register multiple handlers per keyword.
174+
continue;
175+
}
176+
177+
directiveParserMap.Add(directive, (builder, transition) =>
178+
{
179+
handler(builder, transition);
180+
context.SeenDirectives.Add(directive);
181+
});
182+
keywords.Add(directive);
183+
}
184+
}
97185
}
98186

99187
public HtmlMarkupParser HtmlParser { get; set; }
100188

101-
protected internal ISet<string> Keywords { get; private set; }
189+
protected internal ImmutableHashSet<string> Keywords { get; private set; }
102190

103191
public bool IsNested { get; set; }
104192

@@ -908,30 +996,6 @@ protected bool TryParseDirective(in SyntaxListBuilder<RazorSyntaxNode> builder,
908996
return false;
909997
}
910998

911-
private void SetupDirectiveParsers(IEnumerable<DirectiveDescriptor> directiveDescriptors)
912-
{
913-
foreach (var directiveDescriptor in directiveDescriptors)
914-
{
915-
CurrentKeywords.Add(directiveDescriptor.Directive);
916-
MapDirectives((builder, transition) => ParseExtensibleDirective(builder, transition, directiveDescriptor), directiveDescriptor.Directive);
917-
}
918-
919-
MapDirectives(ParseTagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
920-
MapDirectives(ParseAddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
921-
MapDirectives(ParseRemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
922-
923-
// If there wasn't any extensible directives relating to the reserved directives then map them.
924-
if (!_directiveParserMap.ContainsKey("class"))
925-
{
926-
MapDirectives(ParseReservedDirective, "class");
927-
}
928-
929-
if (!_directiveParserMap.ContainsKey("namespace"))
930-
{
931-
MapDirectives(ParseReservedDirective, "namespace");
932-
}
933-
}
934-
935999
private void EnsureDirectiveIsAtStartOfLine()
9361000
{
9371001
// 1 is the offset of the @ transition for the directive.
@@ -955,28 +1019,6 @@ private void EnsureDirectiveIsAtStartOfLine()
9551019
}
9561020
}
9571021

958-
// Internal for testing.
959-
internal void MapDirectives(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler, params string[] directives)
960-
{
961-
foreach (var directive in directives)
962-
{
963-
if (_directiveParserMap.ContainsKey(directive))
964-
{
965-
// It is possible for the list to contain duplicates in cases when the project is misconfigured.
966-
// In those cases, we shouldn't register multiple handlers per keyword.
967-
continue;
968-
}
969-
970-
_directiveParserMap.Add(directive, (builder, transition) =>
971-
{
972-
handler(builder, transition);
973-
Context.SeenDirectives.Add(directive);
974-
});
975-
976-
Keywords.Add(directive);
977-
}
978-
}
979-
9801022
private void ParseTagHelperPrefixDirective(SyntaxListBuilder<RazorSyntaxNode> builder, CSharpTransitionSyntax transition)
9811023
{
9821024
RazorDiagnostic duplicateDiagnostic = null;
@@ -1791,51 +1833,6 @@ private bool AtBooleanLiteral()
17911833
return result.HasValue && (result.Value == CSharpKeyword.True || result.Value == CSharpKeyword.False);
17921834
}
17931835

1794-
private void SetupExpressionParsers()
1795-
{
1796-
MapExpressionKeyword(ParseAwaitExpression, CSharpKeyword.Await);
1797-
}
1798-
1799-
private void SetupKeywordParsers()
1800-
{
1801-
MapKeywords(
1802-
ParseConditionalBlock,
1803-
CSharpKeyword.For,
1804-
CSharpKeyword.Foreach,
1805-
CSharpKeyword.While,
1806-
CSharpKeyword.Switch,
1807-
CSharpKeyword.Lock);
1808-
MapKeywords(ParseCaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default);
1809-
MapKeywords(ParseIfStatement, CSharpKeyword.If);
1810-
MapKeywords(ParseTryStatement, CSharpKeyword.Try);
1811-
MapKeywords(ParseDoStatement, CSharpKeyword.Do);
1812-
MapKeywords(ParseUsingKeyword, CSharpKeyword.Using);
1813-
}
1814-
1815-
private void MapExpressionKeyword(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler, CSharpKeyword keyword)
1816-
{
1817-
_keywordParserMap.Add(keyword, handler);
1818-
1819-
// Expression keywords don't belong in the regular keyword list
1820-
}
1821-
1822-
private void MapKeywords(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler, params CSharpKeyword[] keywords)
1823-
{
1824-
MapKeywords(handler, topLevel: true, keywords: keywords);
1825-
}
1826-
1827-
private void MapKeywords(Action<SyntaxListBuilder<RazorSyntaxNode>, CSharpTransitionSyntax> handler, bool topLevel, params CSharpKeyword[] keywords)
1828-
{
1829-
foreach (var keyword in keywords)
1830-
{
1831-
_keywordParserMap.Add(keyword, handler);
1832-
if (topLevel)
1833-
{
1834-
Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
1835-
}
1836-
}
1837-
}
1838-
18391836
private void ParseAwaitExpression(SyntaxListBuilder<RazorSyntaxNode> builder, CSharpTransitionSyntax transition)
18401837
{
18411838
// Ensure that we're on the await statement (only runs in debug)

src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/ImplicitExpressionEditHandler.cs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Collections.Immutable;
89
using System.Diagnostics;
910
using System.Globalization;
1011
using System.IO;
@@ -16,30 +17,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy;
1617

1718
internal class ImplicitExpressionEditHandler : SpanEditHandler
1819
{
19-
private readonly ISet<string> _keywords;
20-
private readonly IReadOnlyCollection<string> _readOnlyKeywords;
21-
22-
public ImplicitExpressionEditHandler(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer, ISet<string> keywords, bool acceptTrailingDot)
20+
public ImplicitExpressionEditHandler(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer, ImmutableHashSet<string> keywords, bool acceptTrailingDot)
2321
: base(tokenizer)
2422
{
25-
_keywords = keywords ?? new HashSet<string>();
26-
27-
// HashSet<T> implements IReadOnlyCollection<T> as of 4.6, but does not for 4.5.1. If the runtime cast
28-
// succeeds, avoid creating a new collection.
29-
_readOnlyKeywords = (_keywords as IReadOnlyCollection<string>) ?? _keywords.ToArray();
30-
23+
Keywords = keywords;
3124
AcceptTrailingDot = acceptTrailingDot;
3225
}
3326

3427
public bool AcceptTrailingDot { get; }
3528

36-
public IReadOnlyCollection<string> Keywords
37-
{
38-
get
39-
{
40-
return _readOnlyKeywords;
41-
}
42-
}
29+
public ImmutableHashSet<string> Keywords { get; }
4330

4431
public override string ToString()
4532
{
@@ -621,7 +608,7 @@ private bool StartsWithKeyword(string newContent)
621608
{
622609
using (var reader = new StringReader(newContent))
623610
{
624-
return _keywords.Contains(reader.ReadWhile(ParserHelpers.IsIdentifierPart));
611+
return Keywords.Contains(reader.ReadWhile(ParserHelpers.IsIdentifierPart));
625612
}
626613
}
627614
}

src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Microsoft.AspNetCore.Razor.Language.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="$(SharedSourceRoot)\Microsoft.AspNetCore.Razor.LanguageSupport\Microsoft.AspNetCore.Razor.LanguageSupport.csproj" />
13+
<PackageReference Include="System.Collections.Immutable" />
1314
</ItemGroup>
1415

1516
</Project>

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Legacy/CSharpCodeParserTest.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,12 @@ public void MapDirectives_HandlesDuplicates()
220220
var source = TestRazorSourceDocument.Create();
221221
var options = RazorParserOptions.CreateDefault();
222222
var context = new ParserContext(source, options);
223-
var parser = new CSharpCodeParser(context);
224223

225224
// Act & Assert (Does not throw)
226-
parser.MapDirectives((b, t) => { }, "test");
227-
parser.MapDirectives((b, t) => { }, "test");
225+
var directiveDescriptors = new[] {
226+
DirectiveDescriptor.CreateDirective("test", DirectiveKind.SingleLine),
227+
DirectiveDescriptor.CreateDirective("test", DirectiveKind.SingleLine),
228+
};
229+
var parser = new CSharpCodeParser(directiveDescriptors, context);
228230
}
229231
}

0 commit comments

Comments
 (0)