Skip to content

Commit c2baede

Browse files
committed
Throw multi-span exceptions for argument errors
1 parent 0c1370c commit c2baede

20 files changed

+420
-234
lines changed

lib/src/ast/node.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,24 @@ abstract class AstNode {
1111
/// This indicates where in the source Sass or SCSS stylesheet the node was
1212
/// defined.
1313
FileSpan get span;
14+
15+
/// Returns an [AstNode] that doesn't have any data and whose span is
16+
/// generated by [callback].
17+
///
18+
/// Anumber of APIs take [AstNode]s instead of spans because computing spans
19+
/// eagerly can be expensive. This allows arbitrary spans to be passed to
20+
/// those callbacks while sitll being lazily computed.
21+
factory AstNode.fake(FileSpan Function() callback) = _FakeAstNode;
22+
23+
AstNode();
24+
}
25+
26+
/// An [AstNode] that just exposes a single span generated by a callback.
27+
class _FakeAstNode implements AstNode {
28+
FileSpan get span => _callback();
29+
30+
/// The callback to use to generate [span].
31+
final FileSpan Function() _callback;
32+
33+
_FakeAstNode(this._callback);
1434
}

lib/src/ast/sass/argument_declaration.dart

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../../exception.dart';
88
import '../../logger.dart';
99
import '../../parse/scss.dart';
1010
import '../../utils.dart';
11+
import '../../util/character.dart';
1112
import 'argument.dart';
1213
import 'node.dart';
1314

@@ -22,6 +23,32 @@ class ArgumentDeclaration implements SassNode {
2223

2324
final FileSpan span;
2425

26+
/// Returns [span] expanded to include an identifier immediately before the
27+
/// declaration, if possible.
28+
FileSpan get spanWithName {
29+
var text = span.file.getText(0);
30+
31+
// Move backwards through and whitspace between the name and the arguments.
32+
var i = span.start.offset - 1;
33+
while (i > 0 && isWhitespace(text.codeUnitAt(i))) {
34+
i--;
35+
}
36+
37+
// Then move backwards through the name itself.
38+
if (!isName(text.codeUnitAt(i))) return span;
39+
i--;
40+
while (i >= 0 && isName(text.codeUnitAt(i))) {
41+
i--;
42+
}
43+
44+
// If the name didn't start with [isNameStart], it's not a valid identifier.
45+
if (!isNameStart(text.codeUnitAt(i + 1))) return span;
46+
47+
// Trim because it's possible that this span is empty (for example, a mixin
48+
// may be declared without an argument list).
49+
return span.file.span(i + 1, span.end.offset).trim();
50+
}
51+
2552
/// The name of the rest argument as written in the document, without
2653
/// underscores converted to hyphens and including the leading `$`.
2754
///
@@ -47,16 +74,15 @@ class ArgumentDeclaration implements SassNode {
4774
: arguments = const [],
4875
restArgument = null;
4976

50-
/// Parses an argument declaration from [contents], which should not include
51-
/// parentheses.
77+
/// Parses an argument declaration from [contents], which should be of the
78+
/// form `@rule name(args) {`.
5279
///
5380
/// If passed, [url] is the name of the file from which [contents] comes.
5481
///
5582
/// Throws a [SassFormatException] if parsing fails.
5683
factory ArgumentDeclaration.parse(String contents,
5784
{Object url, Logger logger}) =>
58-
ScssParser("($contents)", url: url, logger: logger)
59-
.parseArgumentDeclaration();
85+
ScssParser(contents, url: url, logger: logger).parseArgumentDeclaration();
6086

6187
/// Throws a [SassScriptException] if [positional] and [names] aren't valid
6288
/// for this argument declaration.
@@ -73,27 +99,34 @@ class ArgumentDeclaration implements SassNode {
7399
} else if (names.contains(argument.name)) {
74100
namedUsed++;
75101
} else if (argument.defaultValue == null) {
76-
throw SassScriptException(
77-
"Missing argument ${_originalArgumentName(argument.name)}.");
102+
throw MultiSpanSassScriptException(
103+
"Missing argument ${_originalArgumentName(argument.name)}.",
104+
"invocation",
105+
{spanWithName: "declaration"});
78106
}
79107
}
80108

81109
if (restArgument != null) return;
82110

83111
if (positional > arguments.length) {
84-
throw SassScriptException("Only ${arguments.length} "
85-
"${names.isEmpty ? '' : 'positional '}"
86-
"${pluralize('argument', arguments.length)} allowed, but "
87-
"${positional} ${pluralize('was', positional, plural: 'were')} "
88-
"passed.");
112+
throw MultiSpanSassScriptException(
113+
"Only ${arguments.length} "
114+
"${names.isEmpty ? '' : 'positional '}"
115+
"${pluralize('argument', arguments.length)} allowed, but "
116+
"${positional} ${pluralize('was', positional, plural: 'were')} "
117+
"passed.",
118+
"invocation",
119+
{spanWithName: "declaration"});
89120
}
90121

91122
if (namedUsed < names.length) {
92123
var unknownNames = Set.of(names)
93124
..removeAll(arguments.map((argument) => argument.name));
94-
throw SassScriptException(
125+
throw MultiSpanSassScriptException(
95126
"No ${pluralize('argument', unknownNames.length)} named "
96-
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.");
127+
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.",
128+
"invocation",
129+
{spanWithName: "declaration"});
97130
}
98131
}
99132

lib/src/ast/sass/expression/if.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import '../callable_invocation.dart';
1717
/// evaluated.
1818
class IfExpression implements Expression, CallableInvocation {
1919
/// The declaration of `if()`, as though it were a normal function.
20-
static final declaration =
21-
ArgumentDeclaration.parse(r"$condition, $if-true, $if-false");
20+
static final declaration = ArgumentDeclaration.parse(
21+
r"@function if($condition, $if-true, $if-false) {");
2222

2323
/// The arguments passed to `if()`.
2424
final ArgumentInvocation arguments;

lib/src/ast/sass/statement/include_rule.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:source_span/source_span.dart';
66

7+
import '../../../utils.dart';
78
import '../../../visitor/interface/statement.dart';
89
import '../argument_invocation.dart';
910
import '../callable_invocation.dart';
@@ -29,6 +30,11 @@ class IncludeRule implements Statement, CallableInvocation {
2930

3031
final FileSpan span;
3132

33+
/// Returns this include's span, without its content block (if it has one).
34+
FileSpan get spanWithoutContent => content == null
35+
? span
36+
: span.file.span(span.start.offset, arguments.span.end.offset).trim();
37+
3238
IncludeRule(this.name, this.arguments, this.span,
3339
{this.namespace, this.content});
3440

lib/src/callable.dart

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export 'callable/built_in.dart';
1515
export 'callable/plain_css.dart';
1616
export 'callable/user_defined.dart';
1717

18-
/// An interface functions and mixins that can be invoked from Sass by passing
19-
/// in arguments.
18+
/// An interface for functions and mixins that can be invoked from Sass by
19+
/// passing in arguments.
2020
///
2121
/// This extends [AsyncCallable] because all synchronous callables are also
2222
/// usable in asynchronous contexts. [Callable]s are usable with both the
@@ -65,7 +65,12 @@ export 'callable/user_defined.dart';
6565
/// access a string's length in code points.
6666
@sealed
6767
abstract class Callable extends AsyncCallable {
68-
/// Creates a callable with the given [name] and [arguments] that runs
68+
@Deprecated('Use `new Callable.function` instead.')
69+
factory Callable(String name, String arguments,
70+
ext.Value callback(List<ext.Value> arguments)) =>
71+
Callable.function(name, arguments, callback);
72+
73+
/// Creates a function with the given [name] and [arguments] that runs
6974
/// [callback] when called.
7075
///
7176
/// The argument declaration is parsed from [arguments], which uses the same
@@ -80,7 +85,8 @@ abstract class Callable extends AsyncCallable {
8085
/// For example:
8186
///
8287
/// ```dart
83-
/// new Callable("str-split", r'$string, $divider: " "', (arguments) {
88+
/// new Callable.function("str-split", r'$string, $divider: " "',
89+
/// (arguments) {
8490
/// var string = arguments[0].assertString("string");
8591
/// var divider = arguments[1].assertString("divider");
8692
/// return new SassList(
@@ -90,12 +96,12 @@ abstract class Callable extends AsyncCallable {
9096
/// });
9197
/// ```
9298
///
93-
/// Callables may also take variable length argument lists. These are declared
99+
/// Functions may also take variable length argument lists. These are declared
94100
/// the same way as in Sass, and are passed as the final argument to the
95101
/// callback. For example:
96102
///
97103
/// ```dart
98-
/// new Callable("str-join", r'$strings...', (arguments) {
104+
/// new Callable.function("str-join", r'$strings...', (arguments) {
99105
/// var args = arguments.first as SassArgumentList;
100106
/// var strings = args.map((arg) => arg.assertString()).toList();
101107
/// return new SassString(strings.map((string) => string.text).join(),
@@ -106,8 +112,8 @@ abstract class Callable extends AsyncCallable {
106112
/// Note that the argument list is always an instance of [SassArgumentList],
107113
/// which provides access to keyword arguments using
108114
/// [SassArgumentList.keywords].
109-
factory Callable(String name, String arguments,
115+
factory Callable.function(String name, String arguments,
110116
ext.Value callback(List<ext.Value> arguments)) =>
111-
BuiltInCallable(
117+
BuiltInCallable.function(
112118
name, arguments, (arguments) => callback(arguments) as Value);
113119
}

lib/src/callable/async.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import '../value.dart';
1010
import '../value/external/value.dart' as ext;
1111
import 'async_built_in.dart';
1212

13-
/// An interface functions and mixins that can be invoked from Sass by passing
14-
/// in arguments.
13+
/// An interface for functions and mixins that can be invoked from Sass by
14+
/// passing in arguments.
1515
///
1616
/// This class represents callables that *need* to do asynchronous work. It's
1717
/// only compatible with the asynchonous `compile()` methods. If a callback can
@@ -23,16 +23,21 @@ abstract class AsyncCallable {
2323
/// The callable's name.
2424
String get name;
2525

26+
@Deprecated('Use `new AsyncCallable.function` instead.')
27+
factory AsyncCallable(String name, String arguments,
28+
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
29+
AsyncCallable.function(name, arguments, callback);
30+
2631
/// Creates a callable with the given [name] and [arguments] that runs
2732
/// [callback] when called.
2833
///
2934
/// The argument declaration is parsed from [arguments], which should not
3035
/// include parentheses. Throws a [SassFormatException] if parsing fails.
3136
///
3237
/// See [new Callable] for more details.
33-
factory AsyncCallable(String name, String arguments,
38+
factory AsyncCallable.function(String name, String arguments,
3439
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
35-
AsyncBuiltInCallable(name, arguments, (arguments) {
40+
AsyncBuiltInCallable.function(name, arguments, (arguments) {
3641
var result = callback(arguments);
3742
if (result is ext.Value) return result as Value;
3843
return (result as Future<ext.Value>).then((value) => value as Value);

lib/src/callable/async_built_in.dart

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,49 @@ typedef _Callback = FutureOr<Value> Function(List<Value> arguments);
2222
class AsyncBuiltInCallable implements AsyncCallable {
2323
final String name;
2424

25-
/// The overloads declared for this callable.
26-
final _overloads = <Tuple2<ArgumentDeclaration, _Callback>>[];
25+
/// This callable's arguments.
26+
final ArgumentDeclaration _arguments;
2727

28-
/// Creates a callable with a single [arguments] declaration and a single
28+
/// The callback to run when executing this callable.
29+
final _Callback _callback;
30+
31+
/// Creates a function with a single [arguments] declaration and a single
2932
/// [callback].
3033
///
3134
/// The argument declaration is parsed from [arguments], which should not
3235
/// include parentheses. Throws a [SassFormatException] if parsing fails.
33-
AsyncBuiltInCallable(String name, String arguments,
34-
FutureOr<Value> callback(List<Value> arguments))
35-
: this.parsed(name, ArgumentDeclaration.parse(arguments), callback);
36+
///
37+
/// If passed, [url] is the URL of the module in which the function is
38+
/// defined.
39+
AsyncBuiltInCallable.function(String name, String arguments,
40+
FutureOr<Value> callback(List<Value> arguments), {Object url})
41+
: this.parsed(
42+
name,
43+
ArgumentDeclaration.parse('@function $name($arguments) {',
44+
url: url),
45+
callback);
3646

37-
/// Creates a callable with a single [arguments] declaration and a single
47+
/// Creates a mixin with a single [arguments] declaration and a single
3848
/// [callback].
39-
AsyncBuiltInCallable.parsed(this.name, ArgumentDeclaration arguments,
40-
FutureOr<Value> callback(List<Value> arguments)) {
41-
_overloads.add(Tuple2(arguments, callback));
42-
}
43-
44-
/// Creates a callable with multiple implementations.
4549
///
46-
/// Each key/value pair in [overloads] defines the argument declaration for
47-
/// the overload (which should not include parentheses), and the callback to
48-
/// execute if that argument declaration matches. Throws a
49-
/// [SassFormatException] if parsing fails.
50-
AsyncBuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads) {
51-
overloads.forEach((arguments, callback) {
52-
_overloads.add(Tuple2(ArgumentDeclaration.parse(arguments), callback));
53-
});
54-
}
50+
/// The argument declaration is parsed from [arguments], which should not
51+
/// include parentheses. Throws a [SassFormatException] if parsing fails.
52+
///
53+
/// If passed, [url] is the URL of the module in which the mixin is
54+
/// defined.
55+
AsyncBuiltInCallable.mixin(String name, String arguments,
56+
FutureOr<void> callback(List<Value> arguments),
57+
{Object url})
58+
: this.parsed(name,
59+
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
60+
(arguments) async {
61+
await callback(arguments);
62+
return null;
63+
});
64+
65+
/// Creates a callable with a single [arguments] declaration and a single
66+
/// [callback].
67+
AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback);
5568

5669
/// Returns the argument declaration and Dart callback for the given
5770
/// positional and named arguments.
@@ -60,28 +73,6 @@ class AsyncBuiltInCallable implements AsyncCallable {
6073
/// doesn't guarantee that [positional] and [names] are valid for the returned
6174
/// [ArgumentDeclaration].
6275
Tuple2<ArgumentDeclaration, _Callback> callbackFor(
63-
int positional, Set<String> names) {
64-
Tuple2<ArgumentDeclaration, _Callback> fuzzyMatch;
65-
int minMismatchDistance;
66-
67-
for (var overload in _overloads) {
68-
// Ideally, find an exact match.
69-
if (overload.item1.matches(positional, names)) return overload;
70-
71-
var mismatchDistance = overload.item1.arguments.length - positional;
72-
73-
if (minMismatchDistance != null) {
74-
if (mismatchDistance.abs() > minMismatchDistance.abs()) continue;
75-
// If two overloads have the same mismatch distance, favor the overload
76-
// that has more arguments.
77-
if (mismatchDistance.abs() == minMismatchDistance.abs() &&
78-
mismatchDistance < 0) continue;
79-
}
80-
81-
minMismatchDistance = mismatchDistance;
82-
fuzzyMatch = overload;
83-
}
84-
85-
return fuzzyMatch;
86-
}
76+
int positional, Set<String> names) =>
77+
Tuple2(_arguments, _callback);
8778
}

0 commit comments

Comments
 (0)