// Copyright (c) 2013, 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. /// Dart classes representing the souce spans and source files. library source_maps.span; import 'dart:math' show min, max; import 'src/utils.dart'; /// A simple class that describe a segment of source text. abstract class Span implements Comparable { /// The start location of this span. final Location start; /// The end location of this span, exclusive. final Location end; /// Url of the source (typically a file) containing this span. String get sourceUrl => start.sourceUrl; /// The length of this span, in characters. int get length => end.offset - start.offset; /// The source text for this span, if available. String get text; /// Whether [text] corresponds to an identifier symbol. final bool isIdentifier; Span(this.start, this.end, bool isIdentifier) : isIdentifier = isIdentifier != null ? isIdentifier : false { _checkRange(); } /// Creates a new span that is the union of two existing spans [start] and /// [end]. Note that the resulting span might contain some positions that were /// not in either of the original spans if [start] and [end] are disjoint. Span.union(Span start, Span end) : start = start.start, end = end.end, isIdentifier = false { _checkRange(); } void _checkRange() { if (start.offset < 0) throw new ArgumentError('start $start must be >= 0'); if (end.offset < start.offset) { throw new ArgumentError('end $end must be >= start $start'); } } /// Compares two spans. If the spans are not in the same source, this method /// generates an error. int compareTo(Span other) { int d = start.compareTo(other.start); return d == 0 ? end.compareTo(other.end) : d; } /// Gets the location in standard printed form `filename:line:column`, where /// line and column are adjusted by 1 to match the convention in editors. String get formatLocation => start.formatString; String getLocationMessage(String message, {bool useColors: false, String color}) { return '$formatLocation: $message'; } bool operator ==(Span other) => sourceUrl == other.sourceUrl && start == other.start && end == other.end; int get hashCode => sourceUrl.hashCode + start.offset + (31 * length); String toString() => '<$runtimeType: $start $end $formatLocation $text>'; } /// A location in the source text abstract class Location implements Comparable { /// Url of the source containing this span. String get sourceUrl; /// The offset of this location, 0-based. final int offset; /// The 0-based line in the source of this location. int get line; /// The 0-based column in the source of this location. int get column; Location(this.offset); /// Compares two locations. If the locations are not in the same source, this /// method generates an error. int compareTo(Location other) { if (sourceUrl != other.sourceUrl) { throw new ArgumentError('can only compare locations of the same source'); } return offset - other.offset; } bool operator ==(Location other) => sourceUrl == other.sourceUrl && offset == other.offset; int get hashCode => sourceUrl.hashCode + offset; String toString() => '(Location $offset)'; String get formatString => '$sourceUrl:${line + 1}:${column + 1}'; } /// Implementation of [Location] with fixed values given at allocation time. class FixedLocation extends Location { final String sourceUrl; final int line; final int column; FixedLocation(int offset, this.sourceUrl, this.line, this.column) : super(offset); } /// Implementation of [Span] where all the values are given at allocation time. class FixedSpan extends Span { /// The source text for this span, if available. final String text; /// Creates a span which starts and end in the same line. FixedSpan(String sourceUrl, int start, int line, int column, {String text: '', bool isIdentifier: false}) : text = text, super(new FixedLocation(start, sourceUrl, line, column), new FixedLocation(start + text.length, sourceUrl, line, column + text.length), isIdentifier); } /// [Location] with values computed from an underling [SourceFile]. class FileLocation extends Location { /// The source file containing this location. final SourceFile file; String get sourceUrl => file.url; int get line => file.getLine(offset); int get column => file.getColumn(line, offset); FileLocation(this.file, int offset): super(offset); } /// [Span] where values are computed from an underling [SourceFile]. class FileSpan extends Span { /// The source file containing this span. final SourceFile file; /// The source text for this span, if available. String get text => file.getText(start.offset, end.offset); factory FileSpan(SourceFile file, int start, [int end, bool isIdentifier = false]) { var startLoc = new FileLocation(file, start); var endLoc = end == null ? startLoc : new FileLocation(file, end); return new FileSpan.locations(startLoc, endLoc, isIdentifier); } FileSpan.locations(FileLocation start, FileLocation end, bool isIdentifier) : file = start.file, super(start, end, isIdentifier); /// Creates a new span that is the union of two existing spans [start] and /// [end]. Note that the resulting span might contain some positions that were /// not in either of the original spans if [start] and [end] are disjoint. FileSpan.union(FileSpan start, FileSpan end) : file = start.file, super.union(start, end) { if (start.file != end.file) { throw new ArgumentError('start and end must be from the same file'); } } String getLocationMessage(String message, {bool useColors: false, String color}) { return file.getLocationMessage(message, start.offset, end.offset, useColors: useColors, color: color); } } // Constants to determine end-of-lines. const int _LF = 10; const int _CR = 13; // Color constants used for generating messages. const String _RED_COLOR = '\u001b[31m'; const String _NO_COLOR = '\u001b[0m'; /// Stores information about a source file, to permit computation of the line /// and column. Also contains a nice default error message highlighting the code /// location. class SourceFile { /// Url where the source file is located. final String url; final List _lineStarts; final List _decodedChars; SourceFile(this.url, this._lineStarts, this._decodedChars); SourceFile.text(this.url, String text) : _lineStarts = [0], _decodedChars = text.runes.toList() { for (int i = 0; i < _decodedChars.length; i++) { var c = _decodedChars[i]; if (c == _CR) { // Return not followed by newline is treated as a newline int j = i + 1; if (j >= _decodedChars.length || _decodedChars[j] != _LF) { c = _LF; } } if (c == _LF) _lineStarts.add(i + 1); } } /// Returns a span in this [SourceFile] with the given offsets. Span span(int start, [int end, bool isIdentifier = false]) => new FileSpan(this, start, end, isIdentifier); /// Returns a location in this [SourceFile] with the given offset. Location location(int offset) => new FileLocation(this, offset); /// Gets the 0-based line corresponding to an offset. int getLine(int offset) => binarySearch(_lineStarts, (o) => o > offset) - 1; /// Gets the 0-based column corresponding to an offset. int getColumn(int line, int offset) { if (line < 0 || line >= _lineStarts.length) return 0; return offset - _lineStarts[line]; } /// Get the offset for a given line and column int getOffset(int line, int column) { if (line < 0) return getOffset(0, 0); if (line < _lineStarts.length) { return _lineStarts[line] + column; } else { return _decodedChars.length; } } /// Gets the text at the given offsets. String getText(int start, [int end]) => new String.fromCharCodes(_decodedChars.sublist(max(start, 0), end)); /// Create a pretty string representation from a span. String getLocationMessage(String message, int start, int end, {bool useColors: false, String color}) { // TODO(jmesserly): it would be more useful to pass in an object that // controls how the errors are printed. This method is a bit too smart. var line = getLine(start); var column = getColumn(line, start); var src = url == null ? '' : url; var msg = '$src:${line + 1}:${column + 1}: $message'; if (_decodedChars == null) { // We don't have any text to include, so exit. return msg; } var buf = new StringBuffer(msg); buf.write('\n'); // +1 for 0-indexing, +1 again to avoid the last line var textLine = getText(getOffset(line, 0), getOffset(line + 1, 0)); column = min(column, textLine.length - 1); int toColumn = min(column + end - start, textLine.length); if (useColors) { if (color == null) { color = _RED_COLOR; } buf.write(textLine.substring(0, column)); buf.write(color); buf.write(textLine.substring(column, toColumn)); buf.write(_NO_COLOR); buf.write(textLine.substring(toColumn)); } else { buf.write(textLine); if (textLine != '' && !textLine.endsWith('\n')) buf.write('\n'); } int i = 0; for (; i < column; i++) { buf.write(' '); } if (useColors) buf.write(color); for (; i < toColumn; i++) { buf.write('^'); } if (useColors) buf.write(_NO_COLOR); return buf.toString(); } } /// A convenience type to treat a code segment as if it were a separate /// [SourceFile]. A [SourceFileSegment] shifts all locations by an offset, which /// allows you to set source-map locations based on the locations relative to /// the start of the segment, but that get translated to absolute locations in /// the original source file. class SourceFileSegment extends SourceFile { final int _baseOffset; final int _baseLine; final int _baseColumn; final int _maxOffset; SourceFileSegment(String url, String textSegment, Location startOffset) : _baseOffset = startOffset.offset, _baseLine = startOffset.line, _baseColumn = startOffset.column, _maxOffset = startOffset.offset + textSegment.length, super.text(url, textSegment); /// Craete a span, where [start] is relative to this segment's base offset. /// The returned span stores the real offset on the file, so that error /// messages are reported at the real location. Span span(int start, [int end, bool isIdentifier = false]) => super.span(start + _baseOffset, end == null ? null : end + _baseOffset, isIdentifier); /// Create a location, where [offset] relative to this segment's base offset. /// The returned span stores the real offset on the file, so that error /// messages are reported at the real location. Location location(int offset) => super.location(offset + _baseOffset); /// Return the line on the underlying file associated with the [offset] of the /// underlying file. This method operates on the real offsets from the /// original file, so that error messages can be reported accurately. When the /// requested offset is past the length of the segment, this returns the line /// number after the end of the segment (total lines + 1). int getLine(int offset) { var res = super.getLine(max(offset - _baseOffset, 0)) + _baseLine; return (offset > _maxOffset) ? res + 1 : res; } /// Return the column on the underlying file associated with [line] and /// [offset], where [line] is absolute from the beginning of the underlying /// file. This method operates on the real offsets from the original file, so /// that error messages can be reported accurately. int getColumn(int line, int offset) { var col = super.getColumn(line - _baseLine, max(offset - _baseOffset, 0)); return line == _baseLine ? col + _baseColumn : col; } /// Return the offset associated with a line and column. This method operates /// on the real offsets from the original file, so that error messages can be /// reported accurately. int getOffset(int line, int column) => super.getOffset(line - _baseLine, line == _baseLine ? column - _baseColumn : column) + _baseOffset; /// Retrieve the text associated with the specified range. This method /// operates on the real offsets from the original file, so that error /// messages can be reported accurately. String getText(int start, [int end]) => super.getText(start - _baseOffset, end == null ? null : end - _baseOffset); }