Skip to content

Commit 4c0c6b4

Browse files
committed
Merge branch 'master' into math-functions
2 parents 75cab86 + 5e29ea6 commit 4c0c6b4

30 files changed

+1010
-442
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727

2828
* Add the variables `$pi` and `$e` to the built-in "sass:math" module.
2929

30+
## 1.24.5
31+
32+
* Highlight contextually-relevant sections of the stylesheet in error messages,
33+
rather than only highlighting the section where the error was detected.
34+
3035
## 1.24.4
3136

3237
### JavaScript API

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+
/// A number 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 still 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

0 commit comments

Comments
 (0)