Skip to content

Commit 4e5b86d

Browse files
authored
Ignore missing classname error from C# in style block (#12060)
Fixes #12052
2 parents 2c9fd5d + da23823 commit 4e5b86d

File tree

3 files changed

+214
-17
lines changed

3 files changed

+214
-17
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/CSSErrorCodes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
77
internal static class CSSErrorCodes
88
{
99
public const string UnrecognizedBlockType = "CSS002";
10+
public const string MissingClassNameAfterDot = "CSS008";
1011
public const string MissingOpeningBrace = "CSS023";
12+
public const string MissingPropertyValue = "CSS025";
1113
public const string MissingSelectorAfterCombinator = "CSS029";
1214
public const string MissingSelectorBeforeCombinatorCode = "CSS031";
1315
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorTranslateDiagnosticsService.cs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
2222

2323
using RazorDiagnosticFactory = AspNetCore.Razor.Language.RazorDiagnosticFactory;
24-
using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode;
2524
using RazorSyntaxNodeOrToken = AspNetCore.Razor.Language.Syntax.SyntaxNodeOrToken;
25+
using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode;
2626

2727
/// <summary>
2828
/// Contains several methods for mapping and filtering Razor and C# diagnostics. It allows for
@@ -240,9 +240,11 @@ private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(LspDiagnostic dia
240240
return str switch
241241
{
242242
CSSErrorCodes.UnrecognizedBlockType => IsEscapedAtSign(diagnostic, sourceText),
243-
CSSErrorCodes.MissingOpeningBrace => IsCSharpInStyleBlock(diagnostic, sourceText, syntaxTree),
244-
CSSErrorCodes.MissingSelectorAfterCombinator => IsCSharpInStyleBlock(diagnostic, sourceText, syntaxTree),
245-
CSSErrorCodes.MissingSelectorBeforeCombinatorCode => IsCSharpInStyleBlock(diagnostic, sourceText, syntaxTree),
243+
CSSErrorCodes.MissingOpeningBrace or
244+
CSSErrorCodes.MissingClassNameAfterDot or
245+
CSSErrorCodes.MissingSelectorAfterCombinator or
246+
CSSErrorCodes.MissingPropertyValue or
247+
CSSErrorCodes.MissingSelectorBeforeCombinatorCode => IsAtCSharpTransitionInStyleBlock(diagnostic, sourceText, syntaxTree),
246248
HtmlErrorCodes.UnexpectedEndTagErrorCode => IsHtmlWithBangAndMatchingTags(diagnostic, sourceText, syntaxTree),
247249
HtmlErrorCodes.InvalidNestingErrorCode => IsAnyFilteredInvalidNestingError(diagnostic, sourceText, syntaxTree),
248250
HtmlErrorCodes.MissingEndTagErrorCode => syntaxTree.Options.FileKind.IsComponent(), // Redundant with RZ9980 in Components
@@ -278,20 +280,32 @@ static bool IsEscapedAtSign(LspDiagnostic diagnostic, SourceText sourceText)
278280
sourceText[absoluteIndex - 1] == '@';
279281
}
280282

281-
static bool IsCSharpInStyleBlock(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
283+
static bool IsAtCSharpTransitionInStyleBlock(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree)
282284
{
283-
// C# in a style block causes diagnostics because the HTML background document replaces C# with "~"
284-
var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start);
285-
if (owner is null)
285+
if (!sourceText.TryGetAbsoluteIndex(diagnostic.Range.Start, out var absoluteIndex))
286286
{
287287
return false;
288288
}
289289

290-
var element = owner.FirstAncestorOrSelf<MarkupElementSyntax>(static n => n.StartTag?.Name.Content == "style");
291-
var csharp = owner.FirstAncestorOrSelf<CSharpCodeBlockSyntax>();
290+
// Skip past non-newline whitespace to find the first interesting node
291+
while (sourceText[absoluteIndex] is ' ' or '\t')
292+
{
293+
absoluteIndex++;
294+
if (absoluteIndex == sourceText.Length)
295+
{
296+
return false;
297+
}
298+
}
299+
300+
var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex);
301+
302+
// If we're not at an @ to transition to C#, then we don't want to filter this diagnostic
303+
if (owner is not CSharpTransitionSyntax)
304+
{
305+
return false;
306+
}
292307

293-
return csharp is not null ||
294-
(element?.Body.Any(static c => c is CSharpCodeBlockSyntax) ?? false);
308+
return owner.FirstAncestorOrSelf<MarkupElementSyntax>(static n => n.StartTag?.Name.Content == "style") is not null;
295309
}
296310

297311
// Ideally this would be solved instead by not emitting the "!" at the HTML backing file,

src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs

Lines changed: 186 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public Task Html()
5959
{
6060
Diagnostics =
6161
[
62-
new Diagnostic
62+
new LspDiagnostic
6363
{
6464
Code = "HTM1337",
6565
Range = SourceText.From(input.Text).GetRange(input.NamedSpans.First().Value.First())
@@ -95,12 +95,12 @@ public Task FilterEscapedAtFromCss()
9595
{
9696
Diagnostics =
9797
[
98-
new Diagnostic
98+
new LspDiagnostic
9999
{
100100
Code = CSSErrorCodes.UnrecognizedBlockType,
101101
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("@@") + 1, 1))
102102
},
103-
new Diagnostic
103+
new LspDiagnostic
104104
{
105105
Code = CSSErrorCodes.UnrecognizedBlockType,
106106
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("f"), 1))
@@ -109,6 +109,44 @@ public Task FilterEscapedAtFromCss()
109109
}]);
110110
}
111111

112+
[Fact]
113+
public Task FilterCSharpFromCss()
114+
{
115+
TestCode input = """
116+
<div>
117+
118+
<style>
119+
@{ insertSomeBigBlobOfCSharp(); }
120+
121+
{|CSS031:~|}~~~~
122+
</style>
123+
124+
</div>
125+
126+
@code {
127+
string insertSomeBigBlobOfCSharp() => "body { font-weight: bold; }";
128+
}
129+
""";
130+
131+
return VerifyDiagnosticsAsync(input,
132+
htmlResponse: [new VSInternalDiagnosticReport
133+
{
134+
Diagnostics =
135+
[
136+
new LspDiagnostic
137+
{
138+
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
139+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("@{"), 1))
140+
},
141+
new LspDiagnostic
142+
{
143+
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
144+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("~~"), 1))
145+
}
146+
]
147+
}]);
148+
}
149+
112150
[Fact]
113151
public Task FilterRazorCommentsFromCss()
114152
{
@@ -117,6 +155,8 @@ public Task FilterRazorCommentsFromCss()
117155
118156
<style>
119157
@* This is a Razor comment *@
158+
159+
{|CSS031:~|}~~~~
120160
</style>
121161
122162
</div>
@@ -127,10 +167,15 @@ public Task FilterRazorCommentsFromCss()
127167
{
128168
Diagnostics =
129169
[
130-
new Diagnostic
170+
new LspDiagnostic
131171
{
132172
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
133173
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("@*"), 1))
174+
},
175+
new LspDiagnostic
176+
{
177+
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
178+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("~~"), 1))
134179
}
135180
]
136181
}]);
@@ -144,6 +189,8 @@ public Task FilterRazorCommentsFromCss_Inside()
144189
145190
<style>
146191
@* This is a Razor comment *@
192+
193+
{|CSS031:~|}~~~~
147194
</style>
148195
149196
</div>
@@ -154,15 +201,149 @@ public Task FilterRazorCommentsFromCss_Inside()
154201
{
155202
Diagnostics =
156203
[
157-
new Diagnostic
204+
new LspDiagnostic
158205
{
159206
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
160207
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("Ra"), 1))
208+
},
209+
new LspDiagnostic
210+
{
211+
Code = CSSErrorCodes.MissingSelectorBeforeCombinatorCode,
212+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf("~~"), 1))
161213
}
162214
]
163215
}]);
164216
}
165217

218+
[Fact]
219+
public Task FilterMissingClassNameInCss()
220+
{
221+
TestCode input = """
222+
<div>
223+
224+
<style>
225+
.@(className)
226+
background-color: lightblue;
227+
}
228+
229+
.{|CSS008:{|}
230+
bar: baz;
231+
}
232+
</style>
233+
234+
</div>
235+
236+
@code
237+
{
238+
private string className = "foo";
239+
}
240+
""";
241+
242+
return VerifyDiagnosticsAsync(input,
243+
htmlResponse: [new VSInternalDiagnosticReport
244+
{
245+
Diagnostics =
246+
[
247+
new LspDiagnostic
248+
{
249+
Code = CSSErrorCodes.MissingClassNameAfterDot,
250+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(".@") + 1, 1))
251+
},
252+
new LspDiagnostic
253+
{
254+
Code = CSSErrorCodes.MissingClassNameAfterDot,
255+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(".{") + 1, 1))
256+
},
257+
]
258+
}]);
259+
}
260+
261+
[Fact]
262+
public Task FilterMissingClassNameInCss_WithSpace()
263+
{
264+
TestCode input = """
265+
<div>
266+
267+
<style>
268+
. @(className)
269+
background-color: lightblue;
270+
}
271+
272+
.{|CSS008: |}{
273+
bar: baz;
274+
}
275+
</style>
276+
277+
</div>
278+
279+
@code
280+
{
281+
private string className = "foo";
282+
}
283+
""";
284+
285+
return VerifyDiagnosticsAsync(input,
286+
htmlResponse: [new VSInternalDiagnosticReport
287+
{
288+
Diagnostics =
289+
[
290+
new LspDiagnostic
291+
{
292+
Code = CSSErrorCodes.MissingClassNameAfterDot,
293+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(". @") + 1, 1))
294+
},
295+
new LspDiagnostic
296+
{
297+
Code = CSSErrorCodes.MissingClassNameAfterDot,
298+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(". {") + 1, 1))
299+
},
300+
]
301+
}]);
302+
}
303+
304+
[Fact]
305+
public Task FilterPropertyValueInCss()
306+
{
307+
TestCode input = """
308+
<div>
309+
310+
<style>
311+
.goo {
312+
background-color: @(color);
313+
}
314+
315+
.foo {
316+
background-color:{|CSS025: |}/* no value here */;
317+
}
318+
</style>
319+
320+
</div>
321+
322+
@code
323+
{
324+
private string color = "red";
325+
}
326+
""";
327+
328+
return VerifyDiagnosticsAsync(input,
329+
htmlResponse: [new VSInternalDiagnosticReport
330+
{
331+
Diagnostics =
332+
[
333+
new LspDiagnostic
334+
{
335+
Code = CSSErrorCodes.MissingPropertyValue,
336+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(": @") + 1, 1))
337+
},
338+
new LspDiagnostic
339+
{
340+
Code = CSSErrorCodes.MissingPropertyValue,
341+
Range = SourceText.From(input.Text).GetRange(new TextSpan(input.Text.IndexOf(": /") + 1, 1))
342+
},
343+
]
344+
}]);
345+
}
346+
166347
[Fact]
167348
public Task CombinedAndNestedDiagnostics()
168349
=> VerifyDiagnosticsAsync("""

0 commit comments

Comments
 (0)