| // 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); | |
| } |