blob: 7344181ac1caa742b5929590d5ab5d3206fb506b [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.
import 'package:dwds/src/debugging/dart_scope.dart';
import 'package:dwds/src/debugging/debugger.dart';
import 'package:dwds/src/debugging/location.dart';
import 'package:dwds/src/debugging/modules.dart';
import 'package:dwds/src/loaders/strategy.dart';
import 'package:dwds/src/services/expression_compiler.dart';
import 'package:dwds/src/utilities/domain.dart';
import 'package:dwds/src/utilities/objects.dart' as chrome;
import 'package:logging/logging.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.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');
static const ErrorKind loadModule = ErrorKind._('LoadModuleError');
@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 String _entrypoint;
final AppInspectorInterface _inspector;
final Debugger _debugger;
final Locations _locations;
final Modules _modules;
final ExpressionCompiler _compiler;
final _logger = Logger('ExpressionEvaluator');
bool _closed = false;
/// Strip synthetic library name from compiler error messages.
static final _syntheticNameFilterRegex =
RegExp('org-dartlang-debug:synthetic_debug_expression:.*:.*Error: ');
/// Find module path from the XHR call network error message received from chrome.
///
/// Example:
/// NetworkError: Failed to load 'http://<hostname>.com/path/to/module.js?<cache_busting_token>'
static final _loadModuleErrorRegex =
RegExp(r".*Failed to load '.*\.com/(.*\.js).*");
ExpressionEvaluator(this._entrypoint, this._inspector, this._debugger,
this._locations, this._modules, this._compiler);
RemoteObject createError(ErrorKind severity, String message) {
return RemoteObject(
<String, String>{'type': '$severity', 'value': message});
}
void close() {
_closed = true;
}
/// 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 (_closed) {
return createError(ErrorKind.internal, 'expression evaluator closed.');
}
scope ??= {};
if (expression.isEmpty) {
return createError(ErrorKind.invalidInput, expression);
}
if (libraryUri == null) {
return createError(ErrorKind.invalidInput, 'no library uri');
}
final module = await _modules.moduleForLibrary(libraryUri);
if (module == null) {
return createError(ErrorKind.internal, 'no module for $libraryUri');
}
// Wrap the expression in a lambda so we can call it as a function.
expression = _createDartLambda(expression, scope.keys);
_logger.finest('Evaluating "$expression" at $module');
// Compile expression using an expression compiler, such as
// frontend server or expression compiler worker.
final compilationResult = await _compiler.compileExpressionToJs(
isolateId, libraryUri.toString(), 0, 0, {}, {}, module, expression);
final isError = compilationResult.isError;
final jsResult = compilationResult.result;
if (isError) {
return _formatCompilationError(jsResult);
}
// Strip try/catch incorrectly added by the expression compiler.
var jsCode = _maybeStripTryCatch(jsResult);
// Send JS expression to chrome to evaluate.
jsCode = _createJsLambdaWithTryCatch(jsCode, scope.keys);
var result = await _inspector.callFunction(jsCode, scope.values);
result = await _formatEvaluationError(result);
_logger
.finest('Evaluated "$expression" to "$result" for isolate $isolateId');
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 (scope != null && scope.isNotEmpty) {
// TODO(annagrin): Implement scope support.
// Issue: https://github.com/dart-lang/webdev/issues/1344
return createError(
ErrorKind.internal,
'Using scope for expression evaluation in frame '
'is not supported.');
}
if (expression.isEmpty) {
return createError(ErrorKind.invalidInput, expression);
}
// Get JS scope and current JS location.
final jsFrame = _debugger.jsFrameForIndex(frameIndex);
if (jsFrame == null) {
return createError(
ErrorKind.internal,
'Expression evaluation in async frames '
'is not supported. No frame with index $frameIndex.');
}
final functionName = jsFrame.functionName;
final jsLine = jsFrame.location.lineNumber;
final jsScriptId = jsFrame.location.scriptId;
final jsColumn = jsFrame.location.columnNumber;
final jsScope = await _collectLocalJsScope(jsFrame);
// Find corresponding dart location and scope.
final url = _debugger.urlForScriptId(jsScriptId);
if (url == null) {
return createError(
ErrorKind.internal, 'Cannot find url for JS script: $jsScriptId');
}
final locationMap = await _locations.locationForJs(url, jsLine, jsColumn);
if (locationMap == null) {
return createError(
ErrorKind.internal,
'Cannot find Dart location for JS location: '
'url: $url, '
'function: $functionName, '
'line: $jsLine, '
'column: $jsColumn');
}
final dartLocation = locationMap.dartLocation;
final dartSourcePath = dartLocation.uri.serverPath;
final libraryUri = await _modules.libraryForSource(dartSourcePath);
if (libraryUri == null) {
return createError(
ErrorKind.internal, 'no libraryUri for $dartSourcePath');
}
final module = await _modules.moduleForLibrary(libraryUri.toString());
if (module == null) {
return createError(
ErrorKind.internal, 'no module for $libraryUri ($dartSourcePath)');
}
_logger.finest('Evaluating "$expression" at $module, '
'$libraryUri:${dartLocation.line}:${dartLocation.column}');
// Compile expression using an expression compiler, such as
// frontend server or expression compiler worker.
//
// TODO(annagrin): map JS locals to dart locals in the expression
// and JS scope before passing them to the dart expression compiler.
// Issue: https://github.com/dart-lang/sdk/issues/40273
final compilationResult = await _compiler.compileExpressionToJs(
isolateId,
libraryUri.toString(),
dartLocation.line,
dartLocation.column,
{},
jsScope,
module,
expression);
final isError = compilationResult.isError;
final jsResult = compilationResult.result;
if (isError) {
return _formatCompilationError(jsResult);
}
// Strip try/catch incorrectly added by the expression compiler.
var jsCode = _maybeStripTryCatch(jsResult);
// Send JS expression to chrome to evaluate.
jsCode = _createTryCatch(jsCode);
// Send JS expression to chrome to evaluate.
var result = await _debugger.evaluateJsOnCallFrameIndex(frameIndex, jsCode);
result = await _formatEvaluationError(result);
_logger.finest('Evaluated "$expression" to "${result.json}"');
return result;
}
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 synthetic 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);
}
Future<RemoteObject> _formatEvaluationError(RemoteObject result) async {
if (result.type == 'string') {
var error = '${result.value}';
if (error.startsWith('ReferenceError: ')) {
error = error.replaceFirst('ReferenceError: ', '');
return createError(ErrorKind.reference, error);
} else if (error.startsWith('TypeError: ')) {
error = error.replaceFirst('TypeError: ', '');
return createError(ErrorKind.type, error);
} else if (error.startsWith('NetworkError: ')) {
var modulePath = _loadModuleErrorRegex.firstMatch(error)?.group(1);
final module = modulePath != null
? await globalLoadStrategy.moduleForServerPath(
_entrypoint, modulePath)
: 'unknown';
modulePath ??= 'unknown';
error = 'Module is not loaded : $module (path: $modulePath). '
'Accessing libraries that have not yet been used in the '
'application is not supported during expression evaluation.';
return createError(ErrorKind.loadModule, error);
}
}
return result;
}
Future<Map<String, String>> _collectLocalJsScope(WipCallFrame frame) async {
final jsScope = <String, String>{};
void collectVariables(
String scopeType, Iterable<chrome.Property> variables) {
for (var p in variables) {
final name = p.name;
final value = p.value;
// TODO: null values represent variables optimized by v8.
// Show that to the user.
if (name != null && value != null && !_isUndefined(value)) {
jsScope[name] = name;
}
}
}
// skip library and main scope
final scopeChain = filterScopes(frame).reversed;
for (var scope in scopeChain) {
final objectId = scope.object.objectId;
if (objectId != null) {
final scopeProperties = await _debugger.getProperties(objectId);
collectVariables(scope.scope, scopeProperties);
}
}
return jsScope;
}
bool _isUndefined(RemoteObject value) => value.type == 'undefined';
/// Strip try/catch incorrectly added by the expression compiler.
/// TODO: remove adding try/catch block in expression compiler.
/// https://github.com/dart-lang/webdev/issues/1341, then remove
/// this stripping code.
String _maybeStripTryCatch(String jsCode) {
// Match the wrapping generated by the expression compiler exactly
// so the matching does not succeed naturally after the wrapping is
// removed:
//
// Expression compiler's wrapping:
//
// '\ntry {'
// '\n ($jsExpression('
// '\n $args'
// '\n ))'
// '\n} catch (error) {'
// '\n error.name + ": " + error.message;'
// '\n}';
//
final lines = jsCode.split('\n');
if (lines.length > 5) {
final tryLines = lines.getRange(0, 2).toList();
final bodyLines = lines.getRange(2, lines.length - 3);
final catchLines =
lines.getRange(lines.length - 3, lines.length).toList();
if (tryLines[0].isEmpty &&
tryLines[1] == 'try {' &&
catchLines[0] == '} catch (error) {' &&
catchLines[1] == ' error.name + ": " + error.message;' &&
catchLines[2] == '}') {
return bodyLines.join('\n');
}
}
return jsCode;
}
String _createJsLambdaWithTryCatch(
String expression, Iterable<String> params) {
final args = params.join(', ');
return ' '
' function($args) {\n'
' try {\n'
' return $expression($args);\n'
' } catch (error) {\n'
' return error.name + ": " + error.message;\n'
' }\n'
'} ';
}
String _createTryCatch(String expression) => ' '
' try {\n'
' $expression;\n'
' } catch (error) {\n'
' error.name + ": " + error.message;\n'
' }\n';
String _createDartLambda(String expression, Iterable<String> params) =>
'(${params.join(', ')}) => $expression';
}