blob: 767283ee76586233242c13ed6f278859a43aa9d1 [file] [log] [blame]
// 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;
}
}