| // 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 '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 Component, Library; |
| import 'package:kernel/dart_scope_calculator.dart'; |
| |
| import '../compiler/js_names.dart' as js_ast; |
| import '../compiler/module_builder.dart'; |
| import '../js_ast/js_ast.dart' as js_ast; |
| import 'compiler.dart' show Compiler; |
| |
| 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, []); |
| } |
| |
| class ExpressionCompiler { |
| static final String debugProcedureName = '\$dartEval'; |
| |
| final CompilerContext _context; |
| final CompilerOptions _options; |
| final List<String> errors; |
| final IncrementalCompiler _compiler; |
| final Compiler _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 library [libraryUri] and file [scriptUri] |
| /// at [line]:[column] to JavaScript in [moduleName]. |
| /// |
| /// [libraryUri] and [scriptUri] can be the same, but if for instance |
| /// evaluating expressions in a part file the [libraryUri] will be the uri of |
| /// the "part of" file whereas [scriptUri] will be the uri of the part. |
| /// |
| /// [line] and [column] are 1-based. Library level expressions typically use |
| /// [line] and [column] 1 as an indicator that there is no relevant location. |
| /// For flexibility, a value of 0 is also accepted and recognized |
| /// in the same way. |
| /// |
| /// 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, |
| String? scriptUri, |
| 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 = _findScopeAt(Uri.parse(libraryUri), |
| scriptUri == null ? null : Uri.parse(scriptUri), 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) |
| |
| // Work around mismatched names and lowered representation for late local |
| // variables. |
| // Replace the existing entries with a name that matches the named |
| // extracted from the lowering. |
| // See https://github.com/dart-lang/sdk/issues/55918 |
| var dartLateLocals = [ |
| for (var name in dartScope.definitions.keys) |
| if (isLateLoweredLocalName(name)) name, |
| ]; |
| for (var localName in dartLateLocals) { |
| dartScope.definitions[extractLocalName(localName)] = |
| dartScope.definitions.remove(localName)!; |
| } |
| // remove undefined js variables (this allows us to get a reference error |
| // from chrome on evaluation) |
| dartScope.definitions.removeWhere((variable, type) => |
| !jsScope.containsKey(_dartNameToJsName(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[_dartNameToJsName(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 = '\n ($jsExpression(' |
| '\n $args' |
| '\n ))'; |
| |
| _log('Compiled expression \n$expression to $callExpression'); |
| return callExpression; |
| } catch (e, s) { |
| onDiagnostic( |
| _createInternalError(Uri.parse(libraryUri), line, column, '$e:$s')); |
| return null; |
| } |
| } |
| |
| String? _dartNameToJsName(String? dartName) { |
| if (dartName == null) return dartName; |
| if (isExtensionThisName(dartName)) return r'$this'; |
| return dartName; |
| } |
| |
| DartScope? _findScopeAt( |
| Uri libraryUri, Uri? scriptFileUri, int line, int column) { |
| if (line < 0) { |
| onDiagnostic(_createInternalError( |
| libraryUri, line, column, 'Invalid source location')); |
| return null; |
| } |
| |
| var library = _getLibrary(libraryUri); |
| if (library == null) { |
| onDiagnostic(_createInternalError( |
| libraryUri, line, column, 'Dart library not found for location')); |
| return null; |
| } |
| |
| // TODO(jensj): Eventually make the scriptUri required and always use this, |
| // but for now use the old mechanism when no script is provided. |
| if (scriptFileUri != null) { |
| final offset = _component.getOffset(library.fileUri, line, column); |
| final scope2 = |
| DartScopeBuilder2.findScopeFromOffset(library, scriptFileUri, offset); |
| return scope2; |
| } |
| |
| var scope = DartScopeBuilder.findScope(_component, library, line, column); |
| if (scope == null) { |
| // Fallback mechanism to allow a evaluation of an expression at the |
| // library level within the Dart SDK. |
| // |
| // Currently we lack the full dill and metadata for the Dart SDK module to |
| // be able to use the same mechanism of expression evaluation as the rest |
| // of a program. Because of that, expression evaluation at arbitrary |
| // scopes is not supported in the Dart SDK. However, we can still support |
| // compiling expressions that will be evaluated at the library level. We |
| // determine if that's the case by recognizing that all such requests use |
| // line 1 and column 1. |
| if (line <= 1 && column <= 1 && library.importUri.isScheme('dart')) { |
| _log('Fallback: use library scope for the Dart SDK'); |
| scope = DartScope(library, null, null, {}, []); |
| } else { |
| onDiagnostic(_createInternalError( |
| libraryUri, line, column, 'Dart scope not found for location')); |
| return null; |
| } |
| } |
| |
| _log('Detected expression compilation scope'); |
| return scope; |
| } |
| |
| Library? _getLibrary(Uri libraryUri) { |
| return _compiler.lookupLibrary(libraryUri); |
| } |
| |
| /// 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 methodName = scope.member?.name.text; |
| var member = scope.member; |
| if (member != null) { |
| if (member.isExtensionMember || member.isExtensionTypeMember) { |
| methodName = extractQualifiedNameFromExtensionMethodName(methodName); |
| } |
| } |
| var procedure = await _compiler.compileExpression( |
| expression, |
| scope.definitions, |
| scope.typeParameters, |
| debugProcedureName, |
| scope.library.importUri, |
| methodName: methodName, |
| 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(); |
| } |
| } |