blob: 7688712d829de62ea34252620bf810fc0e0b7398 [file] [log] [blame]
// 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.
part of debugger;
class DebuggerLocation {
DebuggerLocation.file(this.script, this.line, this.col);
DebuggerLocation.func(this.function);
DebuggerLocation.error(this.errorMessage);
static RegExp sourceLocMatcher = new RegExp(r'^([^\d:][^:]+:)?(\d+)(:\d+)?');
static RegExp functionMatcher = new RegExp(r'^([^.]+)([.][^.]+)?');
/// Parses a source location description.
///
/// Formats:
/// '' - current position
/// 13 - line 13, current script
/// 13:20 - line 13, col 20, current script
/// script.dart:13 - line 13, script.dart
/// script.dart:13:20 - line 13, col 20, script.dart
/// main - function
/// FormatException - constructor
/// _SHA1._updateHash - method
static Future<DebuggerLocation> parse(Debugger debugger, String locDesc) {
if (locDesc == '') {
// Special case: '' means return current location.
return _currentLocation(debugger);
}
// Parse the location description.
var match = sourceLocMatcher.firstMatch(locDesc);
if (match != null) {
return _parseScriptLine(debugger, match);
}
match = functionMatcher.firstMatch(locDesc);
if (match != null) {
return _parseFunction(debugger, match);
}
return new Future.value(new DebuggerLocation.error(
"Invalid source location '${locDesc}'"));
}
static Future<Frame> _currentFrame(Debugger debugger) async {
ServiceMap stack = debugger.stack;
if (stack == null || stack['frames'].length == 0) {
return null;
}
return stack['frames'][debugger.currentFrame];
}
static Future<DebuggerLocation> _currentLocation(Debugger debugger) async {
var frame = await _currentFrame(debugger);
if (frame == null) {
return new DebuggerLocation.error(
'A script must be provided when the stack is empty');
}
Script script = frame.location.script;
await script.load();
var line = script.tokenToLine(frame.location.tokenPos);
var col = script.tokenToCol(frame.location.tokenPos);
return new DebuggerLocation.file(script, line, col);
}
static Future<DebuggerLocation> _parseScriptLine(Debugger debugger,
Match match) async {
var scriptName = match.group(1);
if (scriptName != null) {
scriptName = scriptName.substring(0, scriptName.length - 1);
}
var lineStr = match.group(2);
assert(lineStr != null);
var colStr = match.group(3);
if (colStr != null) {
colStr = colStr.substring(1);
}
var line = int.parse(lineStr, onError:(_) => -1);
var col = (colStr != null
? int.parse(colStr, onError:(_) => -1)
: null);
if (line == -1) {
return new Future.value(new DebuggerLocation.error(
"Line '${lineStr}' must be an integer"));
}
if (col == -1) {
return new Future.value(new DebuggerLocation.error(
"Column '${colStr}' must be an integer"));
}
if (scriptName != null) {
// Resolve the script.
var scripts = await _lookupScript(debugger.isolate, scriptName);
if (scripts.length == 0) {
return new DebuggerLocation.error("Script '${scriptName}' not found");
} else if (scripts.length == 1) {
return new DebuggerLocation.file(scripts[0], line, col);
} else {
// TODO(turnidge): Allow the user to disambiguate.
return
new DebuggerLocation.error("Script '${scriptName}' is ambiguous");
}
} else {
// No script provided. Default to top of stack for now.
var frame = await _currentFrame(debugger);
if (frame == null) {
return new Future.value(new DebuggerLocation.error(
'A script must be provided when the stack is empty'));
}
Script script = frame.location.script;
await script.load();
return new DebuggerLocation.file(script, line, col);
}
}
static Future<List<Script>> _lookupScript(Isolate isolate,
String name,
{bool allowPrefix: false}) {
var pending = [];
for (var lib in isolate.libraries) {
if (!lib.loaded) {
pending.add(lib.load());
}
}
return Future.wait(pending).then((_) {
List matches = [];
for (var lib in isolate.libraries) {
for (var script in lib.scripts) {
if (allowPrefix) {
if (script.name.startsWith(name)) {
matches.add(script);
}
} else {
if (name == script.name) {
matches.add(script);
}
}
}
}
return matches;
});
}
static List<ServiceFunction> _lookupFunction(Isolate isolate,
String name,
{ bool allowPrefix: false }) {
var matches = [];
for (var lib in isolate.libraries) {
assert(lib.loaded);
for (var function in lib.functions) {
if (allowPrefix) {
if (function.name.startsWith(name)) {
matches.add(function);
}
} else {
if (name == function.name) {
matches.add(function);
}
}
}
}
return matches;
}
static Future<List<Class>> _lookupClass(Isolate isolate,
String name,
{ bool allowPrefix: false }) async {
if (isolate == null) {
return [];
}
var pending = [];
for (var lib in isolate.libraries) {
assert(lib.loaded);
for (var cls in lib.classes) {
if (!cls.loaded) {
pending.add(cls.load());
}
}
}
await Future.wait(pending);
var matches = [];
for (var lib in isolate.libraries) {
for (var cls in lib.classes) {
if (allowPrefix) {
if (cls.name.startsWith(name)) {
matches.add(cls);
}
} else {
if (name == cls.name) {
matches.add(cls);
}
}
}
}
return matches;
}
static ServiceFunction _getConstructor(Class cls, String name) {
for (var function in cls.functions) {
assert(cls.loaded);
if (name == function.name) {
return function;
}
}
return null;
}
// TODO(turnidge): This does not handle named functions which are
// inside of named functions, e.g. foo.bar.baz.
static Future<DebuggerLocation> _parseFunction(Debugger debugger,
Match match) {
Isolate isolate = debugger.isolate;
var base = match.group(1);
var qualifier = match.group(2);
assert(base != null);
return _lookupClass(isolate, base).then((classes) {
var functions = [];
if (qualifier == null) {
// Unqualified name is either a function or a constructor.
functions.addAll(_lookupFunction(isolate, base));
for (var cls in classes) {
// Look for a self-named constructor.
var constructor = _getConstructor(cls, cls.name);
if (constructor != null) {
functions.add(constructor);
}
}
} else {
// Qualified name.
var functionName = qualifier.substring(1);
for (var cls in classes) {
assert(cls.loaded);
for (var function in cls.functions) {
if (function.kind == FunctionKind.kConstructor) {
// Constructor names are class-qualified.
if (match.group(0) == function.name) {
functions.add(function);
}
} else {
if (functionName == function.name) {
functions.add(function);
}
}
}
}
}
if (functions.length == 0) {
return new DebuggerLocation.error(
"Function '${match.group(0)}' not found");
} else if (functions.length == 1) {
return new DebuggerLocation.func(functions[0]);
} else {
// TODO(turnidge): Allow the user to disambiguate.
return new DebuggerLocation.error(
"Function '${match.group(0)}' is ambiguous");
}
return new DebuggerLocation.error('foo');
});
}
static RegExp partialSourceLocMatcher =
new RegExp(r'^([^\d:]?[^:]+[:]?)?(\d+)?([:]\d*)?');
static RegExp partialFunctionMatcher = new RegExp(r'^([^.]*)([.][^.]*)?');
/// Completes a partial source location description.
static Future<List<String>> complete(Debugger debugger, String locDesc) {
List<Future<List<String>>> pending = [];
var match = partialFunctionMatcher.firstMatch(locDesc);
if (match != null) {
pending.add(_completeFunction(debugger, match));
}
match = partialSourceLocMatcher.firstMatch(locDesc);
if (match != null) {
pending.add(_completeFile(debugger, match));
}
return Future.wait(pending).then((List<List<String>> responses) {
var completions = [];
for (var response in responses) {
completions.addAll(response);
}
return completions;
});
}
static Future<List<String>> _completeFunction(Debugger debugger,
Match match) {
Isolate isolate = debugger.isolate;
var base = match.group(1);
var qualifier = match.group(2);
base = (base == null ? '' : base);
if (qualifier == null) {
return _lookupClass(isolate, base, allowPrefix:true).then((classes) {
var completions = [];
// Complete top-level function names.
var functions = _lookupFunction(isolate, base, allowPrefix:true);
var funcNames = functions.map((f) => f.name).toList();
funcNames.sort();
completions.addAll(funcNames);
// Complete class names.
var classNames = classes.map((f) => f.name).toList();
classNames.sort();
completions.addAll(classNames);
return completions;
});
} else {
return _lookupClass(isolate, base, allowPrefix:false).then((classes) {
var completions = [];
for (var cls in classes) {
for (var function in cls.functions) {
if (function.kind == FunctionKind.kConstructor) {
if (function.name.startsWith(match.group(0))) {
completions.add(function.name);
}
} else {
if (function.qualifiedName.startsWith(match.group(0))) {
completions.add(function.qualifiedName);
}
}
}
}
completions.sort();
return completions;
});
}
}
static bool _startsWithDigit(String s) {
return '0'.compareTo(s[0]) <= 0 && '9'.compareTo(s[0]) >= 0;
}
static Future<List<String>> _completeFile(
Debugger debugger, Match match) async {
var scriptName;
var scriptNameComplete = false;
var lineStr;
var lineStrComplete = false;
var colStr;
if (_startsWithDigit(match.group(1))) {
// CASE 1: We have matched a prefix of (lineStr:)(colStr)
var frame = await _currentFrame(debugger);
if (frame == null) {
return [];
}
scriptName = frame.location.script.name;
scriptNameComplete = true;
lineStr = match.group(1);
lineStr = (lineStr == null ? '' : lineStr);
if (lineStr.endsWith(':')) {
lineStr = lineStr.substring(0, lineStr.length - 1);
lineStrComplete = true;
}
colStr = match.group(2);
colStr = (colStr == null ? '' : colStr);
} else {
// CASE 2: We have matched a prefix of (scriptName:)(lineStr)(:colStr)
scriptName = match.group(1);
scriptName = (scriptName == null ? '' : scriptName);
if (scriptName.endsWith(':')) {
scriptName = scriptName.substring(0, scriptName.length - 1);
scriptNameComplete = true;
}
lineStr = match.group(2);
lineStr = (lineStr == null ? '' : lineStr);
colStr = match.group(3);
colStr = (colStr == null ? '' : colStr);
if (colStr.startsWith(':')) {
lineStrComplete = true;
colStr = colStr.substring(1);
}
}
if (!scriptNameComplete) {
// The script name is incomplete. Complete it.
var scripts =
await _lookupScript(debugger.isolate, scriptName, allowPrefix:true);
List completions = [];
for (var script in scripts) {
completions.add(script.name + ':');
}
completions.sort();
return completions;
} else {
// The script name is complete. Look it up.
var scripts =
await _lookupScript(debugger.isolate, scriptName, allowPrefix:false);
if (scripts.isEmpty) {
return [];
}
var script = scripts[0];
await script.load();
if (!lineStrComplete) {
// Complete the line.
var sharedPrefix = '${script.name}:';
List completions = [];
var report = await script.isolate.getSourceReport(
[Isolate.kPossibleBreakpointsReport], script);
Set<int> possibleBpts = getPossibleBreakpointLines(report, script);
for (var line in possibleBpts) {
var currentLineStr = line.toString();
if (currentLineStr.startsWith(lineStr)) {
completions.add('${sharedPrefix}${currentLineStr} ');
completions.add('${sharedPrefix}${currentLineStr}:');
}
}
return completions;
} else {
// Complete the column.
int lineNum = int.parse(lineStr);
var scriptLine = script.getLine(lineNum);
var sharedPrefix = '${script.name}:${lineStr}:';
List completions = [];
int maxCol = scriptLine.text.trimRight().runes.length;
for (int i = 1; i <= maxCol; i++) {
var currentColStr = i.toString();
if (currentColStr.startsWith(colStr)) {
completions.add('${sharedPrefix}${currentColStr} ');
}
}
return completions;
}
}
}
String toString() {
if (valid) {
if (function != null) {
return '${function.qualifiedName}';
} else if (col != null) {
return '${script.name}:${line}:${col}';
} else {
return '${script.name}:${line}';
}
}
return 'invalid source location (${errorMessage})';
}
Script script;
int line;
int col;
ServiceFunction function;
String errorMessage;
bool get valid => (errorMessage == null);
}