| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:math' as math; | |
| import 'dart:typed_data'; | |
| import 'location.dart'; | |
| import 'location_mixin.dart'; | |
| import 'span.dart'; | |
| import 'span_mixin.dart'; | |
| import 'span_with_context.dart'; | |
| // Constants to determine end-of-lines. | |
| const int _lf = 10; | |
| const int _cr = 13; | |
| /// A class representing a source file. | |
| /// | |
| /// This doesn't necessarily have to correspond to a file on disk, just a chunk | |
| /// of text usually with a URL associated with it. | |
| class SourceFile { | |
| /// The URL where the source file is located. | |
| /// | |
| /// This may be null, indicating that the URL is unknown or unavailable. | |
| final Uri? url; | |
| /// An array of offsets for each line beginning in the file. | |
| /// | |
| /// Each offset refers to the first character *after* the newline. If the | |
| /// source file has a trailing newline, the final offset won't actually be in | |
| /// the file. | |
| final _lineStarts = <int>[0]; | |
| /// The code units of the characters in the file. | |
| /// | |
| /// If this was constructed with the deprecated `SourceFile()` constructor, | |
| /// this will instead contain the code _points_ of the characters in the file | |
| /// (so characters above 2^16 are represented as individual integers rather | |
| /// than surrogate pairs). | |
| List<int> get codeUnits => _decodedChars; | |
| /// The code units of the characters in this file. | |
| final Uint32List _decodedChars; | |
| /// The length of the file in characters. | |
| int get length => _decodedChars.length; | |
| /// The number of lines in the file. | |
| int get lines => _lineStarts.length; | |
| /// The line that the offset fell on the last time [getLine] was called. | |
| /// | |
| /// In many cases, sequential calls to getLine() are for nearby, usually | |
| /// increasing offsets. In that case, we can find the line for an offset | |
| /// quickly by first checking to see if the offset is on the same line as the | |
| /// previous result. | |
| int? _cachedLine; | |
| /// This constructor is deprecated. | |
| /// | |
| /// Use [SourceFile.fromString] instead. | |
| @Deprecated('Will be removed in 2.0.0') | |
| SourceFile(String text, {Object? url}) : this.decoded(text.runes, url: url); | |
| /// Creates a new source file from [text]. | |
| /// | |
| /// [url] may be either a [String], a [Uri], or `null`. | |
| SourceFile.fromString(String text, {Object? url}) | |
| : this._fromList(text.codeUnits, url: url); | |
| /// Creates a new source file from a list of decoded code units. | |
| /// | |
| /// [url] may be either a [String], a [Uri], or `null`. | |
| /// | |
| /// Currently, if [decodedChars] contains characters larger than `0xFFFF`, | |
| /// they'll be treated as single characters rather than being split into | |
| /// surrogate pairs. **This behavior is deprecated**. For | |
| /// forwards-compatibility, callers should only pass in characters less than | |
| /// or equal to `0xFFFF`. | |
| SourceFile.decoded(Iterable<int> decodedChars, {Object? url}) | |
| : this._fromList(decodedChars.toList(), url: url); | |
| SourceFile._fromList(List<int> decodedChars, {Object? url}) | |
| : url = url is String ? Uri.parse(url) : url as Uri?, | |
| _decodedChars = Uint32List(decodedChars.length) { | |
| for (var i = 0; i < _decodedChars.length; i++) { | |
| var c = _decodedChars[i] = decodedChars[i]; | |
| if (c == _cr) { | |
| // Return not followed by newline is treated as a newline | |
| final j = i + 1; | |
| if (j >= decodedChars.length || decodedChars[j] != _lf) c = _lf; | |
| } | |
| if (c == _lf) _lineStarts.add(i + 1); | |
| } | |
| } | |
| /// Returns a span from [start] to [end] (exclusive). | |
| /// | |
| /// If [end] isn't passed, it defaults to the end of the file. | |
| FileSpan span(int start, [int? end]) { | |
| end ??= length; | |
| return _FileSpan(this, start, end); | |
| } | |
| /// Returns a location at [offset]. | |
| FileLocation location(int offset) => FileLocation._(this, offset); | |
| /// Gets the 0-based line corresponding to [offset]. | |
| int getLine(int offset) { | |
| if (offset < 0) { | |
| throw RangeError('Offset may not be negative, was $offset.'); | |
| } else if (offset > length) { | |
| throw RangeError('Offset $offset must not be greater than the number ' | |
| 'of characters in the file, $length.'); | |
| } | |
| if (offset < _lineStarts.first) return -1; | |
| if (offset >= _lineStarts.last) return _lineStarts.length - 1; | |
| if (_isNearCachedLine(offset)) return _cachedLine!; | |
| _cachedLine = _binarySearch(offset) - 1; | |
| return _cachedLine!; | |
| } | |
| /// Returns `true` if [offset] is near [_cachedLine]. | |
| /// | |
| /// Checks on [_cachedLine] and the next line. If it's on the next line, it | |
| /// updates [_cachedLine] to point to that. | |
| bool _isNearCachedLine(int offset) { | |
| if (_cachedLine == null) return false; | |
| final cachedLine = _cachedLine!; | |
| // See if it's before the cached line. | |
| if (offset < _lineStarts[cachedLine]) return false; | |
| // See if it's on the cached line. | |
| if (cachedLine >= _lineStarts.length - 1 || | |
| offset < _lineStarts[cachedLine + 1]) { | |
| return true; | |
| } | |
| // See if it's on the next line. | |
| if (cachedLine >= _lineStarts.length - 2 || | |
| offset < _lineStarts[cachedLine + 2]) { | |
| _cachedLine = cachedLine + 1; | |
| return true; | |
| } | |
| return false; | |
| } | |
| /// Binary search through [_lineStarts] to find the line containing [offset]. | |
| /// | |
| /// Returns the index of the line in [_lineStarts]. | |
| int _binarySearch(int offset) { | |
| var min = 0; | |
| var max = _lineStarts.length - 1; | |
| while (min < max) { | |
| final half = min + ((max - min) ~/ 2); | |
| if (_lineStarts[half] > offset) { | |
| max = half; | |
| } else { | |
| min = half + 1; | |
| } | |
| } | |
| return max; | |
| } | |
| /// Gets the 0-based column corresponding to [offset]. | |
| /// | |
| /// If [line] is passed, it's assumed to be the line containing [offset] and | |
| /// is used to more efficiently compute the column. | |
| int getColumn(int offset, {int? line}) { | |
| if (offset < 0) { | |
| throw RangeError('Offset may not be negative, was $offset.'); | |
| } else if (offset > length) { | |
| throw RangeError('Offset $offset must be not be greater than the ' | |
| 'number of characters in the file, $length.'); | |
| } | |
| if (line == null) { | |
| line = getLine(offset); | |
| } else if (line < 0) { | |
| throw RangeError('Line may not be negative, was $line.'); | |
| } else if (line >= lines) { | |
| throw RangeError('Line $line must be less than the number of ' | |
| 'lines in the file, $lines.'); | |
| } | |
| final lineStart = _lineStarts[line]; | |
| if (lineStart > offset) { | |
| throw RangeError('Line $line comes after offset $offset.'); | |
| } | |
| return offset - lineStart; | |
| } | |
| /// Gets the offset for a [line] and [column]. | |
| /// | |
| /// [column] defaults to 0. | |
| int getOffset(int line, [int? column]) { | |
| column ??= 0; | |
| if (line < 0) { | |
| throw RangeError('Line may not be negative, was $line.'); | |
| } else if (line >= lines) { | |
| throw RangeError('Line $line must be less than the number of ' | |
| 'lines in the file, $lines.'); | |
| } else if (column < 0) { | |
| throw RangeError('Column may not be negative, was $column.'); | |
| } | |
| final result = _lineStarts[line] + column; | |
| if (result > length || | |
| (line + 1 < lines && result >= _lineStarts[line + 1])) { | |
| throw RangeError("Line $line doesn't have $column columns."); | |
| } | |
| return result; | |
| } | |
| /// Returns the text of the file from [start] to [end] (exclusive). | |
| /// | |
| /// If [end] isn't passed, it defaults to the end of the file. | |
| String getText(int start, [int? end]) => | |
| String.fromCharCodes(_decodedChars.sublist(start, end)); | |
| } | |
| /// A [SourceLocation] within a [SourceFile]. | |
| /// | |
| /// Unlike the base [SourceLocation], [FileLocation] lazily computes its line | |
| /// and column values based on its offset and the contents of [file]. | |
| /// | |
| /// A [FileLocation] can be created using [SourceFile.location]. | |
| class FileLocation extends SourceLocationMixin implements SourceLocation { | |
| /// The [file] that `this` belongs to. | |
| final SourceFile file; | |
| @override | |
| final int offset; | |
| @override | |
| Uri? get sourceUrl => file.url; | |
| @override | |
| int get line => file.getLine(offset); | |
| @override | |
| int get column => file.getColumn(offset); | |
| FileLocation._(this.file, this.offset) { | |
| if (offset < 0) { | |
| throw RangeError('Offset may not be negative, was $offset.'); | |
| } else if (offset > file.length) { | |
| throw RangeError('Offset $offset must not be greater than the number ' | |
| 'of characters in the file, ${file.length}.'); | |
| } | |
| } | |
| @override | |
| FileSpan pointSpan() => _FileSpan(file, offset, offset); | |
| } | |
| /// A [SourceSpan] within a [SourceFile]. | |
| /// | |
| /// Unlike the base [SourceSpan], [FileSpan] lazily computes its line and column | |
| /// values based on its offset and the contents of [file]. [SourceSpan.message] | |
| /// is also able to provide more context then [SourceSpan.message], and | |
| /// [SourceSpan.union] will return a [FileSpan] if possible. | |
| /// | |
| /// A [FileSpan] can be created using [SourceFile.span]. | |
| abstract class FileSpan implements SourceSpanWithContext { | |
| /// The [file] that `this` belongs to. | |
| SourceFile get file; | |
| @override | |
| FileLocation get start; | |
| @override | |
| FileLocation get end; | |
| /// Returns a new span that covers both `this` and [other]. | |
| /// | |
| /// Unlike [union], [other] may be disjoint from `this`. If it is, the text | |
| /// between the two will be covered by the returned span. | |
| FileSpan expand(FileSpan other); | |
| } | |
| /// The implementation of [FileSpan]. | |
| /// | |
| /// This is split into a separate class so that `is _FileSpan` checks can be run | |
| /// to make certain operations more efficient. If we used `is FileSpan`, that | |
| /// would break if external classes implemented the interface. | |
| class _FileSpan extends SourceSpanMixin implements FileSpan { | |
| @override | |
| final SourceFile file; | |
| /// The offset of the beginning of the span. | |
| /// | |
| /// [start] is lazily generated from this to avoid allocating unnecessary | |
| /// objects. | |
| final int _start; | |
| /// The offset of the end of the span. | |
| /// | |
| /// [end] is lazily generated from this to avoid allocating unnecessary | |
| /// objects. | |
| final int _end; | |
| @override | |
| Uri? get sourceUrl => file.url; | |
| @override | |
| int get length => _end - _start; | |
| @override | |
| FileLocation get start => FileLocation._(file, _start); | |
| @override | |
| FileLocation get end => FileLocation._(file, _end); | |
| @override | |
| String get text => file.getText(_start, _end); | |
| @override | |
| String get context { | |
| final endLine = file.getLine(_end); | |
| final endColumn = file.getColumn(_end); | |
| int? endOffset; | |
| if (endColumn == 0 && endLine != 0) { | |
| // If [end] is at the very beginning of the line, the span covers the | |
| // previous newline, so we only want to include the previous line in the | |
| // context... | |
| if (length == 0) { | |
| // ...unless this is a point span, in which case we want to include the | |
| // next line (or the empty string if this is the end of the file). | |
| return endLine == file.lines - 1 | |
| ? '' | |
| : file.getText( | |
| file.getOffset(endLine), file.getOffset(endLine + 1)); | |
| } | |
| endOffset = _end; | |
| } else if (endLine == file.lines - 1) { | |
| // If the span covers the last line of the file, the context should go all | |
| // the way to the end of the file. | |
| endOffset = file.length; | |
| } else { | |
| // Otherwise, the context should cover the full line on which [end] | |
| // appears. | |
| endOffset = file.getOffset(endLine + 1); | |
| } | |
| return file.getText(file.getOffset(file.getLine(_start)), endOffset); | |
| } | |
| _FileSpan(this.file, this._start, this._end) { | |
| if (_end < _start) { | |
| throw ArgumentError('End $_end must come after start $_start.'); | |
| } else if (_end > file.length) { | |
| throw RangeError('End $_end must not be greater than the number ' | |
| 'of characters in the file, ${file.length}.'); | |
| } else if (_start < 0) { | |
| throw RangeError('Start may not be negative, was $_start.'); | |
| } | |
| } | |
| @override | |
| int compareTo(SourceSpan other) { | |
| if (other is! _FileSpan) return super.compareTo(other); | |
| final result = _start.compareTo(other._start); | |
| return result == 0 ? _end.compareTo(other._end) : result; | |
| } | |
| @override | |
| SourceSpan union(SourceSpan other) { | |
| if (other is! FileSpan) return super.union(other); | |
| final span = expand(other); | |
| if (other is _FileSpan) { | |
| if (_start > other._end || other._start > _end) { | |
| throw ArgumentError('Spans $this and $other are disjoint.'); | |
| } | |
| } else { | |
| if (_start > other.end.offset || other.start.offset > _end) { | |
| throw ArgumentError('Spans $this and $other are disjoint.'); | |
| } | |
| } | |
| return span; | |
| } | |
| @override | |
| bool operator ==(Object other) { | |
| if (other is! FileSpan) return super == other; | |
| if (other is! _FileSpan) { | |
| return super == other && sourceUrl == other.sourceUrl; | |
| } | |
| return _start == other._start && | |
| _end == other._end && | |
| sourceUrl == other.sourceUrl; | |
| } | |
| @override | |
| int get hashCode => Object.hash(_start, _end, sourceUrl); | |
| /// Returns a new span that covers both `this` and [other]. | |
| /// | |
| /// Unlike [union], [other] may be disjoint from `this`. If it is, the text | |
| /// between the two will be covered by the returned span. | |
| @override | |
| FileSpan expand(FileSpan other) { | |
| if (sourceUrl != other.sourceUrl) { | |
| throw ArgumentError('Source URLs "$sourceUrl" and ' | |
| " \"${other.sourceUrl}\" don't match."); | |
| } | |
| if (other is _FileSpan) { | |
| final start = math.min(_start, other._start); | |
| final end = math.max(_end, other._end); | |
| return _FileSpan(file, start, end); | |
| } else { | |
| final start = math.min(_start, other.start.offset); | |
| final end = math.max(_end, other.end.offset); | |
| return _FileSpan(file, start, end); | |
| } | |
| } | |
| /// See `SourceSpanExtension.subspan`. | |
| FileSpan subspan(int start, [int? end]) { | |
| RangeError.checkValidRange(start, end, length); | |
| if (start == 0 && (end == null || end == length)) return this; | |
| return file.span(_start + start, end == null ? _end : _start + end); | |
| } | |
| } | |
| // TODO(#52): Move these to instance methods in the next breaking release. | |
| /// Extension methods on the [FileSpan] API. | |
| extension FileSpanExtension on FileSpan { | |
| /// See `SourceSpanExtension.subspan`. | |
| FileSpan subspan(int start, [int? end]) { | |
| RangeError.checkValidRange(start, end, length); | |
| if (start == 0 && (end == null || end == length)) return this; | |
| final startOffset = this.start.offset; | |
| return file.span( | |
| startOffset + start, end == null ? this.end.offset : startOffset + end); | |
| } | |
| } |