blob: f14d87b14b22c7bb1e2cf4691b7937a558bf4234 [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 SourceLocation {
SourceLocation.file(this.script, this.line, this.col);
SourceLocation.func(this.function);
SourceLocation.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<SourceLocation> 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 SourceLocation.error(
"Invalid source location '${locDesc}'"));
}
static Future<SourceLocation> _currentLocation(Debugger debugger) {
ServiceMap stack = debugger.stack;
if (stack == null || stack['frames'].length == 0) {
return new Future.value(new SourceLocation.error(
'A script must be provided when the stack is empty'));
}
var frame = stack['frames'][debugger.currentFrame];
Script script = frame['script'];
return script.load().then((_) {
var line = script.tokenToLine(frame['tokenPos']);
// TODO(turnidge): Pass in the column here once the protocol supports it.
return new Future.value(new SourceLocation.file(script, line, null));
});
}
static Future<SourceLocation> _parseScriptLine(Debugger debugger,
Match match) {
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 SourceLocation.error(
"Line '${lineStr}' must be an integer"));
}
if (col == -1) {
return new Future.value(new SourceLocation.error(
"Column '${colStr}' must be an integer"));
}
if (scriptName != null) {
// Resolve the script.
return _lookupScript(debugger.isolate, scriptName).then((scripts) {
if (scripts.length == 0) {
return new SourceLocation.error("Script '${scriptName}' not found");
} else if (scripts.length == 1) {
return new SourceLocation.file(scripts[0], line, col);
} else {
// TODO(turnidge): Allow the user to disambiguate.
return new SourceLocation.error("Script '${scriptName}' is ambigous");
}
});
} else {
// No script provided. Default to top of stack for now.
ServiceMap stack = debugger.stack;
if (stack == null || stack['frames'].length == 0) {
return new Future.value(new SourceLocation.error(
'A script must be provided when the stack is empty'));
}
Script script = stack['frames'][0]['script'];
return new Future.value(new SourceLocation.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 }) {
var pending = [];
for (var lib in isolate.libraries) {
assert(lib.loaded);
for (var cls in lib.classes) {
if (!cls.loaded) {
pending.add(cls.load());
}
}
}
return Future.wait(pending).then((_) {
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) {
var matches = [];
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<SourceLocation> _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 SourceLocation.error(
"Function '${match.group(0)}' not found");
} else if (functions.length == 1) {
return new SourceLocation.func(functions[0]);
} else {
// TODO(turnidge): Allow the user to disambiguate.
return new SourceLocation.error(
"Function '${match.group(0)}' is ambigous");
}
return new SourceLocation.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 Future<List<String>> _completeFile(Debugger debugger, Match match) {
var scriptName = match.group(1);
var lineStr = match.group(2);
var colStr = match.group(3);
if (lineStr != null || colStr != null) {
// TODO(turnidge): Complete valid line and column numbers.
return new Future.value([]);
}
scriptName = (scriptName == null ? '' : scriptName);
return _lookupScript(debugger.isolate, scriptName, allowPrefix:true)
.then((scripts) {
List completions = [];
for (var script in scripts) {
completions.add(script.name + ':');
}
completions.sort();
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);
}