blob: a03a875a58d76a0811f395b95e69e8aaf51a0372 [file] [log] [blame]
// 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 as Uri,
_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
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;
// 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) {
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 otherFile = other as _FileSpan;
final result = _start.compareTo(otherFile._start);
return result == 0 ? _end.compareTo(otherFile._end) : result;
}
@override
SourceSpan union(SourceSpan other) {
if (other is! FileSpan) return super.union(other);
final span = expand(other as _FileSpan);
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 ==(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`
@override
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.
@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);
}
}
}