Skip to content

Commit a9b596e

Browse files
Prevent @import in scoped CSS (#25196)
* Reject @import rules in scoped CSS files * CR feedback: Use SourceText * CR feedback: Another test case * Use same file reading mechanism as "generate" command
1 parent 64c47b7 commit a9b596e

File tree

4 files changed

+153
-35
lines changed

4 files changed

+153
-35
lines changed

src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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

44
using System;
@@ -909,5 +909,21 @@ public static RazorDiagnostic CreateRewriter_InsufficientStack(SourceSpan locati
909909
}
910910

911911
#endregion
912+
913+
#region "CSS Rewriter Errors"
914+
915+
// CSS Rewriter Errors ID Offset = 5000
916+
917+
internal static readonly RazorDiagnosticDescriptor CssRewriting_ImportNotAllowed =
918+
new RazorDiagnosticDescriptor(
919+
$"{DiagnosticPrefix}5000",
920+
() => Resources.CssRewriter_ImportNotAllowed,
921+
RazorDiagnosticSeverity.Error);
922+
public static RazorDiagnostic CreateCssRewriting_ImportNotAllowed(SourceSpan location)
923+
{
924+
return RazorDiagnostic.Create(CssRewriting_ImportNotAllowed, location);
925+
}
926+
927+
#endregion
912928
}
913929
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,7 @@
562562
<data name="DirectiveExpectsBooleanLiteral" xml:space="preserve">
563563
<value>The '{0}' directive expects a boolean literal.</value>
564564
</data>
565+
<data name="CssRewriter_ImportNotAllowed" xml:space="preserve">
566+
<value>@import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.</value>
567+
</data>
565568
</root>

src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.IO;
78
using System.Linq;
89
using System.Text;
910
using System.Text.RegularExpressions;
1011
using System.Threading.Tasks;
12+
using Microsoft.AspNetCore.Razor.Language;
13+
using Microsoft.CodeAnalysis.Text;
1114
using Microsoft.Css.Parser.Parser;
1215
using Microsoft.Css.Parser.Tokens;
1316
using Microsoft.Css.Parser.TreeItems;
@@ -57,28 +60,55 @@ protected override bool ValidateArguments()
5760

5861
protected override Task<int> ExecuteCoreAsync()
5962
{
63+
var allDiagnostics = new ConcurrentQueue<RazorDiagnostic>();
64+
6065
Parallel.For(0, Sources.Values.Count, i =>
6166
{
6267
var source = Sources.Values[i];
6368
var output = Outputs.Values[i];
6469
var cssScope = CssScopes.Values[i];
6570

66-
var inputText = File.ReadAllText(source);
67-
var rewrittenCss = AddScopeToSelectors(inputText, cssScope);
68-
File.WriteAllText(output, rewrittenCss);
71+
using var inputSourceStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
72+
var inputSourceText = SourceText.From(inputSourceStream);
73+
74+
var rewrittenCss = AddScopeToSelectors(source, inputSourceText, cssScope, out var diagnostics);
75+
if (diagnostics.Any())
76+
{
77+
foreach (var diagnostic in diagnostics)
78+
{
79+
allDiagnostics.Enqueue(diagnostic);
80+
}
81+
}
82+
else
83+
{
84+
File.WriteAllText(output, rewrittenCss);
85+
}
6986
});
7087

71-
return Task.FromResult(ExitCodeSuccess);
88+
foreach (var diagnostic in allDiagnostics)
89+
{
90+
Error.WriteLine(diagnostic.ToString());
91+
}
92+
93+
return Task.FromResult(allDiagnostics.Any() ? ExitCodeFailure : ExitCodeSuccess);
7294
}
7395

7496
// Public for tests
75-
public static string AddScopeToSelectors(string inputText, string cssScope)
97+
public static string AddScopeToSelectors(string filePath, string inputSource, string cssScope, out IEnumerable<RazorDiagnostic> diagnostics)
98+
=> AddScopeToSelectors(filePath, SourceText.From(inputSource), cssScope, out diagnostics);
99+
100+
private static string AddScopeToSelectors(string filePath, SourceText inputSourceText, string cssScope, out IEnumerable<RazorDiagnostic> diagnostics)
76101
{
77102
var cssParser = new DefaultParserFactory().CreateParser();
103+
var inputText = inputSourceText.ToString();
78104
var stylesheet = cssParser.Parse(inputText, insertComments: false);
79105

80106
var resultBuilder = new StringBuilder();
81107
var previousInsertionPosition = 0;
108+
var foundDiagnostics = new List<RazorDiagnostic>();
109+
110+
var ensureNoImportsVisitor = new EnsureNoImports(filePath, inputSourceText, stylesheet, foundDiagnostics);
111+
ensureNoImportsVisitor.Visit();
82112

83113
var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet);
84114
scopeInsertionPositionsVisitor.Visit();
@@ -105,6 +135,7 @@ public static string AddScopeToSelectors(string inputText, string cssScope)
105135

106136
resultBuilder.Append(inputText.Substring(previousInsertionPosition));
107137

138+
diagnostics = foundDiagnostics;
108139
return resultBuilder.ToString();
109140
}
110141

@@ -257,6 +288,36 @@ protected override void VisitAtDirective(AtDirective item)
257288
}
258289
}
259290

291+
private class EnsureNoImports : Visitor
292+
{
293+
private readonly string _filePath;
294+
private readonly SourceText _sourceText;
295+
private readonly List<RazorDiagnostic> _diagnostics;
296+
297+
public EnsureNoImports(string filePath, SourceText sourceText, ComplexItem root, List<RazorDiagnostic> diagnostics) : base(root)
298+
{
299+
_filePath = filePath;
300+
_sourceText = sourceText;
301+
_diagnostics = diagnostics;
302+
}
303+
304+
protected override void VisitAtDirective(AtDirective item)
305+
{
306+
if (item.Children.Count >= 2
307+
&& item.Children[0] is TokenItem firstChild
308+
&& firstChild.TokenType == CssTokenType.At
309+
&& item.Children[1] is TokenItem secondChild
310+
&& string.Equals(secondChild.Text, "import", StringComparison.OrdinalIgnoreCase))
311+
{
312+
var linePosition = _sourceText.Lines.GetLinePosition(item.Start);
313+
var sourceSpan = new SourceSpan(_filePath, item.Start, linePosition.Line, linePosition.Character, item.Length);
314+
_diagnostics.Add(RazorDiagnosticFactory.CreateCssRewriting_ImportNotAllowed(sourceSpan));
315+
}
316+
317+
base.VisitAtDirective(item);
318+
}
319+
}
320+
260321
private class Visitor
261322
{
262323
private readonly ComplexItem _root;

0 commit comments

Comments
 (0)