Line data Source code
1 : // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:math' as math;
6 : import 'dart:typed_data';
7 :
8 : import 'location.dart';
9 : import 'location_mixin.dart';
10 : import 'span.dart';
11 : import 'span_mixin.dart';
12 : import 'span_with_context.dart';
13 :
14 : // Constants to determine end-of-lines.
15 : const int _lf = 10;
16 : const int _cr = 13;
17 :
18 : /// A class representing a source file.
19 : ///
20 : /// This doesn't necessarily have to correspond to a file on disk, just a chunk
21 : /// of text usually with a URL associated with it.
22 : class SourceFile {
23 : /// The URL where the source file is located.
24 : ///
25 : /// This may be null, indicating that the URL is unknown or unavailable.
26 : final Uri? url;
27 :
28 : /// An array of offsets for each line beginning in the file.
29 : ///
30 : /// Each offset refers to the first character *after* the newline. If the
31 : /// source file has a trailing newline, the final offset won't actually be in
32 : /// the file.
33 : final _lineStarts = <int>[0];
34 :
35 : /// The code points of the characters in the file.
36 : final Uint32List _decodedChars;
37 :
38 : /// The length of the file in characters.
39 15 : int get length => _decodedChars.length;
40 :
41 : /// The number of lines in the file.
42 0 : int get lines => _lineStarts.length;
43 :
44 : /// The line that the offset fell on the last time [getLine] was called.
45 : ///
46 : /// In many cases, sequential calls to getLine() are for nearby, usually
47 : /// increasing offsets. In that case, we can find the line for an offset
48 : /// quickly by first checking to see if the offset is on the same line as the
49 : /// previous result.
50 : int? _cachedLine;
51 :
52 : /// This constructor is deprecated.
53 : ///
54 : /// Use [new SourceFile.fromString] instead.
55 0 : @Deprecated('Will be removed in 2.0.0')
56 0 : SourceFile(String text, {url}) : this.decoded(text.runes, url: url);
57 :
58 : /// Creates a new source file from [text].
59 : ///
60 : /// [url] may be either a [String], a [Uri], or `null`.
61 5 : SourceFile.fromString(String text, {url})
62 10 : : this.decoded(text.codeUnits, url: url);
63 :
64 : /// Creates a new source file from a list of decoded code units.
65 : ///
66 : /// [url] may be either a [String], a [Uri], or `null`.
67 : ///
68 : /// Currently, if [decodedChars] contains characters larger than `0xFFFF`,
69 : /// they'll be treated as single characters rather than being split into
70 : /// surrogate pairs. **This behavior is deprecated**. For
71 : /// forwards-compatibility, callers should only pass in characters less than
72 : /// or equal to `0xFFFF`.
73 5 : SourceFile.decoded(Iterable<int> decodedChars, {url})
74 5 : : url = url is String ? Uri.parse(url) : url as Uri?,
75 10 : _decodedChars = Uint32List.fromList(decodedChars.toList()) {
76 20 : for (var i = 0; i < _decodedChars.length; i++) {
77 10 : var c = _decodedChars[i];
78 5 : if (c == _cr) {
79 : // Return not followed by newline is treated as a newline
80 0 : final j = i + 1;
81 0 : if (j >= _decodedChars.length || _decodedChars[j] != _lf) c = _lf;
82 : }
83 5 : if (c == _lf) _lineStarts.add(i + 1);
84 : }
85 : }
86 :
87 : /// Returns a span from [start] to [end] (exclusive).
88 : ///
89 : /// If [end] isn't passed, it defaults to the end of the file.
90 5 : FileSpan span(int start, [int? end]) {
91 0 : end ??= length;
92 5 : return _FileSpan(this, start, end);
93 : }
94 :
95 : /// Returns a location at [offset].
96 0 : FileLocation location(int offset) => FileLocation._(this, offset);
97 :
98 : /// Gets the 0-based line corresponding to [offset].
99 0 : int getLine(int offset) {
100 0 : if (offset < 0) {
101 0 : throw RangeError('Offset may not be negative, was $offset.');
102 0 : } else if (offset > length) {
103 0 : throw RangeError('Offset $offset must not be greater than the number '
104 0 : 'of characters in the file, $length.');
105 : }
106 :
107 0 : if (offset < _lineStarts.first) return -1;
108 0 : if (offset >= _lineStarts.last) return _lineStarts.length - 1;
109 :
110 0 : if (_isNearCachedLine(offset)) return _cachedLine!;
111 :
112 0 : _cachedLine = _binarySearch(offset) - 1;
113 0 : return _cachedLine!;
114 : }
115 :
116 : /// Returns `true` if [offset] is near [_cachedLine].
117 : ///
118 : /// Checks on [_cachedLine] and the next line. If it's on the next line, it
119 : /// updates [_cachedLine] to point to that.
120 0 : bool _isNearCachedLine(int offset) {
121 0 : if (_cachedLine == null) return false;
122 0 : final cachedLine = _cachedLine!;
123 :
124 : // See if it's before the cached line.
125 0 : if (offset < _lineStarts[cachedLine]) return false;
126 :
127 : // See if it's on the cached line.
128 0 : if (cachedLine >= _lineStarts.length - 1 ||
129 0 : offset < _lineStarts[cachedLine + 1]) {
130 : return true;
131 : }
132 :
133 : // See if it's on the next line.
134 0 : if (cachedLine >= _lineStarts.length - 2 ||
135 0 : offset < _lineStarts[cachedLine + 2]) {
136 0 : _cachedLine = cachedLine + 1;
137 : return true;
138 : }
139 :
140 : return false;
141 : }
142 :
143 : /// Binary search through [_lineStarts] to find the line containing [offset].
144 : ///
145 : /// Returns the index of the line in [_lineStarts].
146 0 : int _binarySearch(int offset) {
147 : var min = 0;
148 0 : var max = _lineStarts.length - 1;
149 0 : while (min < max) {
150 0 : final half = min + ((max - min) ~/ 2);
151 0 : if (_lineStarts[half] > offset) {
152 : max = half;
153 : } else {
154 0 : min = half + 1;
155 : }
156 : }
157 :
158 : return max;
159 : }
160 :
161 : /// Gets the 0-based column corresponding to [offset].
162 : ///
163 : /// If [line] is passed, it's assumed to be the line containing [offset] and
164 : /// is used to more efficiently compute the column.
165 0 : int getColumn(int offset, {int? line}) {
166 0 : if (offset < 0) {
167 0 : throw RangeError('Offset may not be negative, was $offset.');
168 0 : } else if (offset > length) {
169 0 : throw RangeError('Offset $offset must be not be greater than the '
170 0 : 'number of characters in the file, $length.');
171 : }
172 :
173 : if (line == null) {
174 0 : line = getLine(offset);
175 0 : } else if (line < 0) {
176 0 : throw RangeError('Line may not be negative, was $line.');
177 0 : } else if (line >= lines) {
178 0 : throw RangeError('Line $line must be less than the number of '
179 0 : 'lines in the file, $lines.');
180 : }
181 :
182 0 : final lineStart = _lineStarts[line];
183 0 : if (lineStart > offset) {
184 0 : throw RangeError('Line $line comes after offset $offset.');
185 : }
186 :
187 0 : return offset - lineStart;
188 : }
189 :
190 : /// Gets the offset for a [line] and [column].
191 : ///
192 : /// [column] defaults to 0.
193 0 : int getOffset(int line, [int? column]) {
194 : column ??= 0;
195 :
196 0 : if (line < 0) {
197 0 : throw RangeError('Line may not be negative, was $line.');
198 0 : } else if (line >= lines) {
199 0 : throw RangeError('Line $line must be less than the number of '
200 0 : 'lines in the file, $lines.');
201 0 : } else if (column < 0) {
202 0 : throw RangeError('Column may not be negative, was $column.');
203 : }
204 :
205 0 : final result = _lineStarts[line] + column;
206 0 : if (result > length ||
207 0 : (line + 1 < lines && result >= _lineStarts[line + 1])) {
208 0 : throw RangeError("Line $line doesn't have $column columns.");
209 : }
210 :
211 : return result;
212 : }
213 :
214 : /// Returns the text of the file from [start] to [end] (exclusive).
215 : ///
216 : /// If [end] isn't passed, it defaults to the end of the file.
217 0 : String getText(int start, [int? end]) =>
218 0 : String.fromCharCodes(_decodedChars.sublist(start, end));
219 : }
220 :
221 : /// A [SourceLocation] within a [SourceFile].
222 : ///
223 : /// Unlike the base [SourceLocation], [FileLocation] lazily computes its line
224 : /// and column values based on its offset and the contents of [file].
225 : ///
226 : /// A [FileLocation] can be created using [SourceFile.location].
227 : class FileLocation extends SourceLocationMixin implements SourceLocation {
228 : /// The [file] that `this` belongs to.
229 : final SourceFile file;
230 :
231 : @override
232 : final int offset;
233 :
234 0 : @override
235 0 : Uri? get sourceUrl => file.url;
236 :
237 0 : @override
238 0 : int get line => file.getLine(offset);
239 :
240 0 : @override
241 0 : int get column => file.getColumn(offset);
242 :
243 0 : FileLocation._(this.file, this.offset) {
244 0 : if (offset < 0) {
245 0 : throw RangeError('Offset may not be negative, was $offset.');
246 0 : } else if (offset > file.length) {
247 0 : throw RangeError('Offset $offset must not be greater than the number '
248 0 : 'of characters in the file, ${file.length}.');
249 : }
250 : }
251 :
252 0 : @override
253 0 : FileSpan pointSpan() => _FileSpan(file, offset, offset);
254 : }
255 :
256 : /// A [SourceSpan] within a [SourceFile].
257 : ///
258 : /// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column
259 : /// values based on its offset and the contents of [file]. [SourceSpan.message]
260 : /// is also able to provide more context then [SourceSpan.message], and
261 : /// [SourceSpan.union] will return a [FileSpan] if possible.
262 : ///
263 : /// A [FileSpan] can be created using [SourceFile.span].
264 : abstract class FileSpan implements SourceSpanWithContext {
265 : /// The [file] that `this` belongs to.
266 : SourceFile get file;
267 :
268 : @override
269 : FileLocation get start;
270 :
271 : @override
272 : FileLocation get end;
273 :
274 : /// Returns a new span that covers both `this` and [other].
275 : ///
276 : /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
277 : /// between the two will be covered by the returned span.
278 : FileSpan expand(FileSpan other);
279 : }
280 :
281 : /// The implementation of [FileSpan].
282 : ///
283 : /// This is split into a separate class so that `is _FileSpan` checks can be run
284 : /// to make certain operations more efficient. If we used `is FileSpan`, that
285 : /// would break if external classes implemented the interface.
286 : class _FileSpan extends SourceSpanMixin implements FileSpan {
287 : @override
288 : final SourceFile file;
289 :
290 : /// The offset of the beginning of the span.
291 : ///
292 : /// [start] is lazily generated from this to avoid allocating unnecessary
293 : /// objects.
294 : final int _start;
295 :
296 : /// The offset of the end of the span.
297 : ///
298 : /// [end] is lazily generated from this to avoid allocating unnecessary
299 : /// objects.
300 : final int _end;
301 :
302 0 : @override
303 0 : Uri? get sourceUrl => file.url;
304 :
305 0 : @override
306 0 : int get length => _end - _start;
307 :
308 0 : @override
309 0 : FileLocation get start => FileLocation._(file, _start);
310 :
311 0 : @override
312 0 : FileLocation get end => FileLocation._(file, _end);
313 :
314 0 : @override
315 0 : String get text => file.getText(_start, _end);
316 :
317 0 : @override
318 : String get context {
319 0 : final endLine = file.getLine(_end);
320 0 : final endColumn = file.getColumn(_end);
321 :
322 : int? endOffset;
323 0 : if (endColumn == 0 && endLine != 0) {
324 : // If [end] is at the very beginning of the line, the span covers the
325 : // previous newline, so we only want to include the previous line in the
326 : // context...
327 :
328 0 : if (length == 0) {
329 : // ...unless this is a point span, in which case we want to include the
330 : // next line (or the empty string if this is the end of the file).
331 0 : return endLine == file.lines - 1
332 : ? ''
333 0 : : file.getText(
334 0 : file.getOffset(endLine), file.getOffset(endLine + 1));
335 : }
336 :
337 0 : endOffset = _end;
338 0 : } else if (endLine == file.lines - 1) {
339 : // If the span covers the last line of the file, the context should go all
340 : // the way to the end of the file.
341 0 : endOffset = file.length;
342 : } else {
343 : // Otherwise, the context should cover the full line on which [end]
344 : // appears.
345 0 : endOffset = file.getOffset(endLine + 1);
346 : }
347 :
348 0 : return file.getText(file.getOffset(file.getLine(_start)), endOffset);
349 : }
350 :
351 5 : _FileSpan(this.file, this._start, this._end) {
352 15 : if (_end < _start) {
353 0 : throw ArgumentError('End $_end must come after start $_start.');
354 20 : } else if (_end > file.length) {
355 0 : throw RangeError('End $_end must not be greater than the number '
356 0 : 'of characters in the file, ${file.length}.');
357 10 : } else if (_start < 0) {
358 0 : throw RangeError('Start may not be negative, was $_start.');
359 : }
360 : }
361 :
362 0 : @override
363 : int compareTo(SourceSpan other) {
364 0 : if (other is! _FileSpan) return super.compareTo(other);
365 :
366 0 : final result = _start.compareTo(other._start);
367 0 : return result == 0 ? _end.compareTo(other._end) : result;
368 : }
369 :
370 0 : @override
371 : SourceSpan union(SourceSpan other) {
372 0 : if (other is! FileSpan) return super.union(other);
373 :
374 0 : final span = expand(other);
375 :
376 0 : if (other is _FileSpan) {
377 0 : if (_start > other._end || other._start > _end) {
378 0 : throw ArgumentError('Spans $this and $other are disjoint.');
379 : }
380 : } else {
381 0 : if (_start > other.end.offset || other.start.offset > _end) {
382 0 : throw ArgumentError('Spans $this and $other are disjoint.');
383 : }
384 : }
385 :
386 : return span;
387 : }
388 :
389 0 : @override
390 : bool operator ==(other) {
391 0 : if (other is! FileSpan) return super == other;
392 0 : if (other is! _FileSpan) {
393 0 : return super == other && sourceUrl == other.sourceUrl;
394 : }
395 :
396 0 : return _start == other._start &&
397 0 : _end == other._end &&
398 0 : sourceUrl == other.sourceUrl;
399 : }
400 :
401 : // Eliminates dart2js warning about overriding `==`, but not `hashCode`
402 0 : @override
403 0 : int get hashCode => super.hashCode;
404 :
405 : /// Returns a new span that covers both `this` and [other].
406 : ///
407 : /// Unlike [union], [other] may be disjoint from `this`. If it is, the text
408 : /// between the two will be covered by the returned span.
409 0 : @override
410 : FileSpan expand(FileSpan other) {
411 0 : if (sourceUrl != other.sourceUrl) {
412 0 : throw ArgumentError('Source URLs \"$sourceUrl\" and '
413 0 : " \"${other.sourceUrl}\" don't match.");
414 : }
415 :
416 0 : if (other is _FileSpan) {
417 0 : final start = math.min(_start, other._start);
418 0 : final end = math.max(_end, other._end);
419 0 : return _FileSpan(file, start, end);
420 : } else {
421 0 : final start = math.min(_start, other.start.offset);
422 0 : final end = math.max(_end, other.end.offset);
423 0 : return _FileSpan(file, start, end);
424 : }
425 : }
426 :
427 : /// See `SourceSpanExtension.subspan`.
428 0 : FileSpan subspan(int start, [int? end]) {
429 0 : RangeError.checkValidRange(start, end, length);
430 0 : if (start == 0 && (end == null || end == length)) return this;
431 0 : return file.span(_start + start, end == null ? _end : _start + end);
432 : }
433 : }
434 :
435 : // TODO(#52): Move these to instance methods in the next breaking release.
436 : /// Extension methods on the [FileSpan] API.
437 : extension FileSpanExtension on FileSpan {
438 : /// See `SourceSpanExtension.subspan`.
439 0 : FileSpan subspan(int start, [int? end]) {
440 0 : RangeError.checkValidRange(start, end, length);
441 0 : if (start == 0 && (end == null || end == length)) return this;
442 :
443 0 : final startOffset = this.start.offset;
444 0 : return file.span(
445 0 : startOffset + start, end == null ? this.end.offset : startOffset + end);
446 : }
447 : }
|