| // 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 packageLocMatcher = new RegExp( | |
| r'^package:([^\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 | |
| /// package:a/b.dart:13 - line 13, "b.dart" in package "a". | |
| /// package:a/b.dart:13:20 - line 13, col 20, "b.dart" in package "a". | |
| /// 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 = packageLocMatcher.firstMatch(locDesc); | |
| if (match != null) { | |
| return _parseScriptLine(debugger, match, package: true); | |
| } | |
| 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] as Frame?; | |
| } | |
| 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, { | |
| bool package = false, | |
| }) async { | |
| var scriptName = match[1]; | |
| if (package) { | |
| scriptName = "package:$scriptName"; | |
| } | |
| if (scriptName != null) { | |
| scriptName = scriptName.substring(0, scriptName.length - 1); | |
| } | |
| var lineStr = match[2]; | |
| assert(lineStr != null); | |
| var colStr = match[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. | |
| Set<Script> scripts = await _lookupScript(debugger.isolate, scriptName); | |
| if (scripts.length == 0) { | |
| scripts = await _lookupScript( | |
| debugger.isolate, | |
| scriptName, | |
| useUri: true, | |
| ); | |
| } | |
| if (scripts.length == 0) { | |
| return new DebuggerLocation.error("Script '${scriptName}' not found"); | |
| } else if (scripts.length == 1) { | |
| return new DebuggerLocation.file(scripts.single, 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<Set<Script>> _lookupScript( | |
| Isolate isolate, | |
| String name, { | |
| bool allowPrefix = false, | |
| bool useUri = 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) { | |
| final String haystack = useUri ? script.uri : script.name!; | |
| if (allowPrefix) { | |
| if (haystack.startsWith(name)) { | |
| matches.add(script); | |
| } | |
| } else { | |
| if (name == haystack) { | |
| 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 { | |
| 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[1]!; | |
| var qualifier = match[2]; | |
| 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[0] == function.name) { | |
| functions.add(function); | |
| } | |
| } else { | |
| if (functionName == function.name) { | |
| functions.add(function); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (functions.length == 0) { | |
| return new DebuggerLocation.error("Function '${match[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[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[1] ?? ''; | |
| var qualifier = match[2]; | |
| 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[0]!)) { | |
| completions.add(function.name!); | |
| } | |
| } else { | |
| if (function.qualifiedName!.startsWith(match[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[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[1] ?? ''; | |
| if (lineStr.endsWith(':')) { | |
| lineStr = lineStr.substring(0, lineStr.length - 1); | |
| lineStrComplete = true; | |
| } | |
| colStr = match[2] ?? ''; | |
| } else { | |
| // CASE 2: We have matched a prefix of (scriptName:)(lineStr)(:colStr) | |
| scriptName = match[1] ?? ''; | |
| if (scriptName.endsWith(':')) { | |
| scriptName = scriptName.substring(0, scriptName.length - 1); | |
| scriptNameComplete = true; | |
| } | |
| lineStr = match[2] ?? ''; | |
| colStr = match[3] ?? ''; | |
| 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.first; | |
| await script.load(); | |
| if (!lineStrComplete) { | |
| // Complete the line. | |
| var sharedPrefix = '${script.name}:'; | |
| var completions = <String>[]; | |
| var report = | |
| await script.isolate!.getSourceReport([ | |
| Isolate.kPossibleBreakpointsReport, | |
| ], script) | |
| as ServiceMap; | |
| 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); | |
| } |