| // 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:front_end/src/api_unstable/ddc.dart'; |
| import 'package:kernel/ast.dart' |
| show |
| Block, |
| Class, |
| Component, |
| Constructor, |
| DartType, |
| Field, |
| FunctionNode, |
| Library, |
| Member, |
| Node, |
| Procedure, |
| RedirectingFactory, |
| TreeNode, |
| TypeParameter, |
| VariableDeclaration, |
| Visitor, |
| VisitorNullMixin, |
| VisitorVoidMixin; |
| |
| import '../../dev_compiler.dart'; |
| import '../compiler/js_names.dart' as js_ast; |
| import '../compiler/module_builder.dart'; |
| import '../js_ast/js_ast.dart' as js_ast; |
| |
| DiagnosticMessage _createInternalError(Uri uri, int line, int col, String msg) { |
| return Message(Code<String>('Expression Compiler Internal error'), |
| problemMessage: 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 RedirectingFactory) 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.lookupLibraryBuilder(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, |
| className: scope.cls?.name, |
| isStatic: 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(); |
| } |
| } |