Support VMService.evaluate with scope (#1340)

* Support scope in ChromeProxyService.evaluate()

Support evaluate with scope by
- wrapping the expression in a function with scope keys as parameters
- compiling the wrapper
- and using chrome's Runtime.callFunctionOn to evaluate the compiled
  call with arguments

Will be followed up with a cleanup of try/catch additions and removals.

Closes:  https://github.com/dart-lang/webdev/issues/1336

* Build

* Minor fixes and rebuild

* Addressed CR comments
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 5afe35b..aa5fe0d 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -6,6 +6,7 @@
 - Show lowered final fields using their original dart names.
 - Limit simultaneous connections to asset server to prevent broken sockets.
 - Fix hangs in hot restart.
+- Initial support for passing scope to `ChromeProxyService.evaluate`.
 
 ## 11.1.1
 
diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart
index bfee68a..fc883a2 100644
--- a/dwds/lib/src/debugging/inspector.dart
+++ b/dwds/lib/src/debugging/inspector.dart
@@ -208,6 +208,25 @@
     return RemoteObject(result.result['result'] as Map<String, Object>);
   }
 
+  /// Calls Chrome's Runtime.callFunctionOn method with a global function.
+  ///
+  /// [evalExpression] should be a JS function definition that can accept
+  /// [arguments].
+  Future<RemoteObject> _jsCallFunction(
+      String evalExpression, List<RemoteObject> arguments,
+      {bool returnByValue = false}) async {
+    var jsArguments = arguments.map(callArgumentFor).toList();
+    var result =
+        await remoteDebugger.sendCommand('Runtime.callFunctionOn', params: {
+      'functionDeclaration': evalExpression,
+      'arguments': jsArguments,
+      'executionContextId': await contextId,
+      'returnByValue': returnByValue,
+    });
+    handleErrorIfPresent(result, evalContents: evalExpression);
+    return RemoteObject(result.result['result'] as Map<String, Object>);
+  }
+
   Future<RemoteObject> evaluate(
       String isolateId, String targetId, String expression,
       {Map<String, String> scope}) async {
@@ -313,6 +332,13 @@
     return _evaluateInLibrary(library, evalExpression, arguments);
   }
 
+  /// Call [function] with objects referred by [argumentIds] as arguments.
+  Future<RemoteObject> callFunction(
+      String function, Iterable<String> argumentIds) async {
+    var arguments = argumentIds.map(remoteObjectFor).toList();
+    return _jsCallFunction(function, arguments);
+  }
+
   Future<Library> getLibrary(String isolateId, String objectId) async {
     return await _getLibrary(isolateId, objectId);
   }
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 2e384e2..80fb3c3 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -116,9 +116,6 @@
       _skipLists,
       uri,
     );
-    _expressionEvaluator = _compiler == null
-        ? null
-        : ExpressionEvaluator(debugger, _locations, _modules, _compiler);
     _debuggerCompleter.complete(debugger);
   }
 
@@ -228,6 +225,10 @@
       executionContext,
     );
 
+    _expressionEvaluator = _compiler == null
+        ? null
+        : ExpressionEvaluator(_inspector, _locations, _modules, _compiler);
+
     await debugger.reestablishBreakpoints(
         _previousBreakpoints, _disabledBreakpoints);
     _disabledBreakpoints.clear();
@@ -434,7 +435,7 @@
         var library = await _inspector?.getLibrary(isolateId, targetId);
         var result = await _getEvaluationResult(
             () => _expressionEvaluator.evaluateExpression(
-                isolateId, library.uri, expression),
+                isolateId, library.uri, expression, scope),
             expression);
         if (result is ErrorRef) {
           error = result;
@@ -473,9 +474,19 @@
         await isCompilerInitialized;
         _validateIsolateId(isolateId);
 
+        if (scope != null) {
+          // TODO(annagrin): Implement scope support.
+          // Issue: https://github.com/dart-lang/webdev/issues/1344
+          throw RPCError(
+              'evaluateInFrame',
+              RPCError.kInvalidRequest,
+              'Expression evaluation with scope is not supported '
+                  'for this configuration.');
+        }
+
         var result = await _getEvaluationResult(
             () => _expressionEvaluator.evaluateExpressionInFrame(
-                isolateId, frameIndex, expression),
+                isolateId, frameIndex, expression, scope),
             expression);
 
         if (result is ErrorRef) {
diff --git a/dwds/lib/src/services/expression_evaluator.dart b/dwds/lib/src/services/expression_evaluator.dart
index ebd7d07..767283e 100644
--- a/dwds/lib/src/services/expression_evaluator.dart
+++ b/dwds/lib/src/services/expression_evaluator.dart
@@ -8,7 +8,7 @@
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 import '../debugging/dart_scope.dart';
-import '../debugging/debugger.dart';
+import '../debugging/inspector.dart';
 import '../debugging/location.dart';
 import '../debugging/modules.dart';
 import '../utilities/objects.dart' as chrome;
@@ -33,7 +33,7 @@
 /// collect context for evaluation (scope, types, modules), and using
 /// ExpressionCompilerInterface to compile dart expressions to JavaScript.
 class ExpressionEvaluator {
-  final Future<Debugger> _debugger;
+  final AppInspector _inspector;
   final Locations _locations;
   final Modules _modules;
   final ExpressionCompiler _compiler;
@@ -43,7 +43,7 @@
       RegExp('org-dartlang-debug:synthetic_debug_expression:.*:.*Error: ');
 
   ExpressionEvaluator(
-      this._debugger, this._locations, this._modules, this._compiler);
+      this._inspector, this._locations, this._modules, this._compiler);
 
   RemoteObject _createError(ErrorKind severity, String message) {
     return RemoteObject(
@@ -62,7 +62,11 @@
   /// [libraryUri] dart library to evaluate the expression in.
   /// [expression] dart expression to evaluate.
   Future<RemoteObject> evaluateExpression(
-      String isolateId, String libraryUri, String expression) async {
+    String isolateId,
+    String libraryUri,
+    String expression,
+    Map<String, String> scope,
+  ) async {
     if (_compiler == null) {
       return _createError(ErrorKind.internal,
           'ExpressionEvaluator needs an ExpressionCompiler');
@@ -74,6 +78,10 @@
 
     var module = await _modules.moduleForlibrary(libraryUri);
 
+    if (scope != null && scope.isNotEmpty) {
+      var params = scope.keys.join(', ');
+      expression = '($params) => $expression';
+    }
     _logger.finest('Evaluating "$expression" at $module');
 
     // Compile expression using an expression compiler, such as
@@ -88,8 +96,22 @@
     }
 
     // Send JS expression to chrome to evaluate.
-    var result = await (await _debugger).evaluate(jsResult);
-    result = _formatEvaluationError(result);
+    RemoteObject result;
+    if (scope != null && scope.isNotEmpty) {
+      // Strip try/catch.
+      // TODO: remove adding try/catch block in expression compiler.
+      // https://github.com/dart-lang/webdev/issues/1341
+      var lines = jsResult.split('\n');
+      var inner = lines.getRange(2, lines.length - 3).join('\n');
+      var function = 'function(t) {'
+          '  return $inner(t);'
+          '}';
+      result = await _inspector.callFunction(function, scope.values);
+      result = _formatEvaluationError(result);
+    } else {
+      result = await _inspector.debugger.evaluate(jsResult);
+      result = _formatEvaluationError(result);
+    }
 
     _logger.finest('Evaluated "$expression" to "$result"');
     return result;
@@ -107,8 +129,8 @@
   /// [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) async {
+  Future<RemoteObject> evaluateExpressionInFrame(String isolateId,
+      int frameIndex, String expression, Map<String, String> scope) async {
     if (_compiler == null) {
       return _createError(ErrorKind.internal,
           'ExpressionEvaluator needs an ExpressionCompiler');
@@ -119,7 +141,7 @@
     }
 
     // Get JS scope and current JS location.
-    var jsFrame = (await _debugger).jsFrameForIndex(frameIndex);
+    var jsFrame = _inspector.debugger.jsFrameForIndex(frameIndex);
     if (jsFrame == null) {
       return _createError(
           ErrorKind.internal, 'No frame with index $frameIndex');
@@ -175,7 +197,7 @@
     }
 
     // Send JS expression to chrome to evaluate.
-    var result = await (await _debugger)
+    var result = await _inspector.debugger
         .evaluateJsOnCallFrameIndex(frameIndex, jsResult);
     result = _formatEvaluationError(result);
 
@@ -271,7 +293,7 @@
     var scopeChain = filterScopes(frame).reversed;
     for (var scope in scopeChain) {
       var scopeProperties =
-          await (await _debugger).getProperties(scope.object.objectId);
+          await _inspector.debugger.getProperties(scope.object.objectId);
 
       collectVariables(scope.scope, scopeProperties);
     }
diff --git a/dwds/test/build_daemon_evaluate_test.dart b/dwds/test/build_daemon_evaluate_test.dart
index 7695118..c2bafc1 100644
--- a/dwds/test/build_daemon_evaluate_test.dart
+++ b/dwds/test/build_daemon_evaluate_test.dart
@@ -126,6 +126,27 @@
             await setup.service.resume(isolate.id);
           });
 
+          test('with scope override is not supported yet', () async {
+            await onBreakPoint(isolate.id, mainScript, 'printLocal', () async {
+              var event = await stream.firstWhere(
+                  (event) => event.kind == EventKind.kPauseBreakpoint);
+
+              var object = await setup.service.evaluateInFrame(
+                  isolate.id, event.topFrame.index, 'MainClass(0)');
+
+              var param = object as InstanceRef;
+
+              expect(
+                  () => setup.service.evaluateInFrame(
+                        isolate.id,
+                        event.topFrame.index,
+                        't.toString()',
+                        scope: {'t': param.id},
+                      ),
+                  throwsRPCError);
+            });
+          });
+
           test('local', () async {
             await onBreakPoint(isolate.id, mainScript, 'printLocal', () async {
               var event = await stream.firstWhere(
@@ -477,6 +498,24 @@
 
           tearDown(() async {});
 
+          test('with scope override', () async {
+            var library = isolate.rootLib;
+            var object = await setup.service
+                .evaluate(isolate.id, library.id, 'MainClass(0)');
+
+            var param = object as InstanceRef;
+            var result = await setup.service.evaluate(
+                isolate.id, library.id, 't.toString()',
+                scope: {'t': param.id});
+
+            expect(
+                result,
+                const TypeMatcher<InstanceRef>().having(
+                    (instance) => instance.valueAsString,
+                    'valueAsString',
+                    '0'));
+          });
+
           test('uses symbol from the same library', () async {
             var library = isolate.rootLib;
             var result = await setup.service