Skip to content

Commit d2a0cbc

Browse files
authored
Fix null ref in SyntaxTokenCache (#30978)
Fixes #27154
1 parent 95bf141 commit d2a0cbc

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/InternalSyntax/SyntaxFactory.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
64
namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
75
{
86
internal static partial class SyntaxFactory
97
{
108
internal static SyntaxToken Token(SyntaxKind kind, string content, params RazorDiagnostic[] diagnostics)
119
{
12-
if (SyntaxTokenCache.CanBeCached(kind, diagnostics))
10+
if (SyntaxTokenCache.Instance.CanBeCached(kind, diagnostics))
1311
{
14-
return SyntaxTokenCache.GetCachedToken(kind, content);
12+
return SyntaxTokenCache.Instance.GetCachedToken(kind, content);
1513
}
1614

1715
return new SyntaxToken(kind, content, diagnostics);

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/InternalSyntax/SyntaxTokenCache.cs

+12-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
#nullable enable
5+
46
using System;
5-
using Microsoft.Extensions.Internal;
67

78
namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
89
{
910
// Simplified version of Roslyn's SyntaxNodeCache
10-
internal static class SyntaxTokenCache
11+
internal sealed class SyntaxTokenCache
1112
{
1213
private const int CacheSizeBits = 16;
1314
private const int CacheSize = 1 << CacheSizeBits;
1415
private const int CacheMask = CacheSize - 1;
16+
public static readonly SyntaxTokenCache Instance = new();
1517
private static readonly Entry[] s_cache = new Entry[CacheSize];
1618

17-
private struct Entry
19+
internal SyntaxTokenCache() { }
20+
21+
private readonly struct Entry
1822
{
1923
public int Hash { get; }
20-
public SyntaxToken Token { get; }
24+
public SyntaxToken? Token { get; }
2125

2226
internal Entry(int hash, SyntaxToken token)
2327
{
@@ -26,7 +30,7 @@ internal Entry(int hash, SyntaxToken token)
2630
}
2731
}
2832

29-
public static bool CanBeCached(SyntaxKind kind, params RazorDiagnostic[] diagnostics)
33+
public bool CanBeCached(SyntaxKind kind, params RazorDiagnostic[] diagnostics)
3034
{
3135
if (diagnostics.Length == 0)
3236
{
@@ -50,7 +54,7 @@ public static bool CanBeCached(SyntaxKind kind, params RazorDiagnostic[] diagnos
5054
return false;
5155
}
5256

53-
public static SyntaxToken GetCachedToken(SyntaxKind kind, string content)
57+
public SyntaxToken GetCachedToken(SyntaxKind kind, string content)
5458
{
5559
var hash = (kind, content).GetHashCode();
5660

@@ -60,7 +64,7 @@ public static SyntaxToken GetCachedToken(SyntaxKind kind, string content)
6064
var idx = indexableHash & CacheMask;
6165
var e = s_cache[idx];
6266

63-
if (e.Hash == hash && e.Token.Kind == kind && e.Token.Content == content)
67+
if (e.Hash == hash && e.Token != null && e.Token.Kind == kind && e.Token.Content == content)
6468
{
6569
return e.Token;
6670
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Xunit;
5+
6+
namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
7+
{
8+
public class SyntaxTokenCacheTest
9+
{
10+
// Regression test for https://github.com/dotnet/aspnetcore/issues/27154
11+
[Fact]
12+
public void GetCachedToken_ReturnsNewEntry()
13+
{
14+
// Arrange
15+
var cache = new SyntaxTokenCache();
16+
17+
// Act
18+
var token = cache.GetCachedToken(SyntaxKind.Whitespace, "Hello world");
19+
20+
// Assert
21+
Assert.Equal(SyntaxKind.Whitespace, token.Kind);
22+
Assert.Equal("Hello world", token.Content);
23+
Assert.Empty(token.GetDiagnostics());
24+
}
25+
26+
[Fact]
27+
public void GetCachedToken_ReturnsCachedToken()
28+
{
29+
// Arrange
30+
var cache = new SyntaxTokenCache();
31+
32+
// Act
33+
var token1 = cache.GetCachedToken(SyntaxKind.Whitespace, "Hello world");
34+
var token2 = cache.GetCachedToken(SyntaxKind.Whitespace, "Hello world");
35+
36+
// Assert
37+
Assert.Same(token1, token2);
38+
}
39+
40+
[Fact]
41+
public void GetCachedToken_ReturnsDifferentEntries_IfKindsAreDifferent()
42+
{
43+
// Arrange
44+
var cache = new SyntaxTokenCache();
45+
46+
// Act
47+
var token1 = cache.GetCachedToken(SyntaxKind.Whitespace, "Hello world");
48+
var token2 = cache.GetCachedToken(SyntaxKind.Keyword, "Hello world");
49+
50+
// Assert
51+
Assert.NotSame(token1, token2);
52+
Assert.Equal(SyntaxKind.Whitespace, token1.Kind);
53+
Assert.Equal("Hello world", token1.Content);
54+
55+
Assert.Equal(SyntaxKind.Keyword, token2.Kind);
56+
Assert.Equal("Hello world", token2.Content);
57+
}
58+
59+
[Fact]
60+
public void GetCachedToken_ReturnsDifferentEntries_IfContentsAreDifferent()
61+
{
62+
// Arrange
63+
var cache = new SyntaxTokenCache();
64+
65+
// Act
66+
var token1 = cache.GetCachedToken(SyntaxKind.Keyword, "Text1");
67+
var token2 = cache.GetCachedToken(SyntaxKind.Keyword, "Text2");
68+
69+
// Assert
70+
Assert.NotSame(token1, token2);
71+
Assert.Equal(SyntaxKind.Keyword, token1.Kind);
72+
Assert.Equal("Text1", token1.Content);
73+
74+
Assert.Equal(SyntaxKind.Keyword, token2.Kind);
75+
Assert.Equal("Text2", token2.Content);
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)