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:
Natalie Weizenbaum 2019-06-06 19:42:44 +01:00 committed by GitHub
parent 58cc58a1f4
commit bea609d74b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 280 additions and 69 deletions

View File

@ -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.

View File

@ -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.
///

View File

@ -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,32 +241,45 @@ class _EvaluateVisitor
_logger = logger ?? const Logger.stderr(),
_sourceMap = sourceMap;
Future<EvaluateResult> run(AsyncImporter importer, Stylesheet node) async {
var url = node.span?.sourceUrl;
if (url != null) {
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
} else if (url.toString() != 'stdin') {
_includedFiles.add(url.toString());
Future<EvaluateResult> run(AsyncImporter importer, Stylesheet node) {
return _withWarnCallback(() async {
var url = node.span?.sourceUrl;
if (url != null) {
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
} else if (url.toString() != 'stdin') {
_includedFiles.add(url.toString());
}
}
}
}
var module = await _execute(importer, node);
var module = await _execute(importer, node);
return EvaluateResult(_combineCss(module), _includedFiles);
return EvaluateResult(_combineCss(module), _includedFiles);
});
}
Future<Value> runExpression(Expression expression,
{Map<String, Value> variables}) {
_environment = _newEnvironment();
return _withWarnCallback(() async {
_environment = _newEnvironment();
for (var name in variables?.keys ?? const <String>[]) {
_environment.setVariable(name, variables[name], null, global: true);
}
for (var name in variables?.keys ?? const <String>[]) {
_environment.setVariable(name, variables[name], null, global: true);
}
return expression.accept(this);
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;
}
}

View File

@ -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,30 +250,43 @@ class _EvaluateVisitor
_sourceMap = sourceMap;
EvaluateResult run(Importer importer, Stylesheet node) {
var url = node.span?.sourceUrl;
if (url != null) {
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
} else if (url.toString() != 'stdin') {
_includedFiles.add(url.toString());
return _withWarnCallback(() {
var url = node.span?.sourceUrl;
if (url != null) {
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
} else if (url.toString() != 'stdin') {
_includedFiles.add(url.toString());
}
}
}
}
var module = _execute(importer, node);
var module = _execute(importer, node);
return EvaluateResult(_combineCss(module), _includedFiles);
return EvaluateResult(_combineCss(module), _includedFiles);
});
}
Value runExpression(Expression expression, {Map<String, Value> variables}) {
_environment = _newEnvironment();
return _withWarnCallback(() {
_environment = _newEnvironment();
for (var name in variables?.keys ?? const <String>[]) {
_environment.setVariable(name, variables[name], null, global: true);
}
for (var name in variables?.keys ?? const <String>[]) {
_environment.setVariable(name, variables[name], null, global: true);
}
return expression.accept(this);
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
View 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});
}

View File

@ -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

View File

@ -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);
}

View File

@ -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.

View 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);
}