mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-30 04:39:03 +01:00
Add a top-level warn() function for functions and importers (#711)
In addition to being useful for users of Sass, this will make it possible for core Sass functions to produce warnings without needing an explicit reference to the evaluator.
This commit is contained in:
parent
58cc58a1f4
commit
bea609d74b
@ -1,3 +1,8 @@
|
||||
## 1.21.0
|
||||
|
||||
* Add a top-level `warn()` function for custom functions and importers to print
|
||||
warning messages.
|
||||
|
||||
## 1.20.3
|
||||
|
||||
* No user-visible changes.
|
||||
|
@ -28,6 +28,7 @@ export 'src/syntax.dart';
|
||||
export 'src/value.dart' show ListSeparator;
|
||||
export 'src/value/external/value.dart';
|
||||
export 'src/visitor/serialize.dart' show OutputStyle;
|
||||
export 'src/warn.dart' show warn;
|
||||
|
||||
/// Loads the Sass file at [path], compiles it to CSS, and returns the result.
|
||||
///
|
||||
|
@ -36,6 +36,7 @@ import '../syntax.dart';
|
||||
import '../util/fixed_length_list_builder.dart';
|
||||
import '../utils.dart';
|
||||
import '../value.dart';
|
||||
import '../warn.dart';
|
||||
import 'interface/css.dart';
|
||||
import 'interface/expression.dart';
|
||||
import 'interface/modifiable_css.dart';
|
||||
@ -143,14 +144,19 @@ class _EvaluateVisitor
|
||||
/// The human-readable name of the current stack frame.
|
||||
var _member = "root stylesheet";
|
||||
|
||||
/// The node for the innermost callable that's been invoked.
|
||||
/// The node for the innermost callable that's being invoked.
|
||||
///
|
||||
/// This is used to provide `call()` with a span. It's stored as an [AstNode]
|
||||
/// rather than a [FileSpan] so we can avoid calling [AstNode.span] if the
|
||||
/// span isn't required, since some nodes need to do real work to manufacture
|
||||
/// a source span.
|
||||
/// This is used to produce warnings for function calls. It's stored as an
|
||||
/// [AstNode] rather than a [FileSpan] so we can avoid calling [AstNode.span]
|
||||
/// if the span isn't required, since some nodes need to do real work to
|
||||
/// manufacture a source span.
|
||||
AstNode _callableNode;
|
||||
|
||||
/// The span for the current import that's being resolved.
|
||||
///
|
||||
/// This is used to produce warnings for importers.
|
||||
FileSpan _importSpan;
|
||||
|
||||
/// Whether we're currently executing a function.
|
||||
var _inFunction = false;
|
||||
|
||||
@ -235,7 +241,8 @@ class _EvaluateVisitor
|
||||
_logger = logger ?? const Logger.stderr(),
|
||||
_sourceMap = sourceMap;
|
||||
|
||||
Future<EvaluateResult> run(AsyncImporter importer, Stylesheet node) async {
|
||||
Future<EvaluateResult> run(AsyncImporter importer, Stylesheet node) {
|
||||
return _withWarnCallback(() async {
|
||||
var url = node.span?.sourceUrl;
|
||||
if (url != null) {
|
||||
if (_asNodeSass) {
|
||||
@ -250,10 +257,12 @@ class _EvaluateVisitor
|
||||
var module = await _execute(importer, node);
|
||||
|
||||
return EvaluateResult(_combineCss(module), _includedFiles);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Value> runExpression(Expression expression,
|
||||
{Map<String, Value> variables}) {
|
||||
return _withWarnCallback(() async {
|
||||
_environment = _newEnvironment();
|
||||
|
||||
for (var name in variables?.keys ?? const <String>[]) {
|
||||
@ -261,6 +270,16 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
return expression.accept(this);
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs [callback] with a definition for the top-level `warn` function.
|
||||
T _withWarnCallback<T>(T callback()) {
|
||||
return withWarnCallback(
|
||||
(message, deprecation) => _warn(
|
||||
message, _importSpan ?? _callableNode.span,
|
||||
deprecation: deprecation),
|
||||
callback);
|
||||
}
|
||||
|
||||
/// Executes [stylesheet], loaded by [importer], to produce a module.
|
||||
@ -394,10 +413,9 @@ class _EvaluateVisitor
|
||||
_callableNode.span));
|
||||
|
||||
if (function is SassString) {
|
||||
_warn(
|
||||
warn(
|
||||
"Passing a string to call() is deprecated and will be illegal\n"
|
||||
"in Sass 4.0. Use call(get-function($function)) instead.",
|
||||
_callableNode.span,
|
||||
deprecation: true);
|
||||
|
||||
var expression = FunctionExpression(
|
||||
@ -1056,6 +1074,9 @@ class _EvaluateVisitor
|
||||
Future<Tuple2<AsyncImporter, Stylesheet>> _loadStylesheet(
|
||||
String url, FileSpan span) async {
|
||||
try {
|
||||
assert(_importSpan == null);
|
||||
_importSpan = span;
|
||||
|
||||
if (_nodeImporter != null) {
|
||||
var stylesheet = await _importLikeNode(url);
|
||||
if (stylesheet != null) return Tuple2(null, stylesheet);
|
||||
@ -1083,6 +1104,8 @@ class _EvaluateVisitor
|
||||
message = error.toString();
|
||||
}
|
||||
throw _exception(message, span);
|
||||
} finally {
|
||||
_importSpan = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
// DO NOT EDIT. This file was generated from async_evaluate.dart.
|
||||
// See tool/grind/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: f316e802a42334d416c62f1d60d281a8262016f2
|
||||
// Checksum: b0d4460a876c7bb9248da004dece98c690b798dd
|
||||
//
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
@ -45,6 +45,7 @@ import '../syntax.dart';
|
||||
import '../util/fixed_length_list_builder.dart';
|
||||
import '../utils.dart';
|
||||
import '../value.dart';
|
||||
import '../warn.dart';
|
||||
import 'interface/css.dart';
|
||||
import 'interface/expression.dart';
|
||||
import 'interface/modifiable_css.dart';
|
||||
@ -151,14 +152,19 @@ class _EvaluateVisitor
|
||||
/// The human-readable name of the current stack frame.
|
||||
var _member = "root stylesheet";
|
||||
|
||||
/// The node for the innermost callable that's been invoked.
|
||||
/// The node for the innermost callable that's being invoked.
|
||||
///
|
||||
/// This is used to provide `call()` with a span. It's stored as an [AstNode]
|
||||
/// rather than a [FileSpan] so we can avoid calling [AstNode.span] if the
|
||||
/// span isn't required, since some nodes need to do real work to manufacture
|
||||
/// a source span.
|
||||
/// This is used to produce warnings for function calls. It's stored as an
|
||||
/// [AstNode] rather than a [FileSpan] so we can avoid calling [AstNode.span]
|
||||
/// if the span isn't required, since some nodes need to do real work to
|
||||
/// manufacture a source span.
|
||||
AstNode _callableNode;
|
||||
|
||||
/// The span for the current import that's being resolved.
|
||||
///
|
||||
/// This is used to produce warnings for importers.
|
||||
FileSpan _importSpan;
|
||||
|
||||
/// Whether we're currently executing a function.
|
||||
var _inFunction = false;
|
||||
|
||||
@ -244,6 +250,7 @@ class _EvaluateVisitor
|
||||
_sourceMap = sourceMap;
|
||||
|
||||
EvaluateResult run(Importer importer, Stylesheet node) {
|
||||
return _withWarnCallback(() {
|
||||
var url = node.span?.sourceUrl;
|
||||
if (url != null) {
|
||||
if (_asNodeSass) {
|
||||
@ -258,9 +265,11 @@ class _EvaluateVisitor
|
||||
var module = _execute(importer, node);
|
||||
|
||||
return EvaluateResult(_combineCss(module), _includedFiles);
|
||||
});
|
||||
}
|
||||
|
||||
Value runExpression(Expression expression, {Map<String, Value> variables}) {
|
||||
return _withWarnCallback(() {
|
||||
_environment = _newEnvironment();
|
||||
|
||||
for (var name in variables?.keys ?? const <String>[]) {
|
||||
@ -268,6 +277,16 @@ class _EvaluateVisitor
|
||||
}
|
||||
|
||||
return expression.accept(this);
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs [callback] with a definition for the top-level `warn` function.
|
||||
T _withWarnCallback<T>(T callback()) {
|
||||
return withWarnCallback(
|
||||
(message, deprecation) => _warn(
|
||||
message, _importSpan ?? _callableNode.span,
|
||||
deprecation: deprecation),
|
||||
callback);
|
||||
}
|
||||
|
||||
/// Executes [stylesheet], loaded by [importer], to produce a module.
|
||||
@ -401,10 +420,9 @@ class _EvaluateVisitor
|
||||
_callableNode.span));
|
||||
|
||||
if (function is SassString) {
|
||||
_warn(
|
||||
warn(
|
||||
"Passing a string to call() is deprecated and will be illegal\n"
|
||||
"in Sass 4.0. Use call(get-function($function)) instead.",
|
||||
_callableNode.span,
|
||||
deprecation: true);
|
||||
|
||||
var expression = FunctionExpression(
|
||||
@ -1057,6 +1075,9 @@ class _EvaluateVisitor
|
||||
/// [SassRuntimeException] if loading fails.
|
||||
Tuple2<Importer, Stylesheet> _loadStylesheet(String url, FileSpan span) {
|
||||
try {
|
||||
assert(_importSpan == null);
|
||||
_importSpan = span;
|
||||
|
||||
if (_nodeImporter != null) {
|
||||
var stylesheet = _importLikeNode(url);
|
||||
if (stylesheet != null) return Tuple2(null, stylesheet);
|
||||
@ -1084,6 +1105,8 @@ class _EvaluateVisitor
|
||||
message = error.toString();
|
||||
}
|
||||
throw _exception(message, span);
|
||||
} finally {
|
||||
_importSpan = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
34
lib/src/warn.dart
Normal file
34
lib/src/warn.dart
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2019 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 'dart:async';
|
||||
|
||||
/// Prints a warning message associated with the current `@import` or function
|
||||
/// call.
|
||||
///
|
||||
/// If [deprecation] is `true`, the warning is emitted as a deprecation warning.
|
||||
///
|
||||
/// This may only be called within a custom function or importer callback.
|
||||
void warn(String message, {bool deprecation = false}) {
|
||||
var warnDefinition = Zone.current[#_warn];
|
||||
|
||||
if (warnDefinition == null) {
|
||||
throw ArgumentError(
|
||||
"warn() may only be called within a custom function or importer "
|
||||
"callback.");
|
||||
}
|
||||
|
||||
warnDefinition(message, deprecation);
|
||||
}
|
||||
|
||||
/// Runs [callback] with [warn] as the definition for the top-level `warn()` function.
|
||||
///
|
||||
/// This is zone-based, so if [callback] is asynchronous [warn] is set for the
|
||||
/// duration of that callback.
|
||||
T withWarnCallback<T>(
|
||||
void warn(String message, bool deprecation), T callback()) {
|
||||
return runZoned(() {
|
||||
return callback();
|
||||
}, zoneValues: {#_warn: warn});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
name: sass
|
||||
version: 1.20.3
|
||||
version: 1.21.0-dev
|
||||
description: A Sass implementation in Dart.
|
||||
author: Dart Team <misc@dartlang.org>
|
||||
homepage: https://github.com/sass/dart-sass
|
||||
|
@ -12,10 +12,12 @@ import 'package:test/test.dart';
|
||||
import 'package:sass/sass.dart';
|
||||
import 'package:sass/src/exception.dart';
|
||||
|
||||
import 'test_importer.dart';
|
||||
|
||||
main() {
|
||||
test("uses an importer to resolve an @import", () {
|
||||
var css = compileString('@import "orange";', importers: [
|
||||
_TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
var color = url.path;
|
||||
return ImporterResult('.$color {color: $color}', indented: false);
|
||||
})
|
||||
@ -26,7 +28,7 @@ main() {
|
||||
|
||||
test("passes the canonicalized URL to the importer", () {
|
||||
var css = compileString('@import "orange";', importers: [
|
||||
_TestImporter((url) => Uri.parse('u:blue'), (url) {
|
||||
TestImporter((url) => Uri.parse('u:blue'), (url) {
|
||||
var color = url.path;
|
||||
return ImporterResult('.$color {color: $color}', indented: false);
|
||||
})
|
||||
@ -40,7 +42,7 @@ main() {
|
||||
@import "orange";
|
||||
@import "orange";
|
||||
""", importers: [
|
||||
_TestImporter(
|
||||
TestImporter(
|
||||
(url) => Uri.parse('u:blue'),
|
||||
expectAsync1((url) {
|
||||
var color = url.path;
|
||||
@ -62,7 +64,7 @@ main() {
|
||||
var times = 0;
|
||||
var css = compileString('@import "foo:bar/baz";',
|
||||
importers: [
|
||||
_TestImporter(
|
||||
TestImporter(
|
||||
expectAsync1((url) {
|
||||
times++;
|
||||
if (times == 1) return Uri(path: 'first');
|
||||
@ -93,7 +95,7 @@ main() {
|
||||
SingleMapping map;
|
||||
compileString('@import "orange";',
|
||||
importers: [
|
||||
_TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
var color = url.path;
|
||||
return ImporterResult('.$color {color: $color}',
|
||||
sourceMapUrl: Uri.parse("u:blue"), indented: false);
|
||||
@ -108,7 +110,7 @@ main() {
|
||||
SingleMapping map;
|
||||
compileString('@import "orange";',
|
||||
importers: [
|
||||
_TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
var color = url.path;
|
||||
return ImporterResult('.$color {color: $color}', indented: false);
|
||||
})
|
||||
@ -124,7 +126,7 @@ main() {
|
||||
test("wraps an error in canonicalize()", () {
|
||||
expect(() {
|
||||
compileString('@import "orange";', importers: [
|
||||
_TestImporter((url) {
|
||||
TestImporter((url) {
|
||||
throw "this import is bad actually";
|
||||
}, expectAsync1((_) => null, count: 0))
|
||||
]);
|
||||
@ -139,7 +141,7 @@ main() {
|
||||
test("wraps an error in load()", () {
|
||||
expect(() {
|
||||
compileString('@import "orange";', importers: [
|
||||
_TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
throw "this import is bad actually";
|
||||
})
|
||||
]);
|
||||
@ -154,7 +156,7 @@ main() {
|
||||
test("prefers .message to .toString() for an importer error", () {
|
||||
expect(() {
|
||||
compileString('@import "orange";', importers: [
|
||||
_TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
throw FormatException("bad format somehow");
|
||||
})
|
||||
]);
|
||||
@ -167,16 +169,3 @@ main() {
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/// An [Importer] whose [canonicalize] and [load] methods are provided by
|
||||
/// closures.
|
||||
class _TestImporter extends Importer {
|
||||
final Uri Function(Uri url) _canonicalize;
|
||||
final ImporterResult Function(Uri url) _load;
|
||||
|
||||
_TestImporter(this._canonicalize, this._load);
|
||||
|
||||
Uri canonicalize(Uri url) => _canonicalize(url);
|
||||
|
||||
ImporterResult load(Uri url) => _load(url);
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
import 'package:sass/sass.dart';
|
||||
|
||||
import 'test_importer.dart';
|
||||
|
||||
main() {
|
||||
group("with @warn", () {
|
||||
test("passes the message and stack trace to the logger", () {
|
||||
@ -107,6 +109,122 @@ main() {
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
group("with warn()", () {
|
||||
group("from a function", () {
|
||||
test("synchronously", () {
|
||||
var mustBeCalled = expectAsync0(() {});
|
||||
compileString("""
|
||||
@function bar() {@return foo()}
|
||||
a {b: bar()}
|
||||
""", functions: [
|
||||
Callable("foo", "", expectAsync1((_) {
|
||||
warn("heck");
|
||||
return sassNull;
|
||||
}))
|
||||
], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) {
|
||||
expect(message, equals("heck"));
|
||||
|
||||
expect(span.start.line, equals(0));
|
||||
expect(span.start.column, equals(33));
|
||||
expect(span.end.line, equals(0));
|
||||
expect(span.end.column, equals(38));
|
||||
|
||||
expect(trace.frames.first.member, equals('bar()'));
|
||||
expect(deprecation, isFalse);
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
test("asynchronously", () {
|
||||
var mustBeCalled = expectAsync0(() {});
|
||||
compileStringAsync("""
|
||||
@function bar() {@return foo()}
|
||||
a {b: bar()}
|
||||
""", functions: [
|
||||
AsyncCallable("foo", "", expectAsync1((_) async {
|
||||
warn("heck");
|
||||
return sassNull;
|
||||
}))
|
||||
], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) {
|
||||
expect(message, equals("heck"));
|
||||
|
||||
expect(span.start.line, equals(0));
|
||||
expect(span.start.column, equals(33));
|
||||
expect(span.end.line, equals(0));
|
||||
expect(span.end.column, equals(38));
|
||||
|
||||
expect(trace.frames.first.member, equals('bar()'));
|
||||
expect(deprecation, isFalse);
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
test("asynchronously after a gap", () {
|
||||
var mustBeCalled = expectAsync0(() {});
|
||||
compileStringAsync("""
|
||||
@function bar() {@return foo()}
|
||||
a {b: bar()}
|
||||
""", functions: [
|
||||
AsyncCallable("foo", "", expectAsync1((_) async {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
warn("heck");
|
||||
return sassNull;
|
||||
}))
|
||||
], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) {
|
||||
expect(message, equals("heck"));
|
||||
|
||||
expect(span.start.line, equals(0));
|
||||
expect(span.start.column, equals(33));
|
||||
expect(span.end.line, equals(0));
|
||||
expect(span.end.column, equals(38));
|
||||
|
||||
expect(trace.frames.first.member, equals('bar()'));
|
||||
expect(deprecation, isFalse);
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
test("from an importer", () {
|
||||
var mustBeCalled = expectAsync0(() {});
|
||||
compileString("@import 'foo';", importers: [
|
||||
TestImporter((url) => Uri.parse("u:$url"), (url) {
|
||||
warn("heck");
|
||||
return ImporterResult("", indented: false);
|
||||
})
|
||||
], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) {
|
||||
expect(message, equals("heck"));
|
||||
|
||||
expect(span.start.line, equals(0));
|
||||
expect(span.start.column, equals(8));
|
||||
expect(span.end.line, equals(0));
|
||||
expect(span.end.column, equals(13));
|
||||
|
||||
expect(trace.frames.first.member, equals('root stylesheet'));
|
||||
expect(deprecation, isFalse);
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
test("with deprecation", () {
|
||||
var mustBeCalled = expectAsync0(() {});
|
||||
compileString("a {b: foo()}", functions: [
|
||||
Callable("foo", "", expectAsync1((_) {
|
||||
warn("heck", deprecation: true);
|
||||
return sassNull;
|
||||
}))
|
||||
], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) {
|
||||
expect(message, equals("heck"));
|
||||
expect(deprecation, isTrue);
|
||||
mustBeCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
test("throws an error outside a callback", () {
|
||||
expect(() => warn("heck"), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// A [Logger] whose [warn] and [debug] methods are provided by callbacks.
|
||||
|
18
test/dart_api/test_importer.dart
Normal file
18
test/dart_api/test_importer.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright 2017 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:sass/sass.dart';
|
||||
|
||||
/// An [Importer] whose [canonicalize] and [load] methods are provided by
|
||||
/// closures.
|
||||
class TestImporter extends Importer {
|
||||
final Uri Function(Uri url) _canonicalize;
|
||||
final ImporterResult Function(Uri url) _load;
|
||||
|
||||
TestImporter(this._canonicalize, this._load);
|
||||
|
||||
Uri canonicalize(Uri url) => _canonicalize(url);
|
||||
|
||||
ImporterResult load(Uri url) => _load(url);
|
||||
}
|
Loading…
Reference in New Issue
Block a user