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
This commit is contained in:
Steve Sanderson 2020-08-25 17:28:14 +01:00 committed by GitHub
parent 64c47b733f
commit a9b596e091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 153 additions and 35 deletions

View File

@ -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
}
}

View File

@ -562,4 +562,7 @@
<data name="DirectiveExpectsBooleanLiteral" xml:space="preserve">
<value>The '{0}' directive expects a boolean literal.</value>
</data>
<data name="CssRewriter_ImportNotAllowed" xml:space="preserve">
<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>
</data>
</root>

View File

@ -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<int> ExecuteCoreAsync()
{
var allDiagnostics = new ConcurrentQueue<RazorDiagnostic>();
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<RazorDiagnostic> diagnostics)
=> AddScopeToSelectors(filePath, SourceText.From(inputSource), cssScope, out diagnostics);
private static string AddScopeToSelectors(string filePath, SourceText inputSourceText, string cssScope, out IEnumerable<RazorDiagnostic> 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<RazorDiagnostic>();
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<RazorDiagnostic> _diagnostics;
public EnsureNoImports(string filePath, SourceText sourceText, ComplexItem root, List<RazorDiagnostic> 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;

View File

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