|  | // 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)!; | 
|  | } | 
|  |  | 
|  | // Create a mapping from Dart variable names in scope to the corresponding | 
|  | // JS variable names. The Dart variable may have had a suffix of the | 
|  | // form '$N' added to it where N is either the empty string or an | 
|  | // integer >= 0. | 
|  | final dartNameToJsName = <String, String>{}; | 
|  |  | 
|  | int nameCompare(String a, String b) { | 
|  | final lengthCmp = b.length.compareTo(a.length); | 
|  | if (lengthCmp != 0) return lengthCmp; | 
|  | return b.compareTo(a); | 
|  | } | 
|  |  | 
|  | // Sort Dart names in case a user-defined name includes a '$'. The | 
|  | // resulting normalized JS name might seem like a suffixed version of a | 
|  | // shorter Dart name. Since longer Dart names can't incorrectly match a | 
|  | // shorter JS name (JS names are always at least as long as the Dart | 
|  | // name), we process them from longest to shortest. | 
|  | final dartNames = [...dartScope.definitions.keys]..sort(nameCompare); | 
|  |  | 
|  | // Sort JS names so that the highest suffix value gets assigned to the | 
|  | // corresponding Dart name first. Since names are suffixed in ascending | 
|  | // order as inner scopes are visited, the highest suffix value will be | 
|  | // the one that matches the visible Dart name in a given scope. | 
|  | final jsNames = [...jsScope.keys]..sort(nameCompare); | 
|  |  | 
|  | const removedSentinel = ''; | 
|  | const thisJsName = r'$this'; | 
|  |  | 
|  | for (final dartName in dartNames) { | 
|  | if (isExtensionThisName(dartName)) { | 
|  | if (jsScope.containsKey(thisJsName)) { | 
|  | dartNameToJsName[dartName] = thisJsName; | 
|  | } | 
|  | continue; | 
|  | } | 
|  | // Any name containing a '$' symbol will have that symbol expanded to | 
|  | // '$36' in JS. We do a similar expansion here to normalize the names. | 
|  | final jsNamePrefix = | 
|  | js_ast.toJSIdentifier(dartName).replaceAll('\$', '\\\$'); | 
|  | final regexp = RegExp(r'^' + jsNamePrefix + r'(\$[0-9]*)?$'); | 
|  | for (var i = 0; i < jsNames.length; i++) { | 
|  | final jsName = jsNames[i]; | 
|  | if (jsName == removedSentinel) continue; | 
|  | if (jsName.length < dartName.length) break; | 
|  | if (regexp.hasMatch(jsName)) { | 
|  | dartNameToJsName[dartName] = jsName; | 
|  | jsNames[i] = removedSentinel; | 
|  |  | 
|  | // Remove any additional JS names that match this name as these will | 
|  | // correspond to shadowed Dart variables that are not visible in the | 
|  | // current scope. | 
|  | // | 
|  | // Note: In some extreme cases this can match the wrong variable. | 
|  | // This would require a combination of 36 nested variables with the | 
|  | // same name and a similarly named variable with a $ in its name. | 
|  | for (var j = i; j < jsNames.length; j++) { | 
|  | final jsName = jsNames[j]; | 
|  | if (jsName == removedSentinel) continue; | 
|  | if (jsName.length < dartName.length) break; | 
|  | if (regexp.hasMatch(jsNames[j])) { | 
|  | jsNames[j] = removedSentinel; | 
|  | } | 
|  | } | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // remove undefined js variables (this allows us to get a reference error | 
|  | // from chrome on evaluation) | 
|  | dartScope.definitions.removeWhere( | 
|  | (variable, type) => !dartNameToJsName.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[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  '); | 
|  | // We check for '_boundMethod' in case tearoffs are returned. | 
|  | var callExpression = '((() => {var output = $jsExpression($args); ' | 
|  | 'return output?._boundMethod || output;})())'; | 
|  |  | 
|  | _log('Compiled expression \n$expression to $callExpression'); | 
|  | return callExpression; | 
|  | } catch (e, s) { | 
|  | onDiagnostic( | 
|  | _createInternalError(Uri.parse(libraryUri), line, column, '$e:$s')); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | 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.ScopedNamer(tree))); | 
|  |  | 
|  | _log('Added imports and renamed variables for expression'); | 
|  |  | 
|  | return context.getText(); | 
|  | } | 
|  | } |