|  | // Copyright (c) 2020, 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. | 
|  |  | 
|  | // @dart = 2.9 | 
|  |  | 
|  | import 'package:logging/logging.dart'; | 
|  | import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; | 
|  |  | 
|  | import '../debugging/dart_scope.dart'; | 
|  | import '../debugging/inspector.dart'; | 
|  | import '../debugging/location.dart'; | 
|  | import '../debugging/modules.dart'; | 
|  | import '../utilities/objects.dart' as chrome; | 
|  | import 'expression_compiler.dart'; | 
|  |  | 
|  | class ErrorKind { | 
|  | const ErrorKind._(this._kind); | 
|  |  | 
|  | final String _kind; | 
|  | static const ErrorKind compilation = ErrorKind._('CompilationError'); | 
|  | static const ErrorKind type = ErrorKind._('TypeError'); | 
|  | static const ErrorKind reference = ErrorKind._('ReferenceError'); | 
|  | static const ErrorKind internal = ErrorKind._('InternalError'); | 
|  | static const ErrorKind invalidInput = ErrorKind._('InvalidInputError'); | 
|  |  | 
|  | @override | 
|  | String toString() => _kind; | 
|  | } | 
|  |  | 
|  | /// ExpressionEvaluator provides functionality to evaluate dart expressions | 
|  | /// from text user input in the debugger, using chrome remote debugger to | 
|  | /// collect context for evaluation (scope, types, modules), and using | 
|  | /// ExpressionCompilerInterface to compile dart expressions to JavaScript. | 
|  | class ExpressionEvaluator { | 
|  | final AppInspector _inspector; | 
|  | final Locations _locations; | 
|  | final Modules _modules; | 
|  | final ExpressionCompiler _compiler; | 
|  | final _logger = Logger('ExpressionEvaluator'); | 
|  |  | 
|  | static final _syntheticNameFilterRegex = | 
|  | RegExp('org-dartlang-debug:synthetic_debug_expression:.*:.*Error: '); | 
|  |  | 
|  | ExpressionEvaluator( | 
|  | this._inspector, this._locations, this._modules, this._compiler); | 
|  |  | 
|  | RemoteObject _createError(ErrorKind severity, String message) { | 
|  | return RemoteObject( | 
|  | <String, String>{'type': '$severity', 'value': message}); | 
|  | } | 
|  |  | 
|  | /// Evaluate dart expression inside a given library. | 
|  | /// | 
|  | /// Uses ExpressionCompiler interface to compile the expression to | 
|  | /// JavaScript and sends evaluate requests to chrome to calculate | 
|  | /// the final result. | 
|  | /// | 
|  | /// Returns remote object containing the result of evaluation or error. | 
|  | /// | 
|  | /// [isolateId] current isolate ID. | 
|  | /// [libraryUri] dart library to evaluate the expression in. | 
|  | /// [expression] dart expression to evaluate. | 
|  | Future<RemoteObject> evaluateExpression( | 
|  | String isolateId, | 
|  | String libraryUri, | 
|  | String expression, | 
|  | Map<String, String> scope, | 
|  | ) async { | 
|  | if (_compiler == null) { | 
|  | return _createError(ErrorKind.internal, | 
|  | 'ExpressionEvaluator needs an ExpressionCompiler'); | 
|  | } | 
|  |  | 
|  | if (expression == null || expression.isEmpty) { | 
|  | return _createError(ErrorKind.invalidInput, expression); | 
|  | } | 
|  |  | 
|  | var module = await _modules.moduleForlibrary(libraryUri); | 
|  |  | 
|  | if (scope != null && scope.isNotEmpty) { | 
|  | var params = scope.keys.join(', '); | 
|  | expression = '($params) => $expression'; | 
|  | } | 
|  | _logger.finest('Evaluating "$expression" at $module'); | 
|  |  | 
|  | // Compile expression using an expression compiler, such as | 
|  | // frontend server or expression compiler worker. | 
|  | var compilationResult = await _compiler.compileExpressionToJs( | 
|  | isolateId, libraryUri.toString(), 0, 0, {}, {}, module, expression); | 
|  |  | 
|  | var isError = compilationResult.isError; | 
|  | var jsResult = compilationResult.result; | 
|  | if (isError) { | 
|  | return _formatCompilationError(jsResult); | 
|  | } | 
|  |  | 
|  | // Send JS expression to chrome to evaluate. | 
|  | RemoteObject result; | 
|  | if (scope != null && scope.isNotEmpty) { | 
|  | // Strip try/catch. | 
|  | // TODO: remove adding try/catch block in expression compiler. | 
|  | // https://github.com/dart-lang/webdev/issues/1341 | 
|  | var lines = jsResult.split('\n'); | 
|  | var inner = lines.getRange(2, lines.length - 3).join('\n'); | 
|  | var function = 'function(t) {' | 
|  | '  return $inner(t);' | 
|  | '}'; | 
|  | result = await _inspector.callFunction(function, scope.values); | 
|  | result = _formatEvaluationError(result); | 
|  | } else { | 
|  | result = await _inspector.debugger.evaluate(jsResult); | 
|  | result = _formatEvaluationError(result); | 
|  | } | 
|  |  | 
|  | _logger.finest('Evaluated "$expression" to "$result"'); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /// Evaluate dart expression inside a given frame (function). | 
|  | /// | 
|  | /// Gets necessary context (types, scope, module names) data from chrome, | 
|  | /// uses ExpressionCompiler interface to compile the expression to | 
|  | /// JavaScript, and sends evaluate requests to chrome to calculate the | 
|  | /// final result. | 
|  | /// | 
|  | /// Returns remote object containing the result of evaluation or error. | 
|  | /// | 
|  | /// [isolateId] current isolate ID. | 
|  | /// [frameIndex] JavaScript frame to evaluate the expression in. | 
|  | /// [expression] dart expression to evaluate. | 
|  | Future<RemoteObject> evaluateExpressionInFrame(String isolateId, | 
|  | int frameIndex, String expression, Map<String, String> scope) async { | 
|  | if (_compiler == null) { | 
|  | return _createError(ErrorKind.internal, | 
|  | 'ExpressionEvaluator needs an ExpressionCompiler'); | 
|  | } | 
|  |  | 
|  | if (expression == null || expression.isEmpty) { | 
|  | return _createError(ErrorKind.invalidInput, expression); | 
|  | } | 
|  |  | 
|  | // Get JS scope and current JS location. | 
|  | var jsFrame = _inspector.debugger.jsFrameForIndex(frameIndex); | 
|  | if (jsFrame == null) { | 
|  | return _createError( | 
|  | ErrorKind.internal, 'No frame with index $frameIndex'); | 
|  | } | 
|  |  | 
|  | var functionName = jsFrame.functionName; | 
|  | var jsLine = jsFrame.location.lineNumber + 1; | 
|  | var jsScope = await _collectLocalJsScope(jsFrame); | 
|  |  | 
|  | // Find corresponding dart location and scope. | 
|  | // | 
|  | // TODO(annagrin): handle unknown dart locations | 
|  | // Debugger does not map every js location to a dart location, | 
|  | // so this will result in expressions not evaluated in some | 
|  | // cases. Invent location matching strategy for those cases. | 
|  | // [issue 890](https://github.com/dart-lang/webdev/issues/890) | 
|  | var locationMap = await _locations.locationForJs(jsFrame.url, jsLine); | 
|  | if (locationMap == null) { | 
|  | return _createError( | 
|  | ErrorKind.internal, | 
|  | 'Cannot find Dart location for JS location: ' | 
|  | 'url: ${jsFrame.url}' | 
|  | 'function: $functionName, ' | 
|  | 'line: $jsLine'); | 
|  | } | 
|  |  | 
|  | var dartLocation = locationMap.dartLocation; | 
|  | var libraryUri = | 
|  | await _modules.libraryForSource(dartLocation.uri.serverPath); | 
|  |  | 
|  | var currentModule = | 
|  | await _modules.moduleForSource(dartLocation.uri.serverPath); | 
|  |  | 
|  | _logger.finest('Evaluating "$expression" at $currentModule, ' | 
|  | '$libraryUri:${dartLocation.line}:${dartLocation.column}'); | 
|  |  | 
|  | // Compile expression using an expression compiler, such as | 
|  | // frontend server or expression compiler worker. | 
|  | var compilationResult = await _compiler.compileExpressionToJs( | 
|  | isolateId, | 
|  | libraryUri.toString(), | 
|  | dartLocation.line, | 
|  | dartLocation.column, | 
|  | {}, | 
|  | jsScope, | 
|  | currentModule, | 
|  | expression); | 
|  |  | 
|  | var isError = compilationResult.isError; | 
|  | var jsResult = compilationResult.result; | 
|  | if (isError) { | 
|  | return _formatCompilationError(jsResult); | 
|  | } | 
|  |  | 
|  | // Send JS expression to chrome to evaluate. | 
|  | var result = await _inspector.debugger | 
|  | .evaluateJsOnCallFrameIndex(frameIndex, jsResult); | 
|  | result = _formatEvaluationError(result); | 
|  |  | 
|  | _logger.finest('Evaluated "$expression" to "$result"'); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | String _valueToLiteral(RemoteObject value) { | 
|  | if (value.value == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | var ret = value.value.toString(); | 
|  | if (value.type == 'string') { | 
|  | return '\'$ret\''; | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | RemoteObject _formatCompilationError(String error) { | 
|  | // Frontend currently gives a text message including library name | 
|  | // and function name on compilation error. Strip this information | 
|  | // since it shows syntetic names that are only used for temporary | 
|  | // debug library during expression evaluation. | 
|  | // | 
|  | // TODO(annagrin): modify frontend to avoid stripping dummy names | 
|  | // [issue 40449](https://github.com/dart-lang/sdk/issues/40449) | 
|  | if (error.startsWith('[')) { | 
|  | error = error.substring(1); | 
|  | } | 
|  | if (error.endsWith(']')) { | 
|  | error = error.substring(0, error.lastIndexOf(']')); | 
|  | } | 
|  | if (error.contains('InternalError: ')) { | 
|  | error = error.replaceAll('InternalError: ', ''); | 
|  | return _createError(ErrorKind.internal, error); | 
|  | } | 
|  | error = error.replaceAll(_syntheticNameFilterRegex, ''); | 
|  | return _createError(ErrorKind.compilation, error); | 
|  | } | 
|  |  | 
|  | RemoteObject _formatEvaluationError(RemoteObject result) { | 
|  | if (result.type == 'string') { | 
|  | var error = '${result.value}'; | 
|  | if (error.startsWith('ReferenceError: ')) { | 
|  | error = error.replaceFirst('ReferenceError: ', ''); | 
|  | return _createError(ErrorKind.reference, error); | 
|  | } | 
|  | if (error.startsWith('TypeError: ')) { | 
|  | error = error.replaceFirst('TypeError: ', ''); | 
|  | return _createError(ErrorKind.type, error); | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | Future<Map<String, String>> _collectLocalJsScope(WipCallFrame frame) async { | 
|  | var jsScope = <String, String>{}; | 
|  |  | 
|  | void collectVariables( | 
|  | String scopeType, Iterable<chrome.Property> variables) { | 
|  | for (var p in variables) { | 
|  | var name = p.name; | 
|  | var value = p.value; | 
|  |  | 
|  | if (scopeType == 'closure') { | 
|  | // Substitute potentially unavailable captures with their values from | 
|  | // the stack. | 
|  | // | 
|  | // Note: this makes some uncaptured values available for evaluation, | 
|  | // which might not be formally correct but convenient, for evample: | 
|  | // | 
|  | // int x = 0; | 
|  | // var f = (int y) { | 
|  | //   // 'x' is not captured so it not available at runtime but is | 
|  | //   // captured on stack, so the code below will make it available | 
|  | //   // for evaluation | 
|  | //   print(y); | 
|  | // } | 
|  | // TODO(annagrin): decide if we would like not to support evaluation | 
|  | // of uncaptured variables | 
|  |  | 
|  | var capturedValue = _valueToLiteral(value); | 
|  | jsScope[name] = capturedValue ?? name; | 
|  | } else { | 
|  | jsScope[name] = name; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // skip library and main scope | 
|  | var scopeChain = filterScopes(frame).reversed; | 
|  | for (var scope in scopeChain) { | 
|  | var scopeProperties = | 
|  | await _inspector.debugger.getProperties(scope.object.objectId); | 
|  |  | 
|  | collectVariables(scope.scope, scopeProperties); | 
|  | } | 
|  |  | 
|  | return jsScope; | 
|  | } | 
|  | } |