Skip to content

Commit 9d68793

Browse files
authored
Make sure all source spans use interpolation maps (#2673)
`Scanner.spanFrom` doesn't respect Sass's `InterpolationMap`, so this changes all span accesses to go through `Parser.spanFrom` and `Parser.spanFromPosition`.
1 parent 20a59ee commit 9d68793

File tree

7 files changed

+195
-173
lines changed

7 files changed

+195
-173
lines changed

lib/src/interpolation_map.dart

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ final class InterpolationMap {
4444

4545
/// Maps [error]'s span in the string generated from this interpolation to its
4646
/// original source.
47+
///
48+
/// Returns [error] if its span is null, or if it's already been mapped.
4749
FormatException mapException(SourceSpanFormatException error) {
4850
var target = error.span;
4951
if (target == null) return error;
5052

5153
if (_interpolation.contents.isEmpty) {
54+
if (_isMapped(target)) return error;
5255
return SourceSpanFormatException(
5356
error.message,
5457
_interpolation.span,
@@ -57,6 +60,8 @@ final class InterpolationMap {
5760
}
5861

5962
var source = mapSpan(target);
63+
if (identical(source, target)) return error;
64+
6065
var startIndex = _indexInContents(target.start);
6166
var endIndex = _indexInContents(target.end);
6267

@@ -79,24 +84,36 @@ final class InterpolationMap {
7984

8085
/// Maps a span in the string generated from this interpolation to its
8186
/// original source.
82-
FileSpan mapSpan(SourceSpan target) => switch ((
83-
_mapLocation(target.start),
84-
_mapLocation(target.end),
85-
)) {
86-
(FileSpan start, FileSpan end) => start.expand(end),
87-
(FileSpan start, FileLocation end) => _interpolation.span.file.span(
88-
_expandInterpolationSpanLeft(start.start),
89-
end.offset,
90-
),
91-
(FileLocation start, FileSpan end) => _interpolation.span.file.span(
92-
start.offset,
93-
_expandInterpolationSpanRight(end.end),
94-
),
95-
(FileLocation start, FileLocation end) => _interpolation.span.file.span(
96-
start.offset,
97-
end.offset,
98-
),
99-
_ => throw '[BUG] Unreachable',
87+
///
88+
/// Returns [target] as-is if it's already been mapped.
89+
FileSpan mapSpan(SourceSpan target) {
90+
if (_isMapped(target)) return target as FileSpan;
91+
92+
return switch ((
93+
_mapLocation(target.start),
94+
_mapLocation(target.end),
95+
)) {
96+
(FileSpan start, FileSpan end) => start.expand(end),
97+
(FileSpan start, FileLocation end) => _interpolation.span.file.span(
98+
_expandInterpolationSpanLeft(start.start),
99+
end.offset,
100+
),
101+
(FileLocation start, FileSpan end) => _interpolation.span.file.span(
102+
start.offset,
103+
_expandInterpolationSpanRight(end.end),
104+
),
105+
(FileLocation start, FileLocation end) => _interpolation.span.file.span(
106+
start.offset,
107+
end.offset,
108+
),
109+
_ => throw '[BUG] Unreachable',
110+
};
111+
}
112+
113+
/// Returns whether [span] has already been mapped by this mapper.
114+
bool _isMapped(SourceSpan span) => switch (span) {
115+
FileSpan(:var file) => identical(file, _interpolation.span.file),
116+
_ => false,
100117
};
101118

102119
/// Maps a location in the string generated from this interpolation to its

lib/src/parse/css.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class CssParser extends ScssParser {
4646
super.silentComment();
4747
error(
4848
"Silent comments aren't allowed in plain CSS.",
49-
scanner.spanFrom(start),
49+
spanFrom(start),
5050
);
5151
}
5252

@@ -86,7 +86,7 @@ class CssParser extends ScssParser {
8686
/// Throws an error for a forbidden at-rule.
8787
Never _forbiddenAtRule(LineScannerState start) {
8888
almostAnyValue();
89-
error("This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start));
89+
error("This at-rule isn't allowed in plain CSS.", spanFrom(start));
9090
}
9191

9292
/// Consumes a plain-CSS `@import` rule that disallows interpolation.
@@ -128,8 +128,8 @@ class CssParser extends ScssParser {
128128
var modifiers = tryImportModifiers();
129129
expectStatementSeparator("@import rule");
130130
return ImportRule([
131-
StaticImport(url, scanner.spanFrom(urlStart), modifiers: modifiers),
132-
], scanner.spanFrom(start));
131+
StaticImport(url, spanFrom(urlStart), modifiers: modifiers),
132+
], spanFrom(start));
133133
}
134134

135135
ParenthesizedExpression parentheses() {
@@ -140,7 +140,7 @@ class CssParser extends ScssParser {
140140
_whitespace();
141141
var expression = expressionUntilComma();
142142
scanner.expectChar($rparen);
143-
return ParenthesizedExpression(expression, scanner.spanFrom(start));
143+
return ParenthesizedExpression(expression, spanFrom(start));
144144
}
145145

146146
Expression identifierLike() {
@@ -179,14 +179,14 @@ class CssParser extends ScssParser {
179179
if (_disallowedFunctionNames.contains(plain)) {
180180
error(
181181
"This function isn't allowed in plain CSS.",
182-
scanner.spanFrom(start),
182+
spanFrom(start),
183183
);
184184
}
185185

186186
return FunctionExpression(
187187
plain,
188-
ArgumentList(arguments, const {}, scanner.spanFrom(beforeArguments)),
189-
scanner.spanFrom(start),
188+
ArgumentList(arguments, const {}, spanFrom(beforeArguments)),
189+
spanFrom(start),
190190
);
191191
}
192192

lib/src/parse/parser.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,18 @@ class Parser {
669669
/// Like [scanner.spanFrom], but passes the span through [_interpolationMap]
670670
/// if it's available.
671671
@protected
672-
FileSpan spanFrom(LineScannerState state) {
673-
var span = scanner.spanFrom(state);
672+
FileSpan spanFrom(LineScannerState start, [LineScannerState? end]) {
673+
var span = scanner.spanFrom(start, end);
674+
return _interpolationMap == null
675+
? span
676+
: LazyFileSpan(() => _interpolationMap.mapSpan(span));
677+
}
678+
679+
/// Like [scanner.spanFromPosition], but passes the span through
680+
/// [_interpolationMap] if it's available.
681+
@protected
682+
FileSpan spanFromPosition(int start, [int? end]) {
683+
var span = scanner.spanFromPosition(start, end);
674684
return _interpolationMap == null
675685
? span
676686
: LazyFileSpan(() => _interpolationMap.mapSpan(span));
@@ -728,7 +738,10 @@ class Parser {
728738
var map = _interpolationMap;
729739
if (map == null) rethrow;
730740

731-
throwWithTrace(map.mapException(error), error, stackTrace);
741+
var mapped = map.mapException(error);
742+
if (identical(mapped, error)) rethrow;
743+
744+
throwWithTrace(mapped, error, stackTrace);
732745
}
733746
} on MultiSourceSpanFormatException catch (error, stackTrace) {
734747
var span = error.span as FileSpan;

lib/src/parse/sass.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class SassParser extends StylesheetParser {
5050
} while (buffer.trailingString.trimRight().endsWith(',') &&
5151
scanCharIf((char) => char.isNewline));
5252

53-
return buffer.interpolation(scanner.spanFrom(start));
53+
return buffer.interpolation(spanFrom(start));
5454
}
5555

5656
void expectStatementSeparator([String? name]) {
@@ -97,7 +97,7 @@ class SassParser extends StylesheetParser {
9797
next = scanner.peekChar();
9898
}
9999
var url = scanner.substring(start.position);
100-
var span = scanner.spanFrom(start);
100+
var span = spanFrom(start);
101101

102102
if (isPlainImportUrl(url)) {
103103
// Serialize [url] as a Sass string because [StaticImport] expects it to
@@ -218,7 +218,7 @@ class SassParser extends StylesheetParser {
218218

219219
return lastSilentComment = SilentComment(
220220
buffer.toString(),
221-
scanner.spanFrom(start),
221+
spanFrom(start),
222222
);
223223
}
224224

@@ -269,7 +269,7 @@ class SassParser extends StylesheetParser {
269269
if (scanner.peekChar(1) == $slash) {
270270
buffer.writeCharCode(scanner.readChar());
271271
buffer.writeCharCode(scanner.readChar());
272-
var span = scanner.spanFrom(start);
272+
var span = spanFrom(start);
273273
whitespace(consumeNewlines: false);
274274

275275
// For backwards compatibility, allow additional comments after
@@ -290,7 +290,7 @@ class SassParser extends StylesheetParser {
290290
}
291291
throw MultiSpanSassFormatException(
292292
"Unexpected text after end of comment",
293-
scanner.spanFrom(errorStart),
293+
spanFrom(errorStart),
294294
"extra text",
295295
{span: "comment"},
296296
);
@@ -318,7 +318,7 @@ class SassParser extends StylesheetParser {
318318
_readIndentation();
319319
}
320320

321-
return LoudComment(buffer.interpolation(scanner.spanFrom(start)));
321+
return LoudComment(buffer.interpolation(spanFrom(start)));
322322
}
323323

324324
void whitespaceWithoutComments({required bool consumeNewlines}) {

lib/src/parse/scss.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class ScssParser extends StylesheetParser {
5050
'versions.\n'
5151
'\n'
5252
'Recommendation: @else if',
53-
span: scanner.spanFrom(beforeAt),
53+
span: spanFrom(beforeAt),
5454
));
5555
scanner.position -= 2;
5656
return true;
@@ -140,13 +140,13 @@ class ScssParser extends StylesheetParser {
140140
if (plainCss) {
141141
error(
142142
"Silent comments aren't allowed in plain CSS.",
143-
scanner.spanFrom(start),
143+
spanFrom(start),
144144
);
145145
}
146146

147147
return lastSilentComment = SilentComment(
148148
scanner.substring(start.position),
149-
scanner.spanFrom(start),
149+
spanFrom(start),
150150
);
151151
}
152152

@@ -172,7 +172,7 @@ class ScssParser extends StylesheetParser {
172172
if (scanner.peekChar() != $slash) continue loop;
173173

174174
buffer.writeCharCode(scanner.readChar());
175-
return LoudComment(buffer.interpolation(scanner.spanFrom(start)));
175+
return LoudComment(buffer.interpolation(spanFrom(start)));
176176

177177
case $cr:
178178
scanner.readChar();

lib/src/parse/selector.dart

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ class SelectorParser extends Parser {
236236
if (_plainCss) {
237237
error(
238238
"Placeholder selectors aren't allowed in plain CSS.",
239-
scanner.spanFrom(start),
239+
spanFrom(start),
240240
);
241241
}
242242
return selector;
@@ -247,7 +247,7 @@ class SelectorParser extends Parser {
247247
if (!allowParent) {
248248
error(
249249
"Parent selectors aren't allowed here.",
250-
scanner.spanFrom(start),
250+
spanFrom(start),
251251
);
252252
}
253253
return selector;
@@ -316,7 +316,7 @@ class SelectorParser extends Parser {
316316

317317
/// Consumes an attribute selector's operator.
318318
AttributeOperator _attributeOperator() {
319-
var start = scanner.state;
319+
var start = scanner.position;
320320
switch (scanner.readChar()) {
321321
case $equal:
322322
return AttributeOperator.equal;
@@ -342,7 +342,7 @@ class SelectorParser extends Parser {
342342
return AttributeOperator.substring;
343343

344344
default:
345-
scanner.error('Expected "]".', position: start.position);
345+
scanner.error('Expected "]".', position: start);
346346
}
347347
}
348348

@@ -486,16 +486,12 @@ class SelectorParser extends Parser {
486486
return scanner.scanChar($asterisk)
487487
? UniversalSelector(spanFrom(start), namespace: "*")
488488
: TypeSelector(
489-
QualifiedName(identifier(), namespace: "*"),
490-
spanFrom(start),
491-
);
489+
QualifiedName(identifier(), namespace: "*"), spanFrom(start));
492490
} else if (scanner.scanChar($pipe)) {
493491
return scanner.scanChar($asterisk)
494492
? UniversalSelector(spanFrom(start), namespace: "")
495493
: TypeSelector(
496-
QualifiedName(identifier(), namespace: ""),
497-
spanFrom(start),
498-
);
494+
QualifiedName(identifier(), namespace: ""), spanFrom(start));
499495
}
500496

501497
var nameOrNamespace = identifier();
@@ -505,9 +501,8 @@ class SelectorParser extends Parser {
505501
return UniversalSelector(spanFrom(start), namespace: nameOrNamespace);
506502
} else {
507503
return TypeSelector(
508-
QualifiedName(identifier(), namespace: nameOrNamespace),
509-
spanFrom(start),
510-
);
504+
QualifiedName(identifier(), namespace: nameOrNamespace),
505+
spanFrom(start));
511506
}
512507
}
513508

0 commit comments

Comments
 (0)