| // Copyright (c) 2015, 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:async'; |
| import 'dart:math' as math; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:json_rpc_2/json_rpc_2.dart' as rpc; |
| import 'package:source_span/source_span.dart'; |
| |
| import 'breakpoint.dart'; |
| import 'class.dart'; |
| import 'library.dart'; |
| import 'object.dart'; |
| import 'scope.dart'; |
| import 'source_location.dart'; |
| import 'source_report.dart'; |
| |
| VMScriptRef newVMScriptRef(Scope scope, Map json) { |
| if (json == null) return null; |
| assert(json["type"] == "@Script" || json["type"] == "Script"); |
| return new VMScriptRef._(scope, json); |
| } |
| |
| VMScriptToken newVMScriptToken(String isolateId, String scriptId, |
| int position) { |
| if (position == null) return null; |
| return new VMScriptToken._(isolateId, scriptId, position); |
| } |
| |
| VMScriptToken newVMScriptTokenFromPosition(VMScriptRef script, int position) { |
| if (position == null) return null; |
| return new VMScriptToken._(script._scope.isolateId, script._id, position); |
| } |
| |
| /// A reference to a script in the Dart VM. |
| /// |
| /// A script contains information about the actual text of a library. Usually |
| /// there's only one script per library, but the `part` directive can produce |
| /// libraries made up of multiple scripts. |
| class VMScriptRef implements VMObjectRef { |
| final Scope _scope; |
| |
| /// The ID for script library, which is unique relative to its isolate. |
| final String _id; |
| |
| /// Whether [_id] is guaranteed to be the same for different VM service |
| /// script objects that refer to the same script. |
| final bool _fixedId; |
| |
| Uri get observatoryUrl => _scope.observatoryUrlFor(_id); |
| |
| /// The URI from which this script was loaded. |
| final Uri uri; |
| |
| VMScriptRef._(this._scope, Map json) |
| : _id = json["id"], |
| _fixedId = json["fixedId"] ?? false, |
| uri = Uri.parse(json["uri"]); |
| |
| Future<VMScript> load() async => |
| new VMScript._(_scope, await _scope.loadObject(_id)); |
| |
| /// Adds a breakpoint at [line] (and optionally [column]) in this script. |
| /// |
| /// Both [line] and [column] are 1-based. |
| Future<VMBreakpoint> addBreakpoint(int line, {int column}) async { |
| var params = {"scriptId": _id, "line": line}; |
| if (column != null) params["column"] = column; |
| |
| try { |
| var response = await _scope.sendRequest("addBreakpoint", params); |
| return newVMBreakpoint(_scope, response); |
| } on rpc.RpcException catch (error) { |
| // Error 102 indicates that the breakpoint couldn't be created. |
| if (error.code == 102) return null; |
| rethrow; |
| } |
| } |
| |
| /// Generates a set of reports tied to this script. |
| /// |
| /// If [includeCoverageReport] is `true`, the report includes code coverage |
| /// information via [VMSourceReportRange.hits] and |
| /// [VMSourceReportRange.misses] in [VMSourceReport.ranges]. Otherwise, |
| /// these properties are `null`. |
| /// |
| /// If [includePossibleBreakpoints] is `true`, the report includes a list of |
| /// token positions which correspond to possible breakpoints via |
| /// [VMSourceReportRange.possibleBreakpoints] in [VMSourceReport.ranges]. |
| /// Otherwise, [VMSourceReportRange.possibleBreakpoints] is `null`. |
| /// |
| /// If [forceCompile] is `true`, all functions in the range of the report |
| /// will be compiled. If `false`, functions that are never used may not appear |
| /// in [VMSourceReportRange.misses]. |
| /// Forcing compilation can cause a compilation error, which could terminate |
| /// the running Dart program. |
| /// |
| /// [location] can be provided to restrict analysis to a subrange of the |
| /// script. An [ArgumentError] is thrown if `location.end` is `null`. |
| Future<VMSourceReport> getSourceReport( |
| {bool includeCoverageReport: true, |
| bool includePossibleBreakpoints: true, |
| bool forceCompile: false, |
| VMSourceLocation location}) async { |
| var reports = <String>[]; |
| if (includeCoverageReport) reports.add('Coverage'); |
| if (includePossibleBreakpoints) reports.add('PossibleBreakpoints'); |
| |
| var params = <String, dynamic>{'scriptId': _id, 'reports': reports}; |
| if (forceCompile) params['forceCompile'] = true; |
| if (location != null) { |
| if (location.end == null) { |
| throw new ArgumentError.value( |
| location, 'location', 'location.end cannot be null.'); |
| } |
| params['tokenPos'] = location.token._position; |
| params['endTokenPos'] = location.end._position; |
| } |
| |
| var json = await _scope.sendRequest('getSourceReport', params); |
| return newSourceReport(_scope, json); |
| } |
| |
| bool operator ==(other) => other is VMScriptRef && |
| (_fixedId ? _id == other._id : super == other); |
| |
| int get hashCode => _fixedId ? _id.hashCode : super.hashCode; |
| |
| String toString() => uri.toString(); |
| } |
| |
| /// A script in the Dart VM. |
| class VMScript extends VMScriptRef implements VMObject { |
| final VMClassRef klass; |
| |
| final int size; |
| |
| /// The library that owns this script. |
| final VMLibraryRef library; |
| |
| /// The source code for this script. |
| /// |
| /// For certain built-in libraries, this may be reconstructed without source |
| /// comments. |
| final String source; |
| |
| /// A table encoding a mapping from token position to line and column. |
| /// |
| /// Each subarray consists of an int designating the line, followed by any |
| /// number of position-column pairs that represent the positions of tokens |
| /// known to the VM. |
| /// |
| /// Because this encodes all the known token positions, it's more efficient to |
| /// access than the representation used by a [SourceFile] as long as you're |
| /// looking up a known token boundary. |
| final List<List<int>> _tokenPositions; |
| |
| /// A source file that provides access to location and span information about |
| /// this script. |
| /// |
| /// This is generally less efficient than calling [sourceSpan] and |
| /// [sourceLocation] directly, and should only be used when you don't have |
| /// [VMSourceLocation] or [VMScriptToken] objects. |
| /// |
| /// Note that [SourceFile] uses 0-based lines and columns, whereas the rest of |
| /// this package uses 1-based lines and columns. |
| SourceFile get sourceFile { |
| _sourceFile ??= new SourceFile.fromString(source, url: uri); |
| return _sourceFile; |
| } |
| SourceFile _sourceFile; |
| |
| VMScript._(Scope scope, Map json) |
| : klass = newVMClassRef(scope, json["class"]), |
| size = json["size"], |
| library = newVMLibraryRef(scope, json["library"]), |
| source = json["source"], |
| _tokenPositions = DelegatingList.typed(json["tokenPosTable"]), |
| super._(scope, json); |
| |
| /// Returns a [FileSpan] representing the source covered by [location]. |
| /// |
| /// If [location] doesn't have a [VMSourceLocation.end] token, this will be a |
| /// point span. Note that [FileSpan] uses 0-based lines and columns, whereas |
| /// the rest of this package uses 1-based lines and columns. |
| /// |
| /// Throws an [ArgumentError] if [location] isn't for this script. |
| FileSpan sourceSpan(VMSourceLocation location) { |
| if (location.script._scope.isolateId != _scope.isolateId || |
| (_fixedId && location.script._id != _id)) { |
| throw new ArgumentError("SourceLocation isn't for this script."); |
| } |
| |
| var end = location.end ?? location.token; |
| return new _ScriptSpan(this, location.token._position, end._position); |
| } |
| |
| /// Returns a [FileLocation] representing the location indicated by [token]. |
| /// |
| /// Note that [FileLocation] uses 0-based lines and columns, whereas the rest |
| /// of this package uses 1-based lines and columns. |
| /// |
| /// Throws an [ArgumentError] if [token] isn't for this script. |
| FileLocation sourceLocation(VMScriptToken token) { |
| if (token._isolateId != _scope.isolateId || |
| (_fixedId && token._scriptId != _id)) { |
| throw new ArgumentError("Token isn't for this script."); |
| } |
| |
| return new _ScriptLocation(this, token._position); |
| } |
| |
| /// Binary searches [_tokenPositions] for the line and column information for |
| /// the token at [position]. |
| /// |
| /// This returns a `line, column` pair if the token is found, and throws a |
| /// [StateError] otherwise. |
| List<int> _lineAndColumn(int position) { |
| var min = 0; |
| var max = _tokenPositions.length; |
| while (min < max) { |
| var mid = min + ((max - min) >> 1); |
| |
| var row = _tokenPositions[mid]; |
| |
| if (row[1] > position) { |
| max = mid; |
| } else { |
| for (var i = 1; i < row.length; i += 2) { |
| if (row[i] == position) return [row.first, row[i + 1]]; |
| } |
| |
| min = mid + 1; |
| } |
| } |
| |
| // We only call this for positions that come from the VM, so we shouldn't |
| // ever actually reach this point. |
| throw new StateError("Couldn't find line and column for token $position in " |
| "$uri."); |
| } |
| } |
| |
| /// The location of a token in a Dart script. |
| /// |
| /// A token can be passed to [VMScript.sourceLocation] to get the line and |
| /// column information for the token. |
| class VMScriptToken { |
| /// The ID of this token's script's isolate. |
| final String _isolateId; |
| |
| /// The ID of this token's script. |
| final String _scriptId; |
| |
| /// This is deprecated. |
| /// |
| /// Use `VMScript.sourceLocation().offset` instead. |
| @Deprecated("Will be removed in 0.3.0.") |
| int get offset => _position; |
| final int _position; |
| |
| VMScriptToken._(this._isolateId, this._scriptId, this._position); |
| |
| String toString() => _position.toString(); |
| } |
| |
| /// An implementation of [FileLocation] based on a known token position. |
| class _ScriptLocation extends SourceLocationMixin implements FileLocation { |
| /// The script that produced this location. |
| final VMScript _script; |
| |
| /// The position of the token used to generate this location. |
| /// |
| /// This is opaque, but it can be used to look up the line and column |
| /// information. |
| final int _position; |
| |
| int get offset { |
| // TODO(nweiz): Make this more efficient when sdk#26390 is fixed. |
| _offset ??= _script.sourceFile.getOffset(line, column); |
| return _offset; |
| } |
| int _offset; |
| |
| SourceFile get file => _script.sourceFile; |
| |
| Uri get sourceUrl => _script.uri; |
| |
| int get line { |
| _ensureLineAndColumn(); |
| return _line; |
| } |
| int _line; |
| |
| int get column { |
| _ensureLineAndColumn(); |
| return _column; |
| } |
| int _column; |
| |
| _ScriptLocation(this._script, this._position); |
| |
| /// Ensures that [_line] and [_column] are set based on [_script]'s |
| /// information. |
| void _ensureLineAndColumn() { |
| if (_line != null) return null; |
| var result = _script._lineAndColumn(_position); |
| |
| // SourceLocation lines and columns are zero-based, the VM service's are |
| // 1-based. |
| _line = result.first - 1; |
| _column = result.last - 1; |
| } |
| |
| int compareTo(SourceLocation other) => |
| other is _ScriptLocation && other.sourceUrl == sourceUrl |
| ? _position - other._position |
| : super.compareTo(other); |
| |
| bool operator ==(other) => other is _ScriptLocation |
| ? _position == other._position && sourceUrl == other.sourceUrl |
| : super == other; |
| |
| FileSpan pointSpan() => |
| new _ScriptSpan(_script, _position, _position, this, this); |
| } |
| |
| /// An implementation of [FileSpan] based on known token positions. |
| class _ScriptSpan extends SourceSpanMixin implements FileSpan { |
| /// The script that produced this location. |
| final VMScript _script; |
| |
| SourceFile get file => _script.sourceFile; |
| |
| /// The position of the start token. |
| final int _startPosition; |
| |
| /// The position of the end token. |
| final int _endPosition; |
| |
| Uri get sourceUrl => _script.uri; |
| |
| int get length { |
| // Prefer checking just the lines and columns, since they're more efficient |
| // to access. |
| if (start.line == end.line) return end.column - start.column; |
| return end.offset - start.offset; |
| } |
| |
| FileLocation get start { |
| _start ??= new _ScriptLocation(_script, _startPosition); |
| return _start; |
| } |
| FileLocation _start; |
| |
| FileLocation get end { |
| _end ??= new _ScriptLocation(_script, _endPosition); |
| return _end; |
| } |
| FileLocation _end; |
| |
| String get text => _script.source.substring(start.offset, end.offset); |
| |
| String get context => file.getText(file.getOffset(start.line), |
| end.line == file.lines - 1 ? null : file.getOffset(end.line + 1)); |
| |
| _ScriptSpan(this._script, this._startPosition, this._endPosition, |
| [this._start, this._end]); |
| |
| int compareTo(SourceSpan other) { |
| if (other is! _ScriptSpan || other.sourceUrl != sourceUrl) { |
| return super.compareTo(other); |
| } |
| |
| _ScriptSpan otherFile = other; |
| var result = _startPosition.compareTo(otherFile._startPosition); |
| return result == 0 ? |
| _endPosition.compareTo(otherFile._endPosition) |
| : result; |
| } |
| |
| SourceSpan union(SourceSpan other) { |
| if (other is _ScriptSpan) { |
| var span = expand(other) as _ScriptSpan; |
| var beginSpan = span._startPosition == _startPosition ? this : other; |
| var endSpan = span._endPosition == _endPosition ? this : other; |
| |
| if (beginSpan._endPosition < endSpan._startPosition) { |
| throw new ArgumentError("Spans $this and $other are disjoint."); |
| } |
| |
| return span; |
| } |
| return super.union(other); |
| } |
| |
| bool operator ==(other) => other is _ScriptSpan |
| ? _startPosition == other._startPosition && |
| _endPosition == other._endPosition && |
| sourceUrl == other.sourceUrl |
| : super == other; |
| |
| FileSpan expand(FileSpan other) { |
| if (sourceUrl != other.sourceUrl) { |
| throw new ArgumentError("Source URLs \"${sourceUrl}\" and " |
| " \"${other.sourceUrl}\" don't match."); |
| } |
| |
| if (other is _ScriptSpan) { |
| var startPosition = math.min(this._startPosition, other._startPosition); |
| var endPosition = math.max(this._endPosition, other._endPosition); |
| return new _ScriptSpan(_script, startPosition, endPosition, |
| startPosition == this._startPosition ? this._start : other._start, |
| endPosition == this._endPosition ? this._end : other._end); |
| } else { |
| var start = math.min(this.start.offset, other.start.offset); |
| var end = math.max(this.end.offset, other.end.offset); |
| return file.span(start, end); |
| } |
| } |
| } |