diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
index 3d1fbb234a..9bef04b5bc 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorDiagnosticFactory.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@@ -909,5 +909,21 @@ namespace Microsoft.AspNetCore.Razor.Language
}
#endregion
+
+ #region "CSS Rewriter Errors"
+
+ // CSS Rewriter Errors ID Offset = 5000
+
+ internal static readonly RazorDiagnosticDescriptor CssRewriting_ImportNotAllowed =
+ new RazorDiagnosticDescriptor(
+ $"{DiagnosticPrefix}5000",
+ () => Resources.CssRewriter_ImportNotAllowed,
+ RazorDiagnosticSeverity.Error);
+ public static RazorDiagnostic CreateCssRewriting_ImportNotAllowed(SourceSpan location)
+ {
+ return RazorDiagnostic.Create(CssRewriting_ImportNotAllowed, location);
+ }
+
+ #endregion
}
}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
index 65fdf317bb..a6fe1ab235 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Resources.resx
@@ -562,4 +562,7 @@
The '{0}' directive expects a boolean literal.
+
+ @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.
+
\ No newline at end of file
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs
index f9e33cae14..3babba81e0 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs
@@ -2,12 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.CodeAnalysis.Text;
using Microsoft.Css.Parser.Parser;
using Microsoft.Css.Parser.Tokens;
using Microsoft.Css.Parser.TreeItems;
@@ -57,28 +60,55 @@ namespace Microsoft.AspNetCore.Razor.Tools
protected override Task ExecuteCoreAsync()
{
+ var allDiagnostics = new ConcurrentQueue();
+
Parallel.For(0, Sources.Values.Count, i =>
{
var source = Sources.Values[i];
var output = Outputs.Values[i];
var cssScope = CssScopes.Values[i];
- var inputText = File.ReadAllText(source);
- var rewrittenCss = AddScopeToSelectors(inputText, cssScope);
- File.WriteAllText(output, rewrittenCss);
+ using var inputSourceStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
+ var inputSourceText = SourceText.From(inputSourceStream);
+
+ var rewrittenCss = AddScopeToSelectors(source, inputSourceText, cssScope, out var diagnostics);
+ if (diagnostics.Any())
+ {
+ foreach (var diagnostic in diagnostics)
+ {
+ allDiagnostics.Enqueue(diagnostic);
+ }
+ }
+ else
+ {
+ File.WriteAllText(output, rewrittenCss);
+ }
});
- return Task.FromResult(ExitCodeSuccess);
+ foreach (var diagnostic in allDiagnostics)
+ {
+ Error.WriteLine(diagnostic.ToString());
+ }
+
+ return Task.FromResult(allDiagnostics.Any() ? ExitCodeFailure : ExitCodeSuccess);
}
// Public for tests
- public static string AddScopeToSelectors(string inputText, string cssScope)
+ public static string AddScopeToSelectors(string filePath, string inputSource, string cssScope, out IEnumerable diagnostics)
+ => AddScopeToSelectors(filePath, SourceText.From(inputSource), cssScope, out diagnostics);
+
+ private static string AddScopeToSelectors(string filePath, SourceText inputSourceText, string cssScope, out IEnumerable diagnostics)
{
var cssParser = new DefaultParserFactory().CreateParser();
+ var inputText = inputSourceText.ToString();
var stylesheet = cssParser.Parse(inputText, insertComments: false);
var resultBuilder = new StringBuilder();
var previousInsertionPosition = 0;
+ var foundDiagnostics = new List();
+
+ var ensureNoImportsVisitor = new EnsureNoImports(filePath, inputSourceText, stylesheet, foundDiagnostics);
+ ensureNoImportsVisitor.Visit();
var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet);
scopeInsertionPositionsVisitor.Visit();
@@ -105,6 +135,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
resultBuilder.Append(inputText.Substring(previousInsertionPosition));
+ diagnostics = foundDiagnostics;
return resultBuilder.ToString();
}
@@ -257,6 +288,36 @@ namespace Microsoft.AspNetCore.Razor.Tools
}
}
+ private class EnsureNoImports : Visitor
+ {
+ private readonly string _filePath;
+ private readonly SourceText _sourceText;
+ private readonly List _diagnostics;
+
+ public EnsureNoImports(string filePath, SourceText sourceText, ComplexItem root, List diagnostics) : base(root)
+ {
+ _filePath = filePath;
+ _sourceText = sourceText;
+ _diagnostics = diagnostics;
+ }
+
+ protected override void VisitAtDirective(AtDirective item)
+ {
+ if (item.Children.Count >= 2
+ && item.Children[0] is TokenItem firstChild
+ && firstChild.TokenType == CssTokenType.At
+ && item.Children[1] is TokenItem secondChild
+ && string.Equals(secondChild.Text, "import", StringComparison.OrdinalIgnoreCase))
+ {
+ var linePosition = _sourceText.Lines.GetLinePosition(item.Start);
+ var sourceSpan = new SourceSpan(_filePath, item.Start, linePosition.Line, linePosition.Character, item.Length);
+ _diagnostics.Add(RazorDiagnosticFactory.CreateCssRewriting_ImportNotAllowed(sourceSpan));
+ }
+
+ base.VisitAtDirective(item);
+ }
+ }
+
private class Visitor
{
private readonly ComplexItem _root;
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs
index 2c2d199f80..37b1c37a30 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using Microsoft.AspNetCore.Razor.Language;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Tools
@@ -11,9 +12,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesEmptyFile()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(string.Empty, "TestScope");
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", string.Empty, "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(string.Empty, result);
}
@@ -21,11 +23,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void AddsScopeAfterSelector()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass { color: red; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.myclass[TestScope] { color: red; }
", result);
@@ -35,12 +38,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesMultipleSelectors()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first, .second { color: red; }
.third { color: blue; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.first[TestScope], .second[TestScope] { color: red; }
.third[TestScope] { color: blue; }
@@ -51,11 +55,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesComplexSelectors()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; }
", result);
@@ -65,11 +70,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesSpacesAndCommentsWithinSelectors()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first /* space at end {} */ div , .myclass /* comment at end */ { color: red; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; }
", result);
@@ -79,12 +85,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinator()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first ::deep .second { color: red; }
a ::deep b, c ::deep d { color: blue; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.first[TestScope] .second { color: red; }
a[TestScope] b, c[TestScope] d { color: blue; }
@@ -95,12 +102,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithDirectDescendant()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a > ::deep b { color: red; }
c ::deep > d { color: blue; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
a[TestScope] > b { color: red; }
c[TestScope] > d { color: blue; }
@@ -111,12 +119,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithAdjacentSibling()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a + ::deep b { color: red; }
c ::deep + d { color: blue; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
a[TestScope] + b { color: red; }
c[TestScope] + d { color: blue; }
@@ -127,12 +136,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithGeneralSibling()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a ~ ::deep b { color: red; }
c ::deep ~ d { color: blue; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
a[TestScope] ~ b { color: red; }
c[TestScope] ~ d { color: blue; }
@@ -143,11 +153,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void IgnoresMultipleDeepCombinators()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first ::deep .second ::deep .third { color:red; }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.first[TestScope] .second ::deep .third { color:red; }
", result);
@@ -157,13 +168,14 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithSpacesAndComments()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.a .b /* comment ::deep 1 */ ::deep /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
::deep * { color: blue; } /* Leading deep combinator */
another ::deep { color: green } /* Trailing deep combinator */
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
[TestScope] * { color: blue; } /* Leading deep combinator */
@@ -175,7 +187,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesAtBlocks()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass { color: red; }
@media only screen and (max-width: 600px) {
@@ -183,9 +195,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
content: 'This should not be a selector: .fake-selector { color: red }'
}
}
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.myclass[TestScope] { color: red; }
@@ -201,11 +214,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void AddsScopeToKeyframeNames()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
@keyframes my-animation { /* whatever */ }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
@keyframes my-animation-TestScope { /* whatever */ }
", result);
@@ -215,7 +229,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RewritesAnimationNamesWhenMatchingKnownKeyframes()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass {
color: red;
animation: /* ignore comment */ my-animation 1s infinite;
@@ -228,9 +242,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
@keyframes my-animation { /* whatever */ }
@keyframes different-animation { /* whatever */ }
@keyframes unused-animation { /* whatever */ }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.myclass[TestScope] {
color: red;
@@ -251,14 +266,15 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RewritesMultipleAnimationNames()
{
// Arrange/act
- var result = RewriteCssCommand.AddScopeToSelectors(@"
+ var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass1 { animation-name: my-animation , different-animation }
.myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s }
@keyframes my-animation { }
@keyframes different-animation { }
-", "TestScope");
+", "TestScope", out var diagnostics);
// Assert
+ Assert.Empty(diagnostics);
Assert.Equal(@"
.myclass1[TestScope] { animation-name: my-animation-TestScope , different-animation-TestScope }
.myclass2[TestScope] { animation: 4s linear 0s alternate my-animation-TestScope infinite, different-animation-TestScope 0s }
@@ -266,5 +282,27 @@ namespace Microsoft.AspNetCore.Razor.Tools
@keyframes different-animation-TestScope { }
", result);
}
+
+ [Fact]
+ public void RejectsImportStatements()
+ {
+ // Arrange/act
+ RewriteCssCommand.AddScopeToSelectors("file.css", @"
+ @import ""basic-import.css"";
+ @import ""import-with-media-type.css"" print;
+ @import ""import-with-media-query.css"" screen and (orientation:landscape);
+ @ImPoRt /* comment */ ""scheme://path/to/complex-import"" /* another-comment */ screen;
+ @otheratrule ""should-not-cause-error.css"";
+ /* @import ""should-be-ignored-because-it-is-in-a-comment.css""; */
+ .myclass { color: red; }
+", "TestScope", out var diagnostics);
+
+ // Assert
+ Assert.Collection(diagnostics,
+ diagnostic => Assert.Equal("file.css(2,5): Error RZ5000: @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.", diagnostic.ToString()),
+ diagnostic => Assert.Equal("file.css(3,5): Error RZ5000: @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.", diagnostic.ToString()),
+ diagnostic => Assert.Equal("file.css(4,5): Error RZ5000: @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.", diagnostic.ToString()),
+ diagnostic => Assert.Equal("file.css(5,5): Error RZ5000: @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.", diagnostic.ToString()));
+ }
}
}