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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -909,5 +909,21 @@ namespace Microsoft.AspNetCore.Razor.Language
} }
#endregion #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"> <data name="DirectiveExpectsBooleanLiteral" xml:space="preserve">
<value>The '{0}' directive expects a boolean literal.</value> <value>The '{0}' directive expects a boolean literal.</value>
</data> </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> </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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Css.Parser.Parser; using Microsoft.Css.Parser.Parser;
using Microsoft.Css.Parser.Tokens; using Microsoft.Css.Parser.Tokens;
using Microsoft.Css.Parser.TreeItems; using Microsoft.Css.Parser.TreeItems;
@ -57,28 +60,55 @@ namespace Microsoft.AspNetCore.Razor.Tools
protected override Task<int> ExecuteCoreAsync() protected override Task<int> ExecuteCoreAsync()
{ {
var allDiagnostics = new ConcurrentQueue<RazorDiagnostic>();
Parallel.For(0, Sources.Values.Count, i => Parallel.For(0, Sources.Values.Count, i =>
{ {
var source = Sources.Values[i]; var source = Sources.Values[i];
var output = Outputs.Values[i]; var output = Outputs.Values[i];
var cssScope = CssScopes.Values[i]; var cssScope = CssScopes.Values[i];
var inputText = File.ReadAllText(source); using var inputSourceStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var rewrittenCss = AddScopeToSelectors(inputText, cssScope); var inputSourceText = SourceText.From(inputSourceStream);
File.WriteAllText(output, rewrittenCss);
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 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 cssParser = new DefaultParserFactory().CreateParser();
var inputText = inputSourceText.ToString();
var stylesheet = cssParser.Parse(inputText, insertComments: false); var stylesheet = cssParser.Parse(inputText, insertComments: false);
var resultBuilder = new StringBuilder(); var resultBuilder = new StringBuilder();
var previousInsertionPosition = 0; var previousInsertionPosition = 0;
var foundDiagnostics = new List<RazorDiagnostic>();
var ensureNoImportsVisitor = new EnsureNoImports(filePath, inputSourceText, stylesheet, foundDiagnostics);
ensureNoImportsVisitor.Visit();
var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet); var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet);
scopeInsertionPositionsVisitor.Visit(); scopeInsertionPositionsVisitor.Visit();
@ -105,6 +135,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
resultBuilder.Append(inputText.Substring(previousInsertionPosition)); resultBuilder.Append(inputText.Substring(previousInsertionPosition));
diagnostics = foundDiagnostics;
return resultBuilder.ToString(); 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 class Visitor
{ {
private readonly ComplexItem _root; private readonly ComplexItem _root;

View File

@ -1,6 +1,7 @@
// 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. // 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; using Xunit;
namespace Microsoft.AspNetCore.Razor.Tools namespace Microsoft.AspNetCore.Razor.Tools
@ -11,9 +12,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesEmptyFile() public void HandlesEmptyFile()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(string.Empty, "TestScope"); var result = RewriteCssCommand.AddScopeToSelectors("file.css", string.Empty, "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(string.Empty, result); Assert.Equal(string.Empty, result);
} }
@ -21,11 +23,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void AddsScopeAfterSelector() public void AddsScopeAfterSelector()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass { color: red; } .myclass { color: red; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.myclass[TestScope] { color: red; } .myclass[TestScope] { color: red; }
", result); ", result);
@ -35,12 +38,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesMultipleSelectors() public void HandlesMultipleSelectors()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first, .second { color: red; } .first, .second { color: red; }
.third { color: blue; } .third { color: blue; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.first[TestScope], .second[TestScope] { color: red; } .first[TestScope], .second[TestScope] { color: red; }
.third[TestScope] { color: blue; } .third[TestScope] { color: blue; }
@ -51,11 +55,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesComplexSelectors() public void HandlesComplexSelectors()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; } .first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; } .first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; }
", result); ", result);
@ -65,11 +70,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesSpacesAndCommentsWithinSelectors() public void HandlesSpacesAndCommentsWithinSelectors()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first /* space at end {} */ div , .myclass /* comment at end */ { color: red; } .first /* space at end {} */ div , .myclass /* comment at end */ { color: red; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; } .first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; }
", result); ", result);
@ -79,12 +85,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinator() public void RespectsDeepCombinator()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first ::deep .second { color: red; } .first ::deep .second { color: red; }
a ::deep b, c ::deep d { color: blue; } a ::deep b, c ::deep d { color: blue; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.first[TestScope] .second { color: red; } .first[TestScope] .second { color: red; }
a[TestScope] b, c[TestScope] d { color: blue; } a[TestScope] b, c[TestScope] d { color: blue; }
@ -95,12 +102,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithDirectDescendant() public void RespectsDeepCombinatorWithDirectDescendant()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a > ::deep b { color: red; } a > ::deep b { color: red; }
c ::deep > d { color: blue; } c ::deep > d { color: blue; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
a[TestScope] > b { color: red; } a[TestScope] > b { color: red; }
c[TestScope] > d { color: blue; } c[TestScope] > d { color: blue; }
@ -111,12 +119,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithAdjacentSibling() public void RespectsDeepCombinatorWithAdjacentSibling()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a + ::deep b { color: red; } a + ::deep b { color: red; }
c ::deep + d { color: blue; } c ::deep + d { color: blue; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
a[TestScope] + b { color: red; } a[TestScope] + b { color: red; }
c[TestScope] + d { color: blue; } c[TestScope] + d { color: blue; }
@ -127,12 +136,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithGeneralSibling() public void RespectsDeepCombinatorWithGeneralSibling()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
a ~ ::deep b { color: red; } a ~ ::deep b { color: red; }
c ::deep ~ d { color: blue; } c ::deep ~ d { color: blue; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
a[TestScope] ~ b { color: red; } a[TestScope] ~ b { color: red; }
c[TestScope] ~ d { color: blue; } c[TestScope] ~ d { color: blue; }
@ -143,11 +153,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void IgnoresMultipleDeepCombinators() public void IgnoresMultipleDeepCombinators()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.first ::deep .second ::deep .third { color:red; } .first ::deep .second ::deep .third { color:red; }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.first[TestScope] .second ::deep .third { color:red; } .first[TestScope] .second ::deep .third { color:red; }
", result); ", result);
@ -157,13 +168,14 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RespectsDeepCombinatorWithSpacesAndComments() public void RespectsDeepCombinatorWithSpacesAndComments()
{ {
// Arrange/act // 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; } .a .b /* comment ::deep 1 */ ::deep /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
::deep * { color: blue; } /* Leading deep combinator */ ::deep * { color: blue; } /* Leading deep combinator */
another ::deep { color: green } /* Trailing deep combinator */ another ::deep { color: green } /* Trailing deep combinator */
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; } .a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
[TestScope] * { color: blue; } /* Leading deep combinator */ [TestScope] * { color: blue; } /* Leading deep combinator */
@ -175,7 +187,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void HandlesAtBlocks() public void HandlesAtBlocks()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass { color: red; } .myclass { color: red; }
@media only screen and (max-width: 600px) { @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 }' content: 'This should not be a selector: .fake-selector { color: red }'
} }
} }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.myclass[TestScope] { color: red; } .myclass[TestScope] { color: red; }
@ -201,11 +214,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void AddsScopeToKeyframeNames() public void AddsScopeToKeyframeNames()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
@keyframes my-animation { /* whatever */ } @keyframes my-animation { /* whatever */ }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
@keyframes my-animation-TestScope { /* whatever */ } @keyframes my-animation-TestScope { /* whatever */ }
", result); ", result);
@ -215,7 +229,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RewritesAnimationNamesWhenMatchingKnownKeyframes() public void RewritesAnimationNamesWhenMatchingKnownKeyframes()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass { .myclass {
color: red; color: red;
animation: /* ignore comment */ my-animation 1s infinite; animation: /* ignore comment */ my-animation 1s infinite;
@ -228,9 +242,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
@keyframes my-animation { /* whatever */ } @keyframes my-animation { /* whatever */ }
@keyframes different-animation { /* whatever */ } @keyframes different-animation { /* whatever */ }
@keyframes unused-animation { /* whatever */ } @keyframes unused-animation { /* whatever */ }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.myclass[TestScope] { .myclass[TestScope] {
color: red; color: red;
@ -251,14 +266,15 @@ namespace Microsoft.AspNetCore.Razor.Tools
public void RewritesMultipleAnimationNames() public void RewritesMultipleAnimationNames()
{ {
// Arrange/act // Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@" var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
.myclass1 { animation-name: my-animation , different-animation } .myclass1 { animation-name: my-animation , different-animation }
.myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s } .myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s }
@keyframes my-animation { } @keyframes my-animation { }
@keyframes different-animation { } @keyframes different-animation { }
", "TestScope"); ", "TestScope", out var diagnostics);
// Assert // Assert
Assert.Empty(diagnostics);
Assert.Equal(@" Assert.Equal(@"
.myclass1[TestScope] { animation-name: my-animation-TestScope , different-animation-TestScope } .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 } .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 { } @keyframes different-animation-TestScope { }
", result); ", 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()));
}
} }
} }