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())); + } } }