| // 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 points of the characters in the 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 [new SourceFile.fromString] instead. |
| @Deprecated("Will be removed in 2.0.0") |
| SourceFile(String text, {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, {url}) |
| : this.decoded(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, {url}) |
| : url = url is String ? Uri.parse(url) : url, |
| _decodedChars = Uint32List.fromList(decodedChars.toList()) { |
| for (var i = 0; i < _decodedChars.length; i++) { |
| var c = _decodedChars[i]; |
| if (c == _CR) { |
| // Return not followed by newline is treated as a newline |
| var j = i + 1; |
| if (j >= _decodedChars.length || _decodedChars[j] != _LF) c = _LF; |
| } |
| if (c == _LF) _lineStarts.add(i + 1); |
| } |
| } |
| |
| /// Returns a span in [this] from [start] to [end] (exclusive). |
| /// |
| /// If [end] isn't passed, it defaults to the end of the file. |
| FileSpan span(int start, [int end]) { |
| if (end == null) end = length; |
| return _FileSpan(this, start, end); |
| } |
| |
| /// Returns a location in [this] 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; |
| |
| // 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++; |
| 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) { |
| int min = 0; |
| int max = _lineStarts.length - 1; |
| while (min < max) { |
| var 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."); |
| } |
| |
| var 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]) { |
| if (column == null) 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."); |
| } |
| |
| var 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, 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; |
| |
| final int offset; |
| Uri get sourceUrl => file.url; |
| int get line => file.getLine(offset); |
| 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}."); |
| } |
| } |
| |
| 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]. [FileSpan.message] is |
| /// also able to provide more context then [SourceSpan.message], and |
| /// [FileSpan.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; |
| |
| FileLocation get start; |
| 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 { |
| 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; |
| |
| Uri get sourceUrl => file.url; |
| int get length => _end - _start; |
| FileLocation get start => FileLocation._(file, _start); |
| FileLocation get end => FileLocation._(file, _end); |
| String get text => file.getText(_start, _end); |
| |
| String get context { |
| var endLine = file.getLine(_end); |
| var 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."); |
| } |
| } |
| |
| int compareTo(SourceSpan other) { |
| if (other is! _FileSpan) return super.compareTo(other); |
| |
| _FileSpan otherFile = other; |
| var result = _start.compareTo(otherFile._start); |
| return result == 0 ? _end.compareTo(otherFile._end) : result; |
| } |
| |
| SourceSpan union(SourceSpan other) { |
| if (other is! FileSpan) return super.union(other); |
| |
| _FileSpan span = expand(other); |
| |
| if (other is _FileSpan) { |
| if (this._start > other._end || other._start > this._end) { |
| throw ArgumentError("Spans $this and $other are disjoint."); |
| } |
| } else { |
| if (this._start > other.end.offset || other.start.offset > this._end) { |
| throw ArgumentError("Spans $this and $other are disjoint."); |
| } |
| } |
| |
| return span; |
| } |
| |
| bool operator ==(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; |
| } |
| |
| // Eliminates dart2js warning about overriding `==`, but not `hashCode` |
| int get hashCode => super.hashCode; |
| |
| /// 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) { |
| if (sourceUrl != other.sourceUrl) { |
| throw ArgumentError("Source URLs \"${sourceUrl}\" and " |
| " \"${other.sourceUrl}\" don't match."); |
| } |
| |
| if (other is _FileSpan) { |
| var start = math.min(this._start, other._start); |
| var end = math.max(this._end, other._end); |
| return _FileSpan(file, start, end); |
| } else { |
| var start = math.min(this._start, other.start.offset); |
| var end = math.max(this._end, other.end.offset); |
| return _FileSpan(file, start, end); |
| } |
| } |
| } |