From d660bf12ef6e91d275dc978cb8451c69b1879ee1 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 21 May 2021 15:16:20 -0700 Subject: [PATCH 1/2] Add an option to the CLI and Dart Sass to silence warnings from deps Closes #672 --- CHANGELOG.md | 13 +++- lib/sass.dart | 14 ++++ lib/src/async_compile.dart | 9 ++- lib/src/async_import_cache.dart | 11 +-- lib/src/compile.dart | 11 ++- lib/src/executable/compile_stylesheet.dart | 4 ++ lib/src/executable/options.dart | 8 ++- lib/src/import_cache.dart | 12 ++-- lib/src/stylesheet_graph.dart | 8 +-- lib/src/visitor/async_evaluate.dart | 32 ++++++++- lib/src/visitor/evaluate.dart | 33 ++++++++- pubspec.yaml | 2 +- test/cli/shared.dart | 78 ++++++++++++++++++++++ 13 files changed, 212 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 104c83a8f..814bf151c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ -## 1.33.1 +## 1.34.0 * Don't emit the same warning in the same location multiple times. +### Command Line Interface + +* Add a `--quiet-deps` flag which silences compiler warnings from stylesheets + loaded through `--load-path`s. + +### Dart API + +* Add a `quietDeps` argument to `compile()`, `compileString()`, + `compileAsync()`, and `compileStringAsync()` which silences compiler warnings + from stylesheets loaded through importers, load paths, and `package:` URLs. + ## 1.33.0 * Deprecate the use of `/` for division. The new `math.div()` function should be diff --git a/lib/sass.dart b/lib/sass.dart index 4c79dd622..3d56252b7 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -61,6 +61,9 @@ export 'src/warn.dart' show warn; /// /// The [style] parameter controls the style of the resulting CSS. /// +/// If [quietDeps] is `true`, this will silence compiler warnings emitted for +/// stylesheets loaded through [importers], [loadPaths], or [packageConfig]. +/// /// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which /// sections of the source file(s) correspond to which in the resulting CSS. /// It's called immediately before this method returns, and only if compilation @@ -94,6 +97,7 @@ String compile(String path, PackageConfig? packageConfig, Iterable? functions, OutputStyle? style, + bool quietDeps = false, void sourceMap(SingleMapping map)?, bool charset = true}) { logger ??= Logger.stderr(color: color); @@ -106,6 +110,7 @@ String compile(String path, packageConfig: packageConfig), functions: functions, style: style, + quietDeps: quietDeps, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); @@ -150,6 +155,9 @@ String compile(String path, /// [String] or a [Uri]. If [importer] is passed, [url] must be passed as well /// and `importer.load(url)` should return `source`. /// +/// If [quietDeps] is `true`, this will silence compiler warnings emitted for +/// stylesheets loaded through [importers], [loadPaths], or [packageConfig]. +/// /// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which /// sections of the source file(s) correspond to which in the resulting CSS. /// It's called immediately before this method returns, and only if compilation @@ -186,6 +194,7 @@ String compileString(String source, OutputStyle? style, Importer? importer, Object? url, + bool quietDeps = false, void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) { @@ -202,6 +211,7 @@ String compileString(String source, style: style, importer: importer, url: url, + quietDeps: quietDeps, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); @@ -221,6 +231,7 @@ Future compileAsync(String path, Iterable? loadPaths, Iterable? functions, OutputStyle? style, + bool quietDeps = false, void sourceMap(SingleMapping map)?}) async { logger ??= Logger.stderr(color: color); var result = await c.compileAsync(path, @@ -232,6 +243,7 @@ Future compileAsync(String path, packageConfig: packageConfig), functions: functions, style: style, + quietDeps: quietDeps, sourceMap: sourceMap != null); result.sourceMap.andThen(sourceMap); return result.css; @@ -253,6 +265,7 @@ Future compileStringAsync(String source, OutputStyle? style, AsyncImporter? importer, Object? url, + bool quietDeps = false, void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) async { @@ -269,6 +282,7 @@ Future compileStringAsync(String source, style: style, importer: importer, url: url, + quietDeps: quietDeps, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index 2e90fa50b..f8ba229e0 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -34,6 +34,7 @@ Future compileAsync(String path, bool useSpaces = true, int? indentWidth, LineFeed? lineFeed, + bool quietDeps = false, bool sourceMap = false, bool charset = true}) async { // If the syntax is different than the importer would default to, we have to @@ -43,7 +44,8 @@ Future compileAsync(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= AsyncImportCache.none(logger: logger); stylesheet = (await importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path)))!; + FilesystemImporter('.'), p.toUri(canonicalize(path)), + originalUrl: p.toUri(path)))!; } else { stylesheet = Stylesheet.parse( readFile(path), syntax ?? Syntax.forPath(path), @@ -61,6 +63,7 @@ Future compileAsync(String path, useSpaces, indentWidth, lineFeed, + quietDeps, sourceMap, charset); } @@ -83,6 +86,7 @@ Future compileStringAsync(String source, int? indentWidth, LineFeed? lineFeed, Object? url, + bool quietDeps = false, bool sourceMap = false, bool charset = true}) async { var stylesheet = @@ -99,6 +103,7 @@ Future compileStringAsync(String source, useSpaces, indentWidth, lineFeed, + quietDeps, sourceMap, charset); } @@ -117,6 +122,7 @@ Future _compileStylesheet( bool useSpaces, int? indentWidth, LineFeed? lineFeed, + bool quietDeps, bool sourceMap, bool charset) async { var evaluateResult = await evaluateAsync(stylesheet, @@ -125,6 +131,7 @@ Future _compileStylesheet( importer: importer, functions: functions, logger: logger, + quietDeps: quietDeps, sourceMap: sourceMap); var serializeResult = serialize(evaluateResult.stylesheet, diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 2d25a0e2b..70944fc8b 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -162,8 +162,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed. var tuple = await canonicalize(url, baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport); if (tuple == null) return null; - var stylesheet = - await importCanonical(tuple.item1, tuple.item2, tuple.item3); + var stylesheet = await importCanonical(tuple.item1, tuple.item2, + originalUrl: tuple.item3); if (stylesheet == null) return null; return Tuple2(tuple.item1, stylesheet); } @@ -177,9 +177,12 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// into [canonicalUrl]. It's used to resolve a relative canonical URL, which /// importers may return for legacy reasons. /// + /// If [quiet] is `true`, this will disable logging warnings when parsing the + /// newly imported stylesheet. + /// /// Caches the result of the import and uses cached results if possible. Future importCanonical(AsyncImporter importer, Uri canonicalUrl, - [Uri? originalUrl]) async { + {Uri? originalUrl, bool quiet = false}) async { return await putIfAbsentAsync(_importCache, canonicalUrl, () async { var result = await importer.load(canonicalUrl); if (result == null) return null; @@ -191,7 +194,7 @@ Relative canonical URLs are deprecated and will eventually be disallowed. url: originalUrl == null ? canonicalUrl : originalUrl.resolveUri(canonicalUrl), - logger: _logger); + logger: quiet ? Logger.quiet : _logger); }); } diff --git a/lib/src/compile.dart b/lib/src/compile.dart index ed47a446c..51c505c2c 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: dcb7cfbedf1e1189808c0056debf6a68bd387dab +// Checksum: bdf01f7ff8eea0efafa6c7c93920caf26e324f4e // // ignore_for_file: unused_import @@ -44,6 +44,7 @@ CompileResult compile(String path, bool useSpaces = true, int? indentWidth, LineFeed? lineFeed, + bool quietDeps = false, bool sourceMap = false, bool charset = true}) { // If the syntax is different than the importer would default to, we have to @@ -53,7 +54,8 @@ CompileResult compile(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= ImportCache.none(logger: logger); stylesheet = importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path))!; + FilesystemImporter('.'), p.toUri(canonicalize(path)), + originalUrl: p.toUri(path))!; } else { stylesheet = Stylesheet.parse( readFile(path), syntax ?? Syntax.forPath(path), @@ -71,6 +73,7 @@ CompileResult compile(String path, useSpaces, indentWidth, lineFeed, + quietDeps, sourceMap, charset); } @@ -93,6 +96,7 @@ CompileResult compileString(String source, int? indentWidth, LineFeed? lineFeed, Object? url, + bool quietDeps = false, bool sourceMap = false, bool charset = true}) { var stylesheet = @@ -109,6 +113,7 @@ CompileResult compileString(String source, useSpaces, indentWidth, lineFeed, + quietDeps, sourceMap, charset); } @@ -127,6 +132,7 @@ CompileResult _compileStylesheet( bool useSpaces, int? indentWidth, LineFeed? lineFeed, + bool quietDeps, bool sourceMap, bool charset) { var evaluateResult = evaluate(stylesheet, @@ -135,6 +141,7 @@ CompileResult _compileStylesheet( importer: importer, functions: functions, logger: logger, + quietDeps: quietDeps, sourceMap: sourceMap); var serializeResult = serialize(evaluateResult.stylesheet, diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 1714390c2..a1f8cebdc 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -68,6 +68,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importCache: importCache, importer: FilesystemImporter('.'), style: options.style, + quietDeps: options.quietDeps, sourceMap: options.emitSourceMap, charset: options.charset) : await compileAsync(source, @@ -75,6 +76,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, logger: options.logger, importCache: importCache, style: options.style, + quietDeps: options.quietDeps, sourceMap: options.emitSourceMap, charset: options.charset); } else { @@ -85,6 +87,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importCache: graph.importCache, importer: FilesystemImporter('.'), style: options.style, + quietDeps: options.quietDeps, sourceMap: options.emitSourceMap, charset: options.charset) : compile(source, @@ -92,6 +95,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, logger: options.logger, importCache: graph.importCache, style: options.style, + quietDeps: options.quietDeps, sourceMap: options.emitSourceMap, charset: options.charset); } diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 89ccde749..e3b0fed27 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -99,6 +99,9 @@ class ExecutableOptions { ..addFlag('unicode', help: 'Whether to use Unicode characters for messages.') ..addFlag('quiet', abbr: 'q', help: "Don't print warnings.") + ..addFlag('quiet-deps', + help: "Don't print compiler warnings from dependencies.\n" + "Stylesheets imported through load paths count as dependencies.") ..addFlag('trace', help: 'Print full Dart stack traces for exceptions.') ..addFlag('help', abbr: 'h', help: 'Print this usage information.', negatable: false) @@ -163,9 +166,12 @@ class ExecutableOptions { ? _options['unicode'] as bool : !term_glyph.ascii; - /// Whether to silence normal output. + /// Whether to silence all warnings. bool get quiet => _options['quiet'] as bool; + /// Whether to silence warnings in dependencies. + bool get quietDeps => _options['quiet-deps'] as bool; + /// The logger to use to emit messages from Sass. Logger get logger => quiet ? Logger.quiet : Logger.stderr(color: color); diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index ad88e54bc..65bd7b72b 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 950db49eb9e3a85f35bc4a3d7cfe029fb60ae498 +// Checksum: 6821c9a63333c3c99b0c9515aa04e73a14e0f141 // // ignore_for_file: unused_import @@ -161,7 +161,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed. var tuple = canonicalize(url, baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport); if (tuple == null) return null; - var stylesheet = importCanonical(tuple.item1, tuple.item2, tuple.item3); + var stylesheet = + importCanonical(tuple.item1, tuple.item2, originalUrl: tuple.item3); if (stylesheet == null) return null; return Tuple2(tuple.item1, stylesheet); } @@ -175,9 +176,12 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// into [canonicalUrl]. It's used to resolve a relative canonical URL, which /// importers may return for legacy reasons. /// + /// If [quiet] is `true`, this will disable logging warnings when parsing the + /// newly imported stylesheet. + /// /// Caches the result of the import and uses cached results if possible. Stylesheet? importCanonical(Importer importer, Uri canonicalUrl, - [Uri? originalUrl]) { + {Uri? originalUrl, bool quiet = false}) { return _importCache.putIfAbsent(canonicalUrl, () { var result = importer.load(canonicalUrl); if (result == null) return null; @@ -189,7 +193,7 @@ Relative canonical URLs are deprecated and will eventually be disallowed. url: originalUrl == null ? canonicalUrl : originalUrl.resolveUri(canonicalUrl), - logger: _logger); + logger: quiet ? Logger.quiet : _logger); }); } diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 9d9e7ca03..0b19cb76e 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -95,8 +95,8 @@ class StylesheetGraph { var node = _nodes[canonicalUrl]; if (node != null) return const {}; - var stylesheet = _ignoreErrors( - () => importCache.importCanonical(importer, canonicalUrl, originalUrl)); + var stylesheet = _ignoreErrors(() => importCache + .importCanonical(importer, canonicalUrl, originalUrl: originalUrl)); if (stylesheet == null) return const {}; node = StylesheetNode._(stylesheet, importer, canonicalUrl, @@ -278,8 +278,8 @@ class StylesheetGraph { /// error will be produced during compilation. if (active.contains(canonicalUrl)) return null; - var stylesheet = _ignoreErrors( - () => importCache.importCanonical(importer, canonicalUrl, resolvedUrl)); + var stylesheet = _ignoreErrors(() => importCache + .importCanonical(importer, canonicalUrl, originalUrl: resolvedUrl)); if (stylesheet == null) return null; active.add(canonicalUrl); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 063127dfa..9320baa0a 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -76,12 +76,14 @@ Future evaluateAsync(Stylesheet stylesheet, AsyncImporter? importer, Iterable? functions, Logger? logger, + bool quietDeps = false, bool sourceMap = false}) => _EvaluateVisitor( importCache: importCache, nodeImporter: nodeImporter, functions: functions, logger: logger, + quietDeps: quietDeps, sourceMap: sourceMap) .run(importer, stylesheet); @@ -155,6 +157,15 @@ class _EvaluateVisitor /// consoles with redundant warnings. final _warningsEmitted = >{}; + // The importer from which the entrypoint stylesheet was loaded. + late final AsyncImporter? _originalImporter; + + /// Whether to avoid emitting warnings for files loaded from dependencies. + /// + /// A "dependency" in this context is any stylesheet loaded through an + /// importer other than [_originalImporter]. + final bool _quietDeps; + /// Whether to track source map information. final bool _sourceMap; @@ -251,6 +262,12 @@ class _EvaluateVisitor /// stylesheet. AsyncImporter? _importer; + /// Whether we're in a dependency. + /// + /// A dependency is defined as a stylesheet imported by an importer other than + /// the original. In Node importers, nothing is considered a dependency. + bool get _inDependency => !_asNodeSass && _importer != _originalImporter; + /// The stylesheet that's currently being evaluated. Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); set _stylesheet(Stylesheet value) => __stylesheet = value; @@ -296,12 +313,14 @@ class _EvaluateVisitor NodeImporter? nodeImporter, Iterable? functions, Logger? logger, + bool quietDeps = false, bool sourceMap = false}) : _importCache = nodeImporter == null ? importCache ?? AsyncImportCache.none(logger: logger) : null, _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), + _quietDeps = quietDeps, _sourceMap = sourceMap, // The default environment is overridden in [_execute] for full // stylesheets, but for [AsyncEvaluator] this environment is used. @@ -502,6 +521,7 @@ class _EvaluateVisitor } } + _originalImporter = importer; var module = await _execute(importer, node); return EvaluateResult(_combineCss(module), _includedFiles); @@ -1546,11 +1566,18 @@ class _EvaluateVisitor var importCache = _importCache; if (importCache != null) { - var tuple = await importCache.import(Uri.parse(url), + var tuple = await importCache.canonicalize(Uri.parse(url), baseImporter: _importer, baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, forImport: forImport); - if (tuple != null) return tuple; + + if (tuple != null) { + var stylesheet = await importCache.importCanonical( + tuple.item1, tuple.item2, + originalUrl: tuple.item3, + quiet: _quietDeps && tuple.item1 != _originalImporter); + if (stylesheet != null) return Tuple2(tuple.item1, stylesheet); + } } else { var stylesheet = await _importLikeNode(url, forImport); if (stylesheet != null) return Tuple2(null, stylesheet); @@ -3094,6 +3121,7 @@ class _EvaluateVisitor /// Emits a warning with the given [message] about the given [span]. void _warn(String message, FileSpan span, {bool deprecation = false}) { + if (_quietDeps && _inDependency) return; if (!_warningsEmitted.add(Tuple2(message, span))) return; _logger.warn(message, span: span, trace: _stackTrace(span), deprecation: deprecation); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index f50a2cd48..fb53ea99d 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 7eb518e3fd9269a2117e8f4a4b4149ca05e3ec25 +// Checksum: 1d5f3cb78c4567e19f106241ee67c70f4ae01df1 // // ignore_for_file: unused_import @@ -84,12 +84,14 @@ EvaluateResult evaluate(Stylesheet stylesheet, Importer? importer, Iterable? functions, Logger? logger, + bool quietDeps = false, bool sourceMap = false}) => _EvaluateVisitor( importCache: importCache, nodeImporter: nodeImporter, functions: functions, logger: logger, + quietDeps: quietDeps, sourceMap: sourceMap) .run(importer, stylesheet); @@ -163,6 +165,15 @@ class _EvaluateVisitor /// consoles with redundant warnings. final _warningsEmitted = >{}; + // The importer from which the entrypoint stylesheet was loaded. + late final Importer? _originalImporter; + + /// Whether to avoid emitting warnings for files loaded from dependencies. + /// + /// A "dependency" in this context is any stylesheet loaded through an + /// importer other than [_originalImporter]. + final bool _quietDeps; + /// Whether to track source map information. final bool _sourceMap; @@ -259,6 +270,12 @@ class _EvaluateVisitor /// stylesheet. Importer? _importer; + /// Whether we're in a dependency. + /// + /// A dependency is defined as a stylesheet imported by an importer other than + /// the original. In Node importers, nothing is considered a dependency. + bool get _inDependency => !_asNodeSass && _importer != _originalImporter; + /// The stylesheet that's currently being evaluated. Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); set _stylesheet(Stylesheet value) => __stylesheet = value; @@ -304,12 +321,14 @@ class _EvaluateVisitor NodeImporter? nodeImporter, Iterable? functions, Logger? logger, + bool quietDeps = false, bool sourceMap = false}) : _importCache = nodeImporter == null ? importCache ?? ImportCache.none(logger: logger) : null, _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), + _quietDeps = quietDeps, _sourceMap = sourceMap, // The default environment is overridden in [_execute] for full // stylesheets, but for [AsyncEvaluator] this environment is used. @@ -507,6 +526,7 @@ class _EvaluateVisitor } } + _originalImporter = importer; var module = _execute(importer, node); return EvaluateResult(_combineCss(module), _includedFiles); @@ -1544,11 +1564,17 @@ class _EvaluateVisitor var importCache = _importCache; if (importCache != null) { - var tuple = importCache.import(Uri.parse(url), + var tuple = importCache.canonicalize(Uri.parse(url), baseImporter: _importer, baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, forImport: forImport); - if (tuple != null) return tuple; + + if (tuple != null) { + var stylesheet = importCache.importCanonical(tuple.item1, tuple.item2, + originalUrl: tuple.item3, + quiet: _quietDeps && tuple.item1 != _originalImporter); + if (stylesheet != null) return Tuple2(tuple.item1, stylesheet); + } } else { var stylesheet = _importLikeNode(url, forImport); if (stylesheet != null) return Tuple2(null, stylesheet); @@ -3065,6 +3091,7 @@ class _EvaluateVisitor /// Emits a warning with the given [message] about the given [span]. void _warn(String message, FileSpan span, {bool deprecation = false}) { + if (_quietDeps && _inDependency) return; if (!_warningsEmitted.add(Tuple2(message, span))) return; _logger.warn(message, span: span, trace: _stackTrace(span), deprecation: deprecation); diff --git a/pubspec.yaml b/pubspec.yaml index 9e68a06f5..4d3b98afa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.33.1-dev +version: 1.34.0-dev description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/cli/shared.dart b/test/cli/shared.dart index 5ca3a19df..6e6d15ea4 100644 --- a/test/cli/shared.dart +++ b/test/cli/shared.dart @@ -405,6 +405,84 @@ void sharedTests( }); }); + group("with --quiet-deps", () { + group("in a relative load from the entrypoint", () { + test("emits @warn", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.file("_other.scss", "@warn heck").create(); + + var sass = await runSass(["--quiet-deps", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("heck"))); + await sass.shouldExit(0); + }); + + test("emits @debug", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.file("_other.scss", "@debug heck").create(); + + var sass = await runSass(["--quiet-deps", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("heck"))); + await sass.shouldExit(0); + }); + + test("emits parser warnings", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.file("_other.scss", "a {b: c && d}").create(); + + var sass = await runSass(["--quiet-deps", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("&&"))); + await sass.shouldExit(0); + }); + + test("emits runner warnings", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.file("_other.scss", "#{blue} {x: y}").create(); + + var sass = await runSass(["--quiet-deps", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("blue"))); + await sass.shouldExit(0); + }); + }); + + group("in a load path load", () { + test("emits @warn", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.dir("dir", [d.file("_other.scss", "@warn heck")]).create(); + + var sass = await runSass(["--quiet-deps", "-I", "dir", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("heck"))); + await sass.shouldExit(0); + }); + + test("emits @debug", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.dir("dir", [d.file("_other.scss", "@debug heck")]).create(); + + var sass = await runSass(["--quiet-deps", "-I", "dir", "test.scss"]); + expect(sass.stderr, emitsThrough(contains("heck"))); + await sass.shouldExit(0); + }); + + test("doesn't emit parser warnings", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.dir("dir", [d.file("_other.scss", "a {b: c && d}")]).create(); + + var sass = await runSass(["--quiet-deps", "-I", "dir", "test.scss"]); + expect(sass.stderr, emitsDone); + await sass.shouldExit(0); + }); + + test("doesn't emit runner warnings", () async { + await d.file("test.scss", "@use 'other'").create(); + await d.dir("dir", [d.file("_other.scss", "#{blue} {x: y}")]).create(); + + var sass = await runSass(["--quiet-deps", "-I", "dir", "test.scss"]); + expect(sass.stderr, emitsDone); + await sass.shouldExit(0); + }); + }); + }); + group("with --charset", () { test("doesn't emit @charset for a pure-ASCII stylesheet", () async { await d.file("test.scss", "a {b: c}").create(); From 16f181660c0246bfbe30694315646ea2b87853ba Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 21 May 2021 22:40:30 -0700 Subject: [PATCH 2/2] Only omit 5 deprecation warnings per feature by default (#1327) Closes #1323 --- CHANGELOG.md | 9 ++++ lib/sass.dart | 16 ++++++ lib/src/async_compile.dart | 19 ++++++- lib/src/compile.dart | 21 ++++++-- lib/src/executable/compile_stylesheet.dart | 4 ++ lib/src/executable/options.dart | 5 ++ lib/src/logger/terse.dart | 58 ++++++++++++++++++++++ lib/src/parse/scss.dart | 3 +- lib/src/parse/stylesheet.dart | 11 ++-- lib/src/visitor/async_evaluate.dart | 25 ++++++---- lib/src/visitor/evaluate.dart | 27 ++++++---- pubspec.yaml | 2 +- test/cli/shared.dart | 46 +++++++++++++++++ 13 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 lib/src/logger/terse.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 814bf151c..2f3618899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,26 @@ * Don't emit the same warning in the same location multiple times. +* Cap deprecation warnings at 5 per feature by default. + ### Command Line Interface * Add a `--quiet-deps` flag which silences compiler warnings from stylesheets loaded through `--load-path`s. +* Add a `--verbose` flag which causes the compiler to emit all deprecation + warnings, not just 5 per feature. + ### Dart API * Add a `quietDeps` argument to `compile()`, `compileString()`, `compileAsync()`, and `compileStringAsync()` which silences compiler warnings from stylesheets loaded through importers, load paths, and `package:` URLs. +* Add a `verbose` argument to `compile()`, `compileString()`, `compileAsync()`, + and `compileStringAsync()` which causes the compiler to emit all deprecation + warnings, not just 5 per feature. + ## 1.33.0 * Deprecate the use of `/` for division. The new `math.div()` function should be diff --git a/lib/sass.dart b/lib/sass.dart index 3d56252b7..c52b7187a 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -64,6 +64,10 @@ export 'src/warn.dart' show warn; /// If [quietDeps] is `true`, this will silence compiler warnings emitted for /// stylesheets loaded through [importers], [loadPaths], or [packageConfig]. /// +/// By default, once a deprecation warning for a given feature is printed five +/// times, further warnings for that feature are silenced. If [verbose] is true, +/// all deprecation warnings are printed instead. +/// /// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which /// sections of the source file(s) correspond to which in the resulting CSS. /// It's called immediately before this method returns, and only if compilation @@ -98,6 +102,7 @@ String compile(String path, Iterable? functions, OutputStyle? style, bool quietDeps = false, + bool verbose = false, void sourceMap(SingleMapping map)?, bool charset = true}) { logger ??= Logger.stderr(color: color); @@ -111,6 +116,7 @@ String compile(String path, functions: functions, style: style, quietDeps: quietDeps, + verbose: verbose, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); @@ -158,6 +164,10 @@ String compile(String path, /// If [quietDeps] is `true`, this will silence compiler warnings emitted for /// stylesheets loaded through [importers], [loadPaths], or [packageConfig]. /// +/// By default, once a deprecation warning for a given feature is printed five +/// times, further warnings for that feature are silenced. If [verbose] is true, +/// all deprecation warnings are printed instead. +/// /// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which /// sections of the source file(s) correspond to which in the resulting CSS. /// It's called immediately before this method returns, and only if compilation @@ -195,6 +205,7 @@ String compileString(String source, Importer? importer, Object? url, bool quietDeps = false, + bool verbose = false, void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) { @@ -212,6 +223,7 @@ String compileString(String source, importer: importer, url: url, quietDeps: quietDeps, + verbose: verbose, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); @@ -232,6 +244,7 @@ Future compileAsync(String path, Iterable? functions, OutputStyle? style, bool quietDeps = false, + bool verbose = false, void sourceMap(SingleMapping map)?}) async { logger ??= Logger.stderr(color: color); var result = await c.compileAsync(path, @@ -244,6 +257,7 @@ Future compileAsync(String path, functions: functions, style: style, quietDeps: quietDeps, + verbose: verbose, sourceMap: sourceMap != null); result.sourceMap.andThen(sourceMap); return result.css; @@ -266,6 +280,7 @@ Future compileStringAsync(String source, AsyncImporter? importer, Object? url, bool quietDeps = false, + bool verbose = false, void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) async { @@ -283,6 +298,7 @@ Future compileStringAsync(String source, importer: importer, url: url, quietDeps: quietDeps, + verbose: verbose, sourceMap: sourceMap != null, charset: charset); result.sourceMap.andThen(sourceMap); diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index f8ba229e0..0cae4f8e7 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -15,6 +15,7 @@ import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; import 'logger.dart'; +import 'logger/terse.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/async_evaluate.dart'; @@ -35,8 +36,12 @@ Future compileAsync(String path, int? indentWidth, LineFeed? lineFeed, bool quietDeps = false, + bool verbose = false, bool sourceMap = false, bool charset = true}) async { + TerseLogger? terseLogger; + if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. Stylesheet? stylesheet; @@ -52,7 +57,7 @@ Future compileAsync(String path, url: p.toUri(path), logger: logger); } - return await _compileStylesheet( + var result = await _compileStylesheet( stylesheet, logger, importCache, @@ -66,6 +71,9 @@ Future compileAsync(String path, quietDeps, sourceMap, charset); + + terseLogger?.summarize(node: nodeImporter != null); + return result; } /// Like [compileStringAsync] in `lib/sass.dart`, but provides more options to @@ -87,12 +95,16 @@ Future compileStringAsync(String source, LineFeed? lineFeed, Object? url, bool quietDeps = false, + bool verbose = false, bool sourceMap = false, bool charset = true}) async { + TerseLogger? terseLogger; + if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + var stylesheet = Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger); - return _compileStylesheet( + var result = await _compileStylesheet( stylesheet, logger, importCache, @@ -106,6 +118,9 @@ Future compileStringAsync(String source, quietDeps, sourceMap, charset); + + terseLogger?.summarize(node: nodeImporter != null); + return result; } /// Compiles [stylesheet] and returns its result. diff --git a/lib/src/compile.dart b/lib/src/compile.dart index 51c505c2c..adc072807 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: bdf01f7ff8eea0efafa6c7c93920caf26e324f4e +// Checksum: 8e813f2ead6e78899ce820e279983278809a7ea5 // // ignore_for_file: unused_import @@ -25,6 +25,7 @@ import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; import 'logger.dart'; +import 'logger/terse.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/evaluate.dart'; @@ -45,8 +46,12 @@ CompileResult compile(String path, int? indentWidth, LineFeed? lineFeed, bool quietDeps = false, + bool verbose = false, bool sourceMap = false, bool charset = true}) { + TerseLogger? terseLogger; + if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. Stylesheet? stylesheet; @@ -62,7 +67,7 @@ CompileResult compile(String path, url: p.toUri(path), logger: logger); } - return _compileStylesheet( + var result = _compileStylesheet( stylesheet, logger, importCache, @@ -76,6 +81,9 @@ CompileResult compile(String path, quietDeps, sourceMap, charset); + + terseLogger?.summarize(node: nodeImporter != null); + return result; } /// Like [compileString] in `lib/sass.dart`, but provides more options to @@ -97,12 +105,16 @@ CompileResult compileString(String source, LineFeed? lineFeed, Object? url, bool quietDeps = false, + bool verbose = false, bool sourceMap = false, bool charset = true}) { + TerseLogger? terseLogger; + if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + var stylesheet = Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger); - return _compileStylesheet( + var result = _compileStylesheet( stylesheet, logger, importCache, @@ -116,6 +128,9 @@ CompileResult compileString(String source, quietDeps, sourceMap, charset); + + terseLogger?.summarize(node: nodeImporter != null); + return result; } /// Compiles [stylesheet] and returns its result. diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index a1f8cebdc..23a90121a 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -69,6 +69,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importer: FilesystemImporter('.'), style: options.style, quietDeps: options.quietDeps, + verbose: options.verbose, sourceMap: options.emitSourceMap, charset: options.charset) : await compileAsync(source, @@ -77,6 +78,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importCache: importCache, style: options.style, quietDeps: options.quietDeps, + verbose: options.verbose, sourceMap: options.emitSourceMap, charset: options.charset); } else { @@ -88,6 +90,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importer: FilesystemImporter('.'), style: options.style, quietDeps: options.quietDeps, + verbose: options.verbose, sourceMap: options.emitSourceMap, charset: options.charset) : compile(source, @@ -96,6 +99,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, importCache: graph.importCache, style: options.style, quietDeps: options.quietDeps, + verbose: options.verbose, sourceMap: options.emitSourceMap, charset: options.charset); } diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index e3b0fed27..8687c0d7e 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -102,6 +102,8 @@ class ExecutableOptions { ..addFlag('quiet-deps', help: "Don't print compiler warnings from dependencies.\n" "Stylesheets imported through load paths count as dependencies.") + ..addFlag('verbose', + help: "Print all deprecation warnings even when they're repetitive.") ..addFlag('trace', help: 'Print full Dart stack traces for exceptions.') ..addFlag('help', abbr: 'h', help: 'Print this usage information.', negatable: false) @@ -172,6 +174,9 @@ class ExecutableOptions { /// Whether to silence warnings in dependencies. bool get quietDeps => _options['quiet-deps'] as bool; + /// Whether to emit all repetitive deprecation warnings. + bool get verbose => _options['verbose'] as bool; + /// The logger to use to emit messages from Sass. Logger get logger => quiet ? Logger.quiet : Logger.stderr(color: color); diff --git a/lib/src/logger/terse.dart b/lib/src/logger/terse.dart new file mode 100644 index 000000000..83258da15 --- /dev/null +++ b/lib/src/logger/terse.dart @@ -0,0 +1,58 @@ +// Copyright 2018 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; +import 'package:stack_trace/stack_trace.dart'; + +import '../logger.dart'; + +/// The maximum number of repetitions of the same warning [TerseLogger] will +/// emit before hiding the rest. +const _maxRepetitions = 5; + +/// A logger that wraps an inner logger to omit repeated deprecation warnings. +/// +/// A warning is considered "repeated" if the first paragraph is the same as +/// another warning that's already been emitted. +class TerseLogger implements Logger { + /// A map from the first paragraph of a warning to the number of times this + /// logger has emitted a warning with that line. + final _warningCounts = {}; + + final Logger _inner; + + TerseLogger(this._inner); + + void warn(String message, + {FileSpan? span, Trace? trace, bool deprecation = false}) { + if (deprecation) { + var firstParagraph = message.split("\n\n").first; + var count = _warningCounts[firstParagraph] = + (_warningCounts[firstParagraph] ?? 0) + 1; + if (count > _maxRepetitions) return; + } + + _inner.warn(message, span: span, trace: trace, deprecation: deprecation); + } + + void debug(String message, SourceSpan span) => _inner.debug(message, span); + + /// Prints a warning indicating the number of deprecation warnings that were + /// omitted. + /// + /// The [node] flag indicates whether this is running in Node.js mode, in + /// which case it doesn't mention "verbose mode" because the Node API doesn't + /// support that. + void summarize({required bool node}) { + var total = _warningCounts.values + .where((count) => count > _maxRepetitions) + .map((count) => count - _maxRepetitions) + .sum; + if (total > 0) { + _inner.warn("$total repetitive deprecation warnings omitted." + + (node ? "" : "\nRun in verbose mode to see all warnings.")); + } + } +} diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index a03b7e6f5..c961045f6 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -48,7 +48,8 @@ class ScssParser extends StylesheetParser { logger.warn( '@elseif is deprecated and will not be supported in future Sass ' 'versions.\n' - 'Use "@else if" instead.', + '\n' + 'Recommendation: @else if', span: scanner.spanFrom(beforeAt), deprecation: true); scanner.position -= 2; diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 72bff4deb..024598cbe 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -1334,10 +1334,13 @@ abstract class StylesheetParser extends Parser { var value = buffer.interpolation(scanner.spanFrom(valueStart)); return _withChildren(_statement, start, (children, span) { if (needsDeprecationWarning) { - logger.warn(""" -@-moz-document is deprecated and support will be removed from Sass in a future -release. For details, see http://bit.ly/moz-document. -""", span: span, deprecation: true); + logger.warn( + "@-moz-document is deprecated and support will be removed in Dart " + "Sass 2.0.0.\n" + "\n" + "For details, see http://bit.ly/moz-document.", + span: span, + deprecation: true); } return AtRule(name, span, value: value, children: children); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 9320baa0a..d3aad9d84 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -436,8 +436,10 @@ class _EvaluateVisitor if (function is SassString) { warn( - "Passing a string to call() is deprecated and will be illegal\n" - "in Dart Sass 2.0.0. Use call(get-function($function)) instead.", + "Passing a string to call() is deprecated and will be illegal in " + "Dart Sass 2.0.0.\n" + "\n" + "Recommendation: call(get-function($function))", deprecation: true); var callableNode = _callableNode!; @@ -1972,14 +1974,17 @@ class _EvaluateVisitor if (node.isGlobal && !_environment.globalVariableExists(node.name)) { _warn( _environment.atRoot - ? "As of Dart Sass 2.0.0, !global assignments won't be able to\n" - "declare new variables. Since this assignment is at the root " - "of the stylesheet,\n" - "the !global flag is unnecessary and can safely be removed." - : "As of Dart Sass 2.0.0, !global assignments won't be able to\n" - "declare new variables. Consider adding " - "`${node.originalName}: null` at the root of the\n" - "stylesheet.", + ? "As of Dart Sass 2.0.0, !global assignments won't be able to " + "declare new variables.\n" + "\n" + "Since this assignment is at the root of the stylesheet, the " + "!global flag is\n" + "unnecessary and can safely be removed." + : "As of Dart Sass 2.0.0, !global assignments won't be able to " + "declare new variables.\n" + "\n" + "Recommendation: add `${node.originalName}: null` at the " + "stylesheet root.", node.span, deprecation: true); } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index fb53ea99d..f61b28444 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 1d5f3cb78c4567e19f106241ee67c70f4ae01df1 +// Checksum: 24b9012f1cf8908b2cbde11cd10974113d4c8163 // // ignore_for_file: unused_import @@ -443,8 +443,10 @@ class _EvaluateVisitor if (function is SassString) { warn( - "Passing a string to call() is deprecated and will be illegal\n" - "in Dart Sass 2.0.0. Use call(get-function($function)) instead.", + "Passing a string to call() is deprecated and will be illegal in " + "Dart Sass 2.0.0.\n" + "\n" + "Recommendation: call(get-function($function))", deprecation: true); var callableNode = _callableNode!; @@ -1963,14 +1965,17 @@ class _EvaluateVisitor if (node.isGlobal && !_environment.globalVariableExists(node.name)) { _warn( _environment.atRoot - ? "As of Dart Sass 2.0.0, !global assignments won't be able to\n" - "declare new variables. Since this assignment is at the root " - "of the stylesheet,\n" - "the !global flag is unnecessary and can safely be removed." - : "As of Dart Sass 2.0.0, !global assignments won't be able to\n" - "declare new variables. Consider adding " - "`${node.originalName}: null` at the root of the\n" - "stylesheet.", + ? "As of Dart Sass 2.0.0, !global assignments won't be able to " + "declare new variables.\n" + "\n" + "Since this assignment is at the root of the stylesheet, the " + "!global flag is\n" + "unnecessary and can safely be removed." + : "As of Dart Sass 2.0.0, !global assignments won't be able to " + "declare new variables.\n" + "\n" + "Recommendation: add `${node.originalName}: null` at the " + "stylesheet root.", node.span, deprecation: true); } diff --git a/pubspec.yaml b/pubspec.yaml index 4d3b98afa..159a963e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.34.0-dev +version: 1.34.0 description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/cli/shared.dart b/test/cli/shared.dart index 6e6d15ea4..5625eeab5 100644 --- a/test/cli/shared.dart +++ b/test/cli/shared.dart @@ -483,6 +483,52 @@ void sharedTests( }); }); + group("with a bunch of deprecation warnings", () { + setUp(() async { + await d.file("test.scss", r""" + $_: call("inspect", null); + $_: call("rgb", 0, 0, 0); + $_: call("nth", null, 1); + $_: call("join", null, null); + $_: call("if", true, 1, 2); + $_: call("hsl", 0, 100%, 100%); + + $_: 1/2; + $_: 1/3; + $_: 1/4; + $_: 1/5; + $_: 1/6; + $_: 1/7; + """).create(); + }); + + test("without --verbose, only prints five", () async { + var sass = await runSass(["test.scss"]); + expect(sass.stderr, + emitsInOrder(List.filled(5, emitsThrough(contains("call()"))))); + expect(sass.stderr, neverEmits(contains("call()"))); + + expect(sass.stderr, + emitsInOrder(List.filled(5, emitsThrough(contains("math.div"))))); + expect(sass.stderr, neverEmits(contains("math.div()"))); + + expect(sass.stderr, + emitsThrough(contains("2 repetitive deprecation warnings omitted."))); + }); + + test("with --verbose, prints all", () async { + var sass = await runSass(["--verbose", "test.scss"]); + expect(sass.stderr, + neverEmits(contains("2 repetitive deprecation warnings omitted."))); + + expect(sass.stderr, + emitsInOrder(List.filled(6, emitsThrough(contains("call()"))))); + + expect(sass.stderr, + emitsInOrder(List.filled(6, emitsThrough(contains("math.div"))))); + }); + }); + group("with --charset", () { test("doesn't emit @charset for a pure-ASCII stylesheet", () async { await d.file("test.scss", "a {b: c}").create();