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:
parent
64c47b733f
commit
a9b596e091
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue