diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ae69c7c..98eaad010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +## 1.74.0 + +### JS API + +* Add a new top-level `deprecations` object, which contains various + `Deprecation` objects that define the different types of deprecation used by + the Sass compiler and can be passed to the options below. + +* Add a new `fatalDeprecations` compiler option that causes the compiler to + error if any deprecation warnings of the provided types are encountered. You + can also pass in a `Version` object to treat all deprecations that were active + in that Dart Sass version as fatal. + +* Add a new `futureDeprecations` compiler option that allows you to opt-in to + certain deprecations early (currently just `import`). + +* Add a new `silenceDeprecations` compiler option to ignore any deprecation + warnings of the provided types. + +### Command-Line Interface + +* Add a new `--silence-deprecation` flag, which causes the compiler to ignore + any deprecation warnings of the provided types. + +* Previously, if a future deprecation was passed to `--fatal-deprecation` but + not `--future-deprecation`, it would be treated as fatal despite not being + enabled. Both flags are now required to treat a future deprecation as fatal + with a warning emitted if `--fatal-deprecation` is passed without + `--future-deprecation`, matching the JS API's behavior. + +### Dart API + +* The `compile` methods now take in a `silenceDeprecations` parameter, which + causes the compiler to ignore any deprecation warnings of the provided types. + +* Add `Deprecation.obsoleteIn` to match the JS API. This is currently null for + all deprecations, but will be used once some deprecations become obsolete in + Dart Sass 2.0.0. + +* **Potentially breaking bug fix:** Fix a bug where `compileStringToResultAsync` + ignored `fatalDeprecations` and `futureDeprecations`. + +* The behavior around making future deprecations fatal mentioned in the CLI + section above has also been changed in the Dart API. + ## 1.73.0 * Add support for nesting in plain CSS files. This is not processed by Sass at diff --git a/bin/sass.dart b/bin/sass.dart index d24439eb9..cc912d041 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -15,7 +15,7 @@ import 'package:sass/src/executable/watch.dart'; import 'package:sass/src/import_cache.dart'; import 'package:sass/src/importer/filesystem.dart'; import 'package:sass/src/io.dart'; -import 'package:sass/src/logger/deprecation_handling.dart'; +import 'package:sass/src/logger/deprecation_processing.dart'; import 'package:sass/src/stylesheet_graph.dart'; import 'package:sass/src/utils.dart'; import 'package:sass/src/embedded/executable.dart' @@ -53,7 +53,8 @@ Future main(List args) async { // limit repetition. A separate DeprecationHandlingLogger is created for // each compilation, which will limit repetition if verbose is not // passed in addition to handling fatal/future deprecations. - logger: DeprecationHandlingLogger(options.logger, + logger: DeprecationProcessingLogger(options.logger, + silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, futureDeprecations: options.futureDeprecations, limitRepetition: false))); diff --git a/lib/sass.dart b/lib/sass.dart index dc94a90fd..eb58adc4f 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -108,6 +108,7 @@ CompileResult compileToResult(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compile(path, @@ -123,6 +124,7 @@ CompileResult compileToResult(String path, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -207,6 +209,7 @@ CompileResult compileStringToResult(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileString(source, @@ -225,6 +228,7 @@ CompileResult compileStringToResult(String source, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -245,6 +249,7 @@ Future compileToResultAsync(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileAsync(path, @@ -260,6 +265,7 @@ Future compileToResultAsync(String path, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -285,6 +291,7 @@ Future compileStringToResultAsync(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileStringAsync(source, @@ -302,7 +309,10 @@ Future compileStringToResultAsync(String source, quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, - charset: charset); + charset: charset, + silenceDeprecations: silenceDeprecations, + fatalDeprecations: fatalDeprecations, + futureDeprecations: futureDeprecations); /// Like [compileToResult], but returns [CompileResult.css] rather than /// returning [CompileResult] directly. diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index a940d3f26..063f3d2dc 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -17,7 +17,7 @@ import 'importer/legacy_node.dart'; import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/async_evaluate.dart'; @@ -42,10 +42,12 @@ Future compileAsync(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) async { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); @@ -106,10 +108,12 @@ Future compileStringAsync(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) async { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); diff --git a/lib/src/compile.dart b/lib/src/compile.dart index 94221405d..5a7fe54f2 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: a9421a2975e79ad591ae32474cd076e1379d0e75 +// Checksum: ab2c6fa2588988a86abdbe87512134098e01b39e // // ignore_for_file: unused_import @@ -26,7 +26,7 @@ import 'importer/legacy_node.dart'; import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/evaluate.dart'; @@ -51,10 +51,12 @@ CompileResult compile(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); @@ -115,10 +117,12 @@ CompileResult compileString(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 3f8540634..a7412e2ce 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -112,14 +112,28 @@ enum Deprecation { /// what version of Dart Sass this deprecation will be live in. final bool isFuture; + /// Underlying version string used by [obsoleteIn]. + /// + /// This is necessary because [Version] doesn't have a constant constructor, + /// so we can't use it directly as an enum property. + final String? _obsoleteIn; + + /// The Dart Sass version this feature was fully removed in, making the + /// deprecation obsolete. + /// + /// For deprecations that are not yet obsolete, this should be null. + Version? get obsoleteIn => _obsoleteIn?.andThen(Version.parse); + /// Constructs a regular deprecation. const Deprecation(this.id, {required String? deprecatedIn, this.description}) : _deprecatedIn = deprecatedIn, + _obsoleteIn = null, isFuture = false; /// Constructs a future deprecation. const Deprecation.future(this.id, {this.description}) : _deprecatedIn = null, + _obsoleteIn = null, isFuture = true; @override diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 356130d84..675216022 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:native_synchronization/mailbox.dart'; import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; @@ -124,6 +125,21 @@ final class CompilationDispatcher { : EmbeddedLogger(this, color: request.alertColor, ascii: request.alertAscii); + sass.Deprecation? deprecationOrWarn(String id) { + var deprecation = sass.Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } + return deprecation; + } + + var fatalDeprecations = + request.fatalDeprecation.map(deprecationOrWarn).whereNotNull(); + var silenceDeprecations = + request.silenceDeprecation.map(deprecationOrWarn).whereNotNull(); + var futureDeprecations = + request.futureDeprecation.map(deprecationOrWarn).whereNotNull(); + try { var importers = request.importers.map((importer) => _decodeImporter(request, importer) ?? @@ -148,6 +164,9 @@ final class CompilationDispatcher { url: input.url.isEmpty ? null : input.url, quietDeps: request.quietDeps, verbose: request.verbose, + fatalDeprecations: fatalDeprecations, + silenceDeprecations: silenceDeprecations, + futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, charset: request.charset); @@ -165,6 +184,9 @@ final class CompilationDispatcher { style: style, quietDeps: request.quietDeps, verbose: request.verbose, + fatalDeprecations: fatalDeprecations, + silenceDeprecations: silenceDeprecations, + futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, charset: request.charset); } on FileSystemException catch (error) { diff --git a/lib/src/embedded/logger.dart b/lib/src/embedded/logger.dart index 331acdf2b..dd1f2a223 100644 --- a/lib/src/embedded/logger.dart +++ b/lib/src/embedded/logger.dart @@ -6,6 +6,7 @@ import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; +import '../deprecation.dart'; import '../logger.dart'; import '../util/nullable.dart'; import '../utils.dart'; @@ -14,7 +15,7 @@ import 'embedded_sass.pb.dart' hide SourceSpan; import 'utils.dart'; /// A Sass logger that sends log messages as `LogEvent`s. -final class EmbeddedLogger implements Logger { +final class EmbeddedLogger extends LoggerWithDeprecationType { /// The [CompilationDispatcher] to which to send events. final CompilationDispatcher _dispatcher; @@ -39,16 +40,16 @@ final class EmbeddedLogger implements Logger { ': $message\n'); } - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { var formatted = withGlyphs(() { var buffer = StringBuffer(); if (_color) { buffer.write('\u001b[33m\u001b[1m'); - if (deprecation) buffer.write('Deprecation '); + if (deprecation != null) buffer.write('Deprecation '); buffer.write('Warning\u001b[0m'); } else { - if (deprecation) buffer.write('DEPRECATION '); + if (deprecation != null) buffer.write('DEPRECATION '); buffer.write('WARNING'); } if (span == null) { @@ -65,12 +66,14 @@ final class EmbeddedLogger implements Logger { }, ascii: _ascii); var event = OutboundMessage_LogEvent() - ..type = - deprecation ? LogEventType.DEPRECATION_WARNING : LogEventType.WARNING + ..type = deprecation != null + ? LogEventType.DEPRECATION_WARNING + : LogEventType.WARNING ..message = message ..formatted = formatted; if (span != null) event.span = protofySpan(span); if (trace != null) event.stackTrace = trace.toString(); + if (deprecation != null) event.deprecationType = deprecation.id; _dispatcher.sendLog(event); } } diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 876b55814..c0aeaa077 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -95,29 +95,10 @@ final class ExecutableOptions { help: 'Deprecations to treat as errors. You may also pass a Sass\n' 'version to include any behavior deprecated in or before it.\n' 'See https://sass-lang.com/documentation/breaking-changes for \n' - 'a complete list.', - allowedHelp: { - for (var deprecation in Deprecation.values) - if (deprecation - case Deprecation( - deprecatedIn: _?, - :var id, - :var description? - )) - id: description - }) + 'a complete list.') + ..addMultiOption('silence-deprecation', help: 'Deprecations to ignore.') ..addMultiOption('future-deprecation', - help: 'Opt in to a deprecation early.', - allowedHelp: { - for (var deprecation in Deprecation.values) - if (deprecation - case Deprecation( - deprecatedIn: null, - :var id, - :var description? - )) - id: description - }); + help: 'Opt in to a deprecation early.'); parser ..addSeparator(_separator('Other')) @@ -521,6 +502,12 @@ final class ExecutableOptions { : p.absolute(path)); } + /// The set of deprecations whose warnings should be silenced. + Set get silenceDeprecations => { + for (var id in _options['silence-deprecation'] as List) + Deprecation.fromId(id) ?? _fail('Invalid deprecation "$id".') + }; + /// The set of deprecations that cause errors. Set get fatalDeprecations => _fatalDeprecations ??= () { var deprecations = {}; diff --git a/lib/src/js.dart b/lib/src/js.dart index 3246fc743..79bf90180 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -2,7 +2,10 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:js/js_util.dart'; + import 'js/exception.dart'; +import 'js/deprecations.dart'; import 'js/exports.dart'; import 'js/compile.dart'; import 'js/compiler.dart'; @@ -52,6 +55,8 @@ void main() { warn: allowInteropNamed('sass.Logger.silent.warn', (_, __) {}), debug: allowInteropNamed('sass.Logger.silent.debug', (_, __) {}))); exports.NodePackageImporter = nodePackageImporterClass; + exports.deprecations = jsify(deprecations); + exports.Version = versionClass; exports.info = "dart-sass\t${const String.fromEnvironment('version')}\t(Sass Compiler)\t" diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index a08806975..ab5c57cc0 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -7,6 +7,7 @@ import 'package:node_interop/js.dart'; import 'package:node_interop/util.dart' hide futureToPromise; import 'package:term_glyph/term_glyph.dart' as glyph; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import '../../sass.dart'; import '../importer/no_op.dart'; @@ -20,6 +21,7 @@ import '../logger/js_to_dart.dart'; import '../util/nullable.dart'; import 'compile_options.dart'; import 'compile_result.dart'; +import 'deprecations.dart' as js show Deprecation; import 'exception.dart'; import 'importer.dart'; import 'reflection.dart'; @@ -35,6 +37,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { } var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); try { var result = compileToResult(path, color: color, @@ -44,10 +48,15 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers?.map(_parseImporter), - functions: _parseFunctions(options?.functions).cast()); + functions: _parseFunctions(options?.functions).cast(), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); } on SassException catch (error, stackTrace) { @@ -62,6 +71,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { NodeCompileResult compileString(String text, [CompileStringOptions? options]) { var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); try { var result = compileStringToResult(text, syntax: parseSyntax(options?.syntax), @@ -73,12 +84,17 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers?.map(_parseImporter), importer: options?.importer.andThen(_parseImporter) ?? (options?.url == null ? NoOpImporter() : null), - functions: _parseFunctions(options?.functions).cast()); + functions: _parseFunctions(options?.functions).cast(), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); } on SassException catch (error, stackTrace) { @@ -96,6 +112,8 @@ Promise compileAsync(String path, [CompileOptions? options]) { } var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); return _wrapAsyncSassExceptions(futureToPromise(() async { var result = await compileToResultAsync(path, color: color, @@ -105,11 +123,16 @@ Promise compileAsync(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers ?.map((importer) => _parseAsyncImporter(importer)), - functions: _parseFunctions(options?.functions, asynch: true)); + functions: _parseFunctions(options?.functions, asynch: true), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); }()), color: color, ascii: ascii); @@ -122,6 +145,8 @@ Promise compileAsync(String path, [CompileOptions? options]) { Promise compileStringAsync(String text, [CompileStringOptions? options]) { var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); return _wrapAsyncSassExceptions(futureToPromise(() async { var result = await compileStringToResultAsync(text, syntax: parseSyntax(options?.syntax), @@ -133,14 +158,19 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers ?.map((importer) => _parseAsyncImporter(importer)), importer: options?.importer .andThen((importer) => _parseAsyncImporter(importer)) ?? (options?.url == null ? NoOpImporter() : null), - functions: _parseFunctions(options?.functions, asynch: true)); + functions: _parseFunctions(options?.functions, asynch: true), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); }()), color: color, ascii: ascii); @@ -329,6 +359,39 @@ List _parseFunctions(Object? functions, {bool asynch = false}) { return result; } +/// Parses a list of [deprecations] from JS into an list of Dart [Deprecation] +/// objects. +/// +/// [deprecations] can contain deprecation IDs, JS Deprecation objects, and +/// (if [supportVersions] is true) [Version]s. +Iterable? _parseDeprecations( + JSToDartLogger logger, List? deprecations, + {bool supportVersions = false}) { + if (deprecations == null) return null; + return () sync* { + for (var item in deprecations) { + switch (item) { + case String id: + var deprecation = Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } else { + yield deprecation; + } + case js.Deprecation(:var id): + var deprecation = Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } else { + yield deprecation; + } + case Version version when supportVersions: + yield* Deprecation.forVersion(version); + } + } + }(); +} + /// The exported `NodePackageImporter` class that can be added to the /// `importers` option to enable loading `pkg:` URLs from `node_modules`. final JSClass nodePackageImporterClass = () { diff --git a/lib/src/js/compile_options.dart b/lib/src/js/compile_options.dart index 1789539d5..c53a1893d 100644 --- a/lib/src/js/compile_options.dart +++ b/lib/src/js/compile_options.dart @@ -23,6 +23,9 @@ class CompileOptions { external JSLogger? get logger; external List? get importers; external Object? get functions; + external List? get fatalDeprecations; + external List? get silenceDeprecations; + external List? get futureDeprecations; } @JS() diff --git a/lib/src/js/deprecations.dart b/lib/src/js/deprecations.dart new file mode 100644 index 000000000..51e3aec1a --- /dev/null +++ b/lib/src/js/deprecations.dart @@ -0,0 +1,65 @@ +// Copyright 2023 Google LLC. 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:js/js.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../deprecation.dart' as dart show Deprecation; +import 'reflection.dart'; + +@JS() +@anonymous +class Deprecation { + external String get id; + external String get status; + external String? get description; + external Version? get deprecatedIn; + external Version? get obsoleteIn; + + external factory Deprecation( + {required String id, + required String status, + String? description, + Version? deprecatedIn, + Version? obsoleteIn}); +} + +final Map deprecations = { + for (var deprecation in dart.Deprecation.values) + // `calc-interp` was never actually used, so we don't want to expose it + // in the JS API. + if (deprecation != dart.Deprecation.calcInterp) + deprecation.id: Deprecation( + id: deprecation.id, + status: (() => switch (deprecation) { + dart.Deprecation(isFuture: true) => 'future', + dart.Deprecation(deprecatedIn: null, obsoleteIn: null) => + 'user', + dart.Deprecation(obsoleteIn: null) => 'active', + _ => 'obsolete' + })(), + description: deprecation.description, + deprecatedIn: deprecation.deprecatedIn, + obsoleteIn: deprecation.deprecatedIn), +}; + +/// The JavaScript `Version` class. +final JSClass versionClass = () { + var jsClass = createJSClass('sass.Version', + (Object self, int major, int minor, int patch) { + return Version(major, minor, patch); + }); + + jsClass.defineStaticMethod('parse', (String version) { + var v = Version.parse(version); + if (v.isPreRelease || v.build.isNotEmpty) { + throw FormatException( + 'Build identifiers and prerelease versions not supported.'); + } + return v; + }); + + getJSClass(Version(0, 0, 0)).injectSuperclass(jsClass); + return jsClass; +}(); diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index 9a45268a8..e106ad610 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -27,6 +27,8 @@ class Exports { external set Exception(JSClass function); external set Logger(LoggerNamespace namespace); external set NodePackageImporter(JSClass function); + external set deprecations(Object? object); + external set Version(JSClass version); // Value APIs external set Value(JSClass function); diff --git a/lib/src/js/logger.dart b/lib/src/js/logger.dart index fb28030d5..2e6fd6fc0 100644 --- a/lib/src/js/logger.dart +++ b/lib/src/js/logger.dart @@ -5,6 +5,8 @@ import 'package:js/js.dart'; import 'package:source_span/source_span.dart'; +import 'deprecations.dart'; + @JS() @anonymous class JSLogger { @@ -20,11 +22,15 @@ class JSLogger { @anonymous class WarnOptions { external bool get deprecation; + external Deprecation? get deprecationType; external SourceSpan? get span; external String? get stack; external factory WarnOptions( - {required bool deprecation, SourceSpan? span, String? stack}); + {required bool deprecation, + Deprecation? deprecationType, + SourceSpan? span, + String? stack}); } @JS() diff --git a/lib/src/logger.dart b/lib/src/logger.dart index a329b3b79..7569b6e7c 100644 --- a/lib/src/logger.dart +++ b/lib/src/logger.dart @@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'deprecation.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'logger/stderr.dart'; /// An interface for loggers that print messages produced by Sass stylesheets. @@ -37,6 +37,39 @@ abstract class Logger { void debug(String message, SourceSpan span); } +/// A base class for loggers that support the [Deprecation] object, rather than +/// just a boolean flag for whether a warnings is a deprecation warning or not. +/// +/// In Dart Sass 2.0.0, we will eliminate this interface and change +/// [Logger.warn]'s signature to match that of [internalWarn]. This is used +/// in the meantime to provide access to the [Deprecation] object to internal +/// loggers. +/// +/// Implementers should override the protected [internalWarn] method instead of +/// [warn]. +@internal +abstract class LoggerWithDeprecationType implements Logger { + /// This forwards all calls to [internalWarn]. + /// + /// For non-user deprecation warnings, the [warnForDeprecation] extension + /// method should be called instead. + void warn(String message, + {FileSpan? span, Trace? trace, bool deprecation = false}) { + internalWarn(message, + span: span, + trace: trace, + deprecation: deprecation ? Deprecation.userAuthored : null); + } + + /// Equivalent to [Logger.warn], but for internal loggers that support + /// the [Deprecation] object. + /// + /// Subclasses of this logger should override this method instead of [warn]. + @protected + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}); +} + /// An extension to add a `warnForDeprecation` method to loggers without /// making a breaking API change. @internal @@ -44,9 +77,11 @@ extension WarnForDeprecation on Logger { /// Emits a deprecation warning for [deprecation] with the given [message]. void warnForDeprecation(Deprecation deprecation, String message, {FileSpan? span, Trace? trace}) { - if (this case DeprecationHandlingLogger self) { - self.warnForDeprecation(deprecation, message, span: span, trace: trace); - } else if (!deprecation.isFuture) { + if (deprecation.isFuture && this is! DeprecationProcessingLogger) return; + if (this case LoggerWithDeprecationType self) { + self.internalWarn(message, + span: span, trace: trace, deprecation: deprecation); + } else { warn(message, span: span, trace: trace, deprecation: true); } } diff --git a/lib/src/logger/deprecation_handling.dart b/lib/src/logger/deprecation_processing.dart similarity index 51% rename from lib/src/logger/deprecation_handling.dart rename to lib/src/logger/deprecation_processing.dart index 4b185651b..37bd4464f 100644 --- a/lib/src/logger/deprecation_handling.dart +++ b/lib/src/logger/deprecation_processing.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; @@ -11,17 +12,21 @@ import '../exception.dart'; import '../logger.dart'; /// The maximum number of repetitions of the same warning -/// [DeprecationHandlingLogger] will emit before hiding the rest. +/// [DeprecationProcessingLogger] will emit before hiding the rest. const _maxRepetitions = 5; /// A logger that wraps an inner logger to have special handling for -/// deprecation warnings. -final class DeprecationHandlingLogger implements Logger { +/// deprecation warnings, silencing, making fatal, enabling future, and/or +/// limiting repetition based on its inputs. +final class DeprecationProcessingLogger extends LoggerWithDeprecationType { /// A map of how many times each deprecation has been emitted by this logger. final _warningCounts = {}; final Logger _inner; + /// Deprecation warnings of these types will be ignored. + final Set silenceDeprecations; + /// Deprecation warnings of one of these types will cause an error to be /// thrown. /// @@ -36,27 +41,79 @@ final class DeprecationHandlingLogger implements Logger { /// [_maxRepetitions]. final bool limitRepetition; - DeprecationHandlingLogger(this._inner, - {required this.fatalDeprecations, + DeprecationProcessingLogger(this._inner, + {required this.silenceDeprecations, + required this.fatalDeprecations, required this.futureDeprecations, - this.limitRepetition = true}); + this.limitRepetition = true}) { + for (var deprecation in fatalDeprecations) { + switch (deprecation) { + case Deprecation(isFuture: true) + when !futureDeprecations.contains(deprecation): + warn('Future $deprecation deprecation must be enabled before it can ' + 'be made fatal.'); + case Deprecation(obsoleteIn: Version()): + warn('$deprecation deprecation is obsolete, so does not need to be ' + 'made fatal.'); + case _ when silenceDeprecations.contains(deprecation): + warn('Ignoring setting to silence $deprecation deprecation, since it ' + 'has also been made fatal.'); + default: + // No warning. + } + } - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { - _inner.warn(message, span: span, trace: trace, deprecation: deprecation); + for (var deprecation in silenceDeprecations) { + switch (deprecation) { + case Deprecation.userAuthored: + warn('User-authored deprecations should not be silenced.'); + case Deprecation(obsoleteIn: Version()): + warn('$deprecation deprecation is obsolete. If you were previously ' + 'silencing it, your code may now behave in unexpected ways.'); + case Deprecation(isFuture: true) + when futureDeprecations.contains(deprecation): + warn('Conflicting options for future $deprecation deprecation cancel ' + 'each other out.'); + case Deprecation(isFuture: true): + warn('Future $deprecation deprecation is not yet active, so ' + 'silencing it is unnecessary.'); + default: + // No warning. + } + } + + for (var deprecation in futureDeprecations) { + if (!deprecation.isFuture) { + warn('$deprecation is not a future deprecation, so it does not need to ' + 'be explicitly enabled.'); + } + } + } + + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { + if (deprecation != null) { + _handleDeprecation(deprecation, message, span: span, trace: trace); + } else { + _inner.warn(message, span: span, trace: trace); + } } /// Processes a deprecation warning. /// /// If [deprecation] is in [fatalDeprecations], this shows an error. /// - /// If it's a future deprecation that hasn't been opted into or its a + /// If it's a future deprecation that hasn't been opted into or it's a /// deprecation that's already been warned for [_maxReptitions] times and /// [limitRepetitions] is true, the warning is dropped. /// /// Otherwise, this is passed on to [warn]. - void warnForDeprecation(Deprecation deprecation, String message, + void _handleDeprecation(Deprecation deprecation, String message, {FileSpan? span, Trace? trace}) { + if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { + return; + } + if (fatalDeprecations.contains(deprecation)) { message += "\n\nThis is only an error because you've set the " '$deprecation deprecation to be fatal.\n' @@ -67,10 +124,7 @@ final class DeprecationHandlingLogger implements Logger { _ => SassScriptException(message) }; } - - if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { - return; - } + if (silenceDeprecations.contains(deprecation)) return; if (limitRepetition) { var count = @@ -78,7 +132,12 @@ final class DeprecationHandlingLogger implements Logger { if (count > _maxRepetitions) return; } - warn(message, span: span, trace: trace, deprecation: true); + if (_inner case LoggerWithDeprecationType inner) { + inner.internalWarn(message, + span: span, trace: trace, deprecation: deprecation); + } else { + _inner.warn(message, span: span, trace: trace, deprecation: true); + } } void debug(String message, SourceSpan span) => _inner.debug(message, span); diff --git a/lib/src/logger/js_to_dart.dart b/lib/src/logger/js_to_dart.dart index aa58a243d..4a11bf546 100644 --- a/lib/src/logger/js_to_dart.dart +++ b/lib/src/logger/js_to_dart.dart @@ -7,11 +7,13 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:term_glyph/term_glyph.dart' as glyph; +import '../deprecation.dart'; import '../logger.dart'; +import '../js/deprecations.dart' show deprecations; import '../js/logger.dart'; /// A wrapper around a [JSLogger] that exposes it as a Dart [Logger]. -final class JSToDartLogger implements Logger { +final class JSToDartLogger extends LoggerWithDeprecationType { /// The wrapped logger object. final JSLogger? _node; @@ -27,19 +29,20 @@ final class JSToDartLogger implements Logger { JSToDartLogger(this._node, this._fallback, {bool? ascii}) : _ascii = ascii ?? glyph.ascii; - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { if (_node?.warn case var warn?) { warn( message, WarnOptions( span: span ?? (undefined as SourceSpan?), stack: trace.toString(), - deprecation: deprecation)); + deprecation: deprecation != null, + deprecationType: deprecations[deprecation?.id])); } else { _withAscii(() { _fallback.warn(message, - span: span, trace: trace, deprecation: deprecation); + span: span, trace: trace, deprecation: deprecation != null); }); } } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 6e8d05390..32421eb86 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.1.0 + +* No user-visible changes. + ## 10.0.0 * Remove the `allowPlaceholders` argument from `SelectorList.parse()`. Instead, @@ -95,7 +99,7 @@ * All uses of classes from the `tuple` package have been replaced by record types. - + ## 7.2.2 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 4780522de..c6b9f287b 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 10.0.0 +version: 10.1.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.73.0 + sass: 1.74.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 52c8c58df..9fada08e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.73.0 +version: 1.74.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/test/embedded/utils.dart b/test/embedded/utils.dart index 68c1e2f83..7741706d6 100644 --- a/test/embedded/utils.dart +++ b/test/embedded/utils.dart @@ -7,6 +7,7 @@ import 'package:test/test.dart'; import 'package:sass/src/embedded/embedded_sass.pb.dart'; import 'package:sass/src/embedded/utils.dart'; +import 'package:sass/src/util/nullable.dart'; import 'embedded_process.dart'; @@ -27,7 +28,10 @@ InboundMessage compileString(String css, bool? sourceMapIncludeSources, Iterable? importers, InboundMessage_CompileRequest_Importer? importer, - Iterable? functions}) { + Iterable? functions, + Iterable? fatalDeprecations, + Iterable? futureDeprecations, + Iterable? silenceDeprecations}) { var input = InboundMessage_CompileRequest_StringInput()..source = css; if (syntax != null) input.syntax = syntax; if (url != null) input.url = url; @@ -43,7 +47,9 @@ InboundMessage compileString(String css, if (functions != null) request.globalFunctions.addAll(functions); if (alertColor != null) request.alertColor = alertColor; if (alertAscii != null) request.alertAscii = alertAscii; - + fatalDeprecations.andThen(request.fatalDeprecation.addAll); + futureDeprecations.andThen(request.futureDeprecation.addAll); + silenceDeprecations.andThen(request.silenceDeprecation.addAll); return InboundMessage()..compileRequest = request; } diff --git a/tool/grind.dart b/tool/grind.dart index 95d9b3cc8..33b56ff2e 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -87,6 +87,8 @@ void main(List args) { 'NULL', 'types', 'NodePackageImporter', + 'deprecations', + 'Version', }; pkg.githubReleaseNotes.fn = () =>