| // 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.tryParse(lineStr) ?? -1; |
| var col = (colStr != null ? int.tryParse(colStr) ?? -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 = <Future>[]; |
| for (var lib in isolate.libraries) { |
| if (!lib.loaded) { |
| pending.add(lib.load()); |
| } |
| } |
| return Future.wait(pending).then((_) { |
| var matches = <Script>[]; |
| 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 = <ServiceFunction>[]; |
| 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 = <Future>[]; |
| 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 = <Class>[]; |
| 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 == M.FunctionKind.constructor) { |
| // 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"); |
| } |
| }); |
| } |
| |
| 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) { |
| var pending = <Future<List<String>>>[]; |
| 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 = <String>[]; |
| 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 = <String>[]; |
| |
| // 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 = <String>[]; |
| for (var cls in classes) { |
| for (var function in cls.functions) { |
| if (function.kind == M.FunctionKind.constructor) { |
| 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); |
| var completions = <String>[]; |
| 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}:'; |
| var completions = <String>[]; |
| 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}:'; |
| var completions = <String>[]; |
| 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); |
| } |