a6e3008ded
See: https://dart-review.googlesource.com/c/sdk/+/196026 TEST=Code cleanup exclusively (sorting imports); no new tests. Change-Id: Ib07a82ff418138c542d6a83cfab9aabbb285f866 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/196180 Auto-Submit: Phil Quitslund <pquitslund@google.com> Reviewed-by: Phil Quitslund <pquitslund@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Ben Konyi <bkonyi@google.com> Reviewed-by: Bob Nystrom <rnystrom@google.com> Commit-Queue: Phil Quitslund <pquitslund@google.com>
474 lines
15 KiB
Dart
474 lines
15 KiB
Dart
// 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 'dart:async';
|
|
|
|
import 'package:_fe_analyzer_shared/src/messages/codes.dart'
|
|
show Code, Message, PlainAndColorizedString;
|
|
import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart'
|
|
show DiagnosticMessage, DiagnosticMessageHandler;
|
|
import 'package:dev_compiler/dev_compiler.dart';
|
|
import 'package:dev_compiler/src/compiler/js_names.dart' as js_ast;
|
|
import 'package:dev_compiler/src/compiler/module_builder.dart';
|
|
import 'package:dev_compiler/src/js_ast/js_ast.dart' as js_ast;
|
|
import 'package:front_end/src/api_unstable/ddc.dart';
|
|
import 'package:kernel/ast.dart'
|
|
show
|
|
Block,
|
|
Class,
|
|
Component,
|
|
Constructor,
|
|
DartType,
|
|
Field,
|
|
FunctionNode,
|
|
Library,
|
|
Member,
|
|
Node,
|
|
Procedure,
|
|
RedirectingFactoryConstructor,
|
|
TreeNode,
|
|
TypeParameter,
|
|
VariableDeclaration,
|
|
Visitor,
|
|
VisitorNullMixin,
|
|
VisitorVoidMixin;
|
|
|
|
DiagnosticMessage _createInternalError(Uri uri, int line, int col, String msg) {
|
|
return Message(Code<String>('Expression Compiler Internal error'),
|
|
message: msg)
|
|
.withLocation(uri, 0, 0)
|
|
.withFormatting(PlainAndColorizedString.plainOnly('Internal error: $msg'),
|
|
line, col, Severity.internalProblem, []);
|
|
}
|
|
|
|
/// Dart scope
|
|
///
|
|
/// Provides information about symbols available inside a dart scope.
|
|
class DartScope {
|
|
final Library library;
|
|
final Class cls;
|
|
final Member member;
|
|
final bool isStatic;
|
|
final Map<String, DartType> definitions;
|
|
final List<TypeParameter> typeParameters;
|
|
|
|
DartScope(this.library, this.cls, this.member, this.definitions,
|
|
this.typeParameters)
|
|
: isStatic = member is Procedure ? member.isStatic : false;
|
|
|
|
@override
|
|
String toString() {
|
|
return '''DartScope {
|
|
Library: ${library.importUri},
|
|
Class: ${cls?.name},
|
|
Procedure: $member,
|
|
isStatic: $isStatic,
|
|
Scope: $definitions,
|
|
typeParameters: $typeParameters
|
|
}
|
|
''';
|
|
}
|
|
}
|
|
|
|
/// DartScopeBuilder finds dart scope information for a location.
|
|
///
|
|
/// Find all definitions in scope at a given 1-based [line] and [column]:
|
|
///
|
|
/// - library
|
|
/// - class
|
|
/// - locals
|
|
/// - formals
|
|
/// - captured variables (for closures)
|
|
class DartScopeBuilder extends Visitor<void> with VisitorVoidMixin {
|
|
final Component _component;
|
|
final int _line;
|
|
final int _column;
|
|
|
|
Library _library;
|
|
Class _cls;
|
|
Member _member;
|
|
int _offset;
|
|
|
|
DiagnosticMessageHandler onDiagnostic;
|
|
|
|
final List<FunctionNode> _functions = [];
|
|
final Map<String, DartType> _definitions = {};
|
|
final List<TypeParameter> _typeParameters = [];
|
|
|
|
DartScopeBuilder._(this._component, this._line, this._column);
|
|
|
|
static DartScope findScope(Component component, Library library, int line,
|
|
int column, DiagnosticMessageHandler onDiagnostic) {
|
|
var builder = DartScopeBuilder._(component, line, column)
|
|
..onDiagnostic = onDiagnostic;
|
|
library.accept(builder);
|
|
return builder.build();
|
|
}
|
|
|
|
DartScope build() {
|
|
if (_offset == null || _library == null) return null;
|
|
|
|
return DartScope(_library, _cls, _member, _definitions, _typeParameters);
|
|
}
|
|
|
|
@override
|
|
void defaultTreeNode(Node node) {
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
@override
|
|
void visitLibrary(Library library) {
|
|
_library = library;
|
|
_offset = 0;
|
|
if (_line > 0) {
|
|
_offset = _component.getOffset(_library.fileUri, _line, _column);
|
|
}
|
|
|
|
// Exit early if the evaluation offset is not found.
|
|
// Note: the complete scope is not found in this case,
|
|
// so the expression compiler will report an error.
|
|
if (_offset >= 0) super.visitLibrary(library);
|
|
}
|
|
|
|
@override
|
|
void visitClass(Class cls) {
|
|
if (_scopeContainsOffset(cls.fileOffset, cls.fileEndOffset, _offset)) {
|
|
_cls = cls;
|
|
_typeParameters.addAll(cls.typeParameters);
|
|
|
|
super.visitClass(cls);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void defaultMember(Member m) {
|
|
if (_scopeContainsOffset(m.fileOffset, m.fileEndOffset, _offset)) {
|
|
_member = m;
|
|
|
|
super.defaultMember(m);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitFunctionNode(FunctionNode fun) {
|
|
if (_scopeContainsOffset(fun.fileOffset, fun.fileEndOffset, _offset)) {
|
|
_functions.add(fun);
|
|
_typeParameters.addAll(fun.typeParameters);
|
|
|
|
super.visitFunctionNode(fun);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitVariableDeclaration(VariableDeclaration decl) {
|
|
// Collect locals and formals appearing before current breakpoint.
|
|
// Note that we include variables with no offset because the offset
|
|
// is not set in many cases in generated code, so omitting them would
|
|
// make expression evaluation fail in too many cases.
|
|
// Issue: https://github.com/dart-lang/sdk/issues/43966
|
|
if (decl.fileOffset < 0 || decl.fileOffset < _offset) {
|
|
_definitions[decl.name] = decl.type;
|
|
}
|
|
super.visitVariableDeclaration(decl);
|
|
}
|
|
|
|
@override
|
|
void visitBlock(Block block) {
|
|
var fileEndOffset = FileEndOffsetCalculator.calculateEndOffset(block);
|
|
if (_scopeContainsOffset(block.fileOffset, fileEndOffset, _offset)) {
|
|
super.visitBlock(block);
|
|
}
|
|
}
|
|
|
|
bool _scopeContainsOffset(int startOffset, int endOffset, int offset) {
|
|
if (offset < 0 || startOffset < 0 || endOffset < 0) {
|
|
return false;
|
|
}
|
|
return startOffset <= offset && offset <= endOffset;
|
|
}
|
|
}
|
|
|
|
/// File end offset calculator.
|
|
///
|
|
/// Helps calculate file end offsets for nodes with internal scope
|
|
/// that do not have .fileEndOffset field.
|
|
///
|
|
/// For example - [Block]
|
|
class FileEndOffsetCalculator extends Visitor<int> with VisitorNullMixin<int> {
|
|
static const int noOffset = -1;
|
|
|
|
final int _startOffset;
|
|
final TreeNode _root;
|
|
|
|
int _endOffset = noOffset;
|
|
|
|
/// Create calculator for a scoping node with no .fileEndOffset.
|
|
///
|
|
/// [_root] is the parent of the scoping node.
|
|
/// [_startOffset] is the start offset of the scoping node.
|
|
FileEndOffsetCalculator._(this._root, this._startOffset);
|
|
|
|
/// Calculate file end offset for a scoping node.
|
|
///
|
|
/// This calculator finds the first node in the ancestor chain that
|
|
/// can give such information for a given [node], i.e. satisfies one
|
|
/// of the following conditions:
|
|
///
|
|
/// - a node with with a greater start offset that is a child of the
|
|
/// closest ancestor. The start offset of this child is used as a
|
|
/// file end offset of the [node].
|
|
///
|
|
/// - the closest ancestor with .fileEndOffset information. The file
|
|
/// end offset of the ancestor is used as the file end offset of
|
|
/// the [node.]
|
|
///
|
|
/// If none found, return [noOffset].
|
|
static int calculateEndOffset(TreeNode node) {
|
|
for (var n = node.parent; n != null; n = n.parent) {
|
|
var calculator = FileEndOffsetCalculator._(n, node.fileOffset);
|
|
var offset = n.accept(calculator);
|
|
if (offset != noOffset) return offset;
|
|
}
|
|
return noOffset;
|
|
}
|
|
|
|
@override
|
|
int defaultTreeNode(TreeNode node) {
|
|
if (node == _root) {
|
|
node.visitChildren(this);
|
|
if (_endOffset != noOffset) return _endOffset;
|
|
return _endOffsetForNode(node);
|
|
}
|
|
if (_endOffset == noOffset && node.fileOffset > _startOffset) {
|
|
_endOffset = node.fileOffset;
|
|
}
|
|
return _endOffset;
|
|
}
|
|
|
|
static int _endOffsetForNode(TreeNode node) {
|
|
if (node is Class) return node.fileEndOffset;
|
|
if (node is Constructor) return node.fileEndOffset;
|
|
if (node is Procedure) return node.fileEndOffset;
|
|
if (node is Field) return node.fileEndOffset;
|
|
if (node is RedirectingFactoryConstructor) return node.fileEndOffset;
|
|
if (node is FunctionNode) return node.fileEndOffset;
|
|
return noOffset;
|
|
}
|
|
}
|
|
|
|
class ExpressionCompiler {
|
|
static final String debugProcedureName = '\$dartEval';
|
|
|
|
final CompilerContext _context;
|
|
final CompilerOptions _options;
|
|
final List<String> errors;
|
|
final IncrementalCompiler _compiler;
|
|
final ProgramCompiler _kernel2jsCompiler;
|
|
final Component _component;
|
|
final ModuleFormat _moduleFormat;
|
|
|
|
DiagnosticMessageHandler onDiagnostic;
|
|
|
|
void _log(String message) {
|
|
if (_options.verbose) {
|
|
_context.options.ticker.logMs(message);
|
|
}
|
|
}
|
|
|
|
ExpressionCompiler(
|
|
this._options,
|
|
this._moduleFormat,
|
|
this.errors,
|
|
this._compiler,
|
|
this._kernel2jsCompiler,
|
|
this._component,
|
|
) : onDiagnostic = _options.onDiagnostic,
|
|
_context = _compiler.context;
|
|
|
|
/// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
|
|
/// in [moduleName].
|
|
///
|
|
/// [line] and [column] are 1-based.
|
|
///
|
|
/// Values listed in [jsFrameValues] are substituted for their names in the
|
|
/// [expression].
|
|
///
|
|
/// Returns expression compiled to JavaScript or null on error.
|
|
/// Errors are reported using onDiagnostic function.
|
|
///
|
|
/// [jsFrameValues] is a map from js variable name to its primitive value
|
|
/// or another variable name, for example
|
|
/// { 'x': '1', 'y': 'y', 'o': 'null' }
|
|
Future<String> compileExpressionToJs(String libraryUri, int line, int column,
|
|
Map<String, String> jsScope, String expression) async {
|
|
try {
|
|
// 1. find dart scope where debugger is paused
|
|
|
|
_log('Compiling expression \n$expression');
|
|
|
|
var dartScope = await _findScopeAt(Uri.parse(libraryUri), line, column);
|
|
if (dartScope == null) {
|
|
_log('Scope not found at $libraryUri:$line:$column');
|
|
return null;
|
|
}
|
|
_log('DartScope: $dartScope');
|
|
|
|
// 2. perform necessary variable substitutions
|
|
|
|
// TODO(annagrin): we only substitute for the same name or a value
|
|
// currently, need to extend to cases where js variable names are
|
|
// different from dart.
|
|
// See [issue 40273](https://github.com/dart-lang/sdk/issues/40273)
|
|
|
|
// remove undefined js variables (this allows us to get a reference error
|
|
// from chrome on evaluation)
|
|
dartScope.definitions
|
|
.removeWhere((variable, type) => !jsScope.containsKey(variable));
|
|
|
|
dartScope.typeParameters
|
|
.removeWhere((parameter) => !jsScope.containsKey(parameter.name));
|
|
|
|
// map from values from the stack when available (this allows to evaluate
|
|
// captured variables optimized away in chrome)
|
|
var localJsScope = [
|
|
...dartScope.typeParameters.map((parameter) => jsScope[parameter.name]),
|
|
...dartScope.definitions.keys.map((variable) => jsScope[variable])
|
|
];
|
|
|
|
_log('Performed scope substitutions for expression');
|
|
|
|
// 3. compile dart expression to JS
|
|
|
|
var jsExpression = await _compileExpression(dartScope, expression);
|
|
|
|
if (jsExpression == null) {
|
|
_log('Failed to compile expression: \n$expression');
|
|
return null;
|
|
}
|
|
|
|
// some adjustments to get proper binding to 'this',
|
|
// making closure variables available, and catching errors
|
|
|
|
// TODO(annagrin): make compiler produce correct expression:
|
|
// See [issue 40277](https://github.com/dart-lang/sdk/issues/40277)
|
|
// - evaluate to an expression in function and class context
|
|
// - allow setting values
|
|
// See [issue 40273](https://github.com/dart-lang/sdk/issues/40273)
|
|
// - bind to proper 'this'
|
|
// - map to correct js names for dart symbols
|
|
|
|
// 4. create call the expression
|
|
|
|
if (dartScope.cls != null && !dartScope.isStatic) {
|
|
// bind to correct 'this' instead of 'globalThis'
|
|
jsExpression = '$jsExpression.bind(this)';
|
|
}
|
|
|
|
// 5. wrap in a try/catch to catch errors
|
|
|
|
var args = localJsScope.join(',\n ');
|
|
jsExpression = jsExpression.split('\n').join('\n ');
|
|
var callExpression = '\ntry {'
|
|
'\n ($jsExpression('
|
|
'\n $args'
|
|
'\n ))'
|
|
'\n} catch (error) {'
|
|
'\n error.name + ": " + error.message;'
|
|
'\n}';
|
|
|
|
_log('Compiled expression \n$expression to $callExpression');
|
|
return callExpression;
|
|
} catch (e, s) {
|
|
onDiagnostic(
|
|
_createInternalError(Uri.parse(libraryUri), line, column, '$e:$s'));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<DartScope> _findScopeAt(Uri libraryUri, int line, int column) async {
|
|
if (line < 0) {
|
|
onDiagnostic(_createInternalError(
|
|
libraryUri, line, column, 'Invalid source location'));
|
|
return null;
|
|
}
|
|
|
|
var library = await _getLibrary(libraryUri);
|
|
if (library == null) {
|
|
onDiagnostic(_createInternalError(
|
|
libraryUri, line, column, 'Dart library not found for location'));
|
|
return null;
|
|
}
|
|
|
|
var scope = DartScopeBuilder.findScope(
|
|
_component, library, line, column, onDiagnostic);
|
|
if (scope == null) {
|
|
onDiagnostic(_createInternalError(
|
|
libraryUri, line, column, 'Dart scope not found for location'));
|
|
return null;
|
|
}
|
|
|
|
_log('Detected expression compilation scope');
|
|
return scope;
|
|
}
|
|
|
|
Future<Library> _getLibrary(Uri libraryUri) async {
|
|
return await _compiler.context.runInContext((_) async {
|
|
var builder = _compiler.userCode.loader.builders[libraryUri];
|
|
if (builder != null) {
|
|
var library =
|
|
_compiler.userCode.loader.read(libraryUri, -1, accessor: builder);
|
|
|
|
return library.library;
|
|
}
|
|
|
|
_log('Loaded library for expression');
|
|
return null;
|
|
});
|
|
}
|
|
|
|
/// Return a JS function that returns the evaluated results when called.
|
|
///
|
|
/// [scope] current dart scope information.
|
|
/// [expression] expression to compile in given [scope].
|
|
Future<String> _compileExpression(DartScope scope, String expression) async {
|
|
var procedure = await _compiler.compileExpression(
|
|
expression,
|
|
scope.definitions,
|
|
scope.typeParameters,
|
|
debugProcedureName,
|
|
scope.library.importUri,
|
|
scope.cls?.name,
|
|
scope.isStatic);
|
|
|
|
_log('Compiled expression to kernel');
|
|
|
|
// TODO: make this code clear and assumptions enforceable
|
|
// https://github.com/dart-lang/sdk/issues/43273
|
|
if (errors.isNotEmpty) {
|
|
return null;
|
|
}
|
|
|
|
var imports = <js_ast.ModuleItem>[];
|
|
var jsFun = _kernel2jsCompiler.emitFunctionIncremental(imports,
|
|
scope.library, scope.cls, procedure.function, '$debugProcedureName');
|
|
|
|
_log('Generated JavaScript for expression');
|
|
|
|
// print JS ast to string for evaluation
|
|
var context = js_ast.SimpleJavaScriptPrintingContext();
|
|
var opts =
|
|
js_ast.JavaScriptPrintingOptions(allowKeywordsInProperties: true);
|
|
|
|
var tree = transformFunctionModuleFormat(imports, jsFun, _moduleFormat);
|
|
tree.accept(
|
|
js_ast.Printer(opts, context, localNamer: js_ast.TemporaryNamer(tree)));
|
|
|
|
_log('Added imports and renamed variables for expression');
|
|
|
|
return context.getText();
|
|
}
|
|
}
|