Enable expression evaluation in debugger for web platform (#53595)

diff --git a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
index e2e02d4..030b75c 100644
--- a/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
+++ b/packages/flutter_tools/lib/src/build_runner/devfs_web.dart
@@ -36,6 +36,39 @@
 import '../web/bootstrap.dart';
 import '../web/chrome.dart';
 
+/// An expression compiler connecting to FrontendServer
+///
+/// This is only used in development mode
+class WebExpressionCompiler implements ExpressionCompiler {
+  WebExpressionCompiler(this._generator);
+
+  final ResidentCompiler _generator;
+
+  @override
+  Future<ExpressionCompilationResult> compileExpressionToJs(
+    String isolateId,
+    String libraryUri,
+    int line,
+    int column,
+    Map<String, String> jsModules,
+    Map<String, String> jsFrameValues,
+    String moduleName,
+    String expression,
+  ) async {
+    final CompilerOutput compilerOutput = await _generator.compileExpressionToJs(libraryUri,
+        line, column, jsModules, jsFrameValues, moduleName, expression);
+
+    if (compilerOutput != null && compilerOutput.outputFilename != null) {
+      final String content = utf8.decode(
+          globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
+      return ExpressionCompilationResult(
+          content, compilerOutput.errorCount > 0);
+    }
+
+    throw Exception('Failed to compile $expression');
+  }
+}
+
 /// A web server which handles serving JavaScript and assets.
 ///
 /// This is only used in development mode.
@@ -85,7 +118,8 @@
     UrlTunneller urlTunneller,
     BuildMode buildMode,
     bool enableDwds,
-    Uri entrypoint, {
+    Uri entrypoint,
+    ExpressionCompiler expressionCompiler, {
     bool testMode = false,
   }) async {
     try {
@@ -170,6 +204,7 @@
           serverPathForModule,
           serverPathForAppUri,
         ),
+        expressionCompiler: expressionCompiler
       );
       shelf.Pipeline pipeline = const shelf.Pipeline();
       if (enableDwds) {
@@ -501,6 +536,7 @@
     @required this.buildMode,
     @required this.enableDwds,
     @required this.entrypoint,
+    @required this.expressionCompiler,
     this.testMode = false,
   });
 
@@ -512,6 +548,7 @@
   final BuildMode buildMode;
   final bool enableDwds;
   final bool testMode;
+  final ExpressionCompiler expressionCompiler;
 
   WebAssetServer webAssetServer;
 
@@ -572,6 +609,7 @@
       buildMode,
       enableDwds,
       entrypoint,
+      expressionCompiler,
       testMode: testMode,
     );
     _baseUri = Uri.parse('http://$hostname:$port');
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
index 8e8b156..a014da4 100644
--- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -421,6 +421,7 @@
           buildMode: debuggingOptions.buildInfo.mode,
           enableDwds: _enableDwds,
           entrypoint: globals.fs.file(target).uri,
+          expressionCompiler: WebExpressionCompiler(device.generator),
         );
         final Uri url = await device.devFS.create();
         if (debuggingOptions.buildInfo.isDebug) {
diff --git a/packages/flutter_tools/lib/src/codegen.dart b/packages/flutter_tools/lib/src/codegen.dart
index e49cd0a..8e8c9ac 100644
--- a/packages/flutter_tools/lib/src/codegen.dart
+++ b/packages/flutter_tools/lib/src/codegen.dart
@@ -161,6 +161,15 @@
   }
 
   @override
+  Future<CompilerOutput> compileExpressionToJs(
+    String libraryUri, int line, int column, Map<String, String> jsModules,
+    Map<String, String> jsFrameValues, String moduleName, String expression
+  ) {
+    return _residentCompiler.compileExpressionToJs(
+      libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression);
+  }
+
+  @override
   Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, {String outputPath, String packagesFilePath}) async {
     if (_codegenDaemon.lastStatus != CodegenStatus.Succeeded && _codegenDaemon.lastStatus != CodegenStatus.Failed) {
       await _codegenDaemon.buildResults.firstWhere((CodegenStatus status) {
diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart
index 7abe67c..7c6307c 100644
--- a/packages/flutter_tools/lib/src/commands/update_packages.dart
+++ b/packages/flutter_tools/lib/src/commands/update_packages.dart
@@ -26,7 +26,8 @@
   'mockito': '^4.1.0',  // Prevent mockito from downgrading to 4.0.0
   'vm_service_client': '0.2.6+2', // Final version before being marked deprecated.
   'video_player': '0.10.6', // 0.10.7 fails a gallery smoke test for toString.
-  'package_config': '1.9.1'
+  'package_config': '1.9.1',
+  'flutter_template_images': '1.0.0', // 1.0.1 breaks windows tests
 };
 
 class UpdatePackagesCommand extends FlutterCommand {
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index 2694ab0..cdfa2fc 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -431,6 +431,31 @@
       compiler._compileExpression(this);
 }
 
+class _CompileExpressionToJsRequest extends _CompilationRequest {
+  _CompileExpressionToJsRequest(
+    Completer<CompilerOutput> completer,
+    this.libraryUri,
+    this.line,
+    this.column,
+    this.jsModules,
+    this.jsFrameValues,
+    this.moduleName,
+    this.expression,
+  ) : super(completer);
+
+  final String libraryUri;
+  final int line;
+  final int column;
+  final Map<String, String> jsModules;
+  final Map<String, String> jsFrameValues;
+  final String moduleName;
+  final String expression;
+
+  @override
+  Future<CompilerOutput> _run(DefaultResidentCompiler compiler) async =>
+      compiler._compileExpressionToJs(this);
+}
+
 class _RejectRequest extends _CompilationRequest {
   _RejectRequest(Completer<CompilerOutput> completer) : super(completer);
 
@@ -489,6 +514,36 @@
     bool isStatic,
   );
 
+  /// Compiles [expression] in [libraryUri] at [line]:[column] to JavaScript
+  /// in [moduleName].
+  ///
+  /// Values listed in [jsFrameValues] are substituted for their names in the
+  /// [expression].
+  ///
+  /// Ensures that all [jsModules] are loaded and accessible inside the
+  /// expression.
+  ///
+  /// Example values of parameters:
+  /// [moduleName] is of the form '/packages/hello_world_main.dart'
+  /// [jsFrameValues] is a map from js variable name to its primitive value
+  /// or another variable name, for example
+  /// { 'x': '1', 'y': 'y', 'o': 'null' }
+  /// [jsModules] is a map from variable name to the module name, where
+  /// variable name is the name originally used in JavaScript to contain the
+  /// module object, for example:
+  /// { 'dart':'dart_sdk', 'main': '/packages/hello_world_main.dart' }
+  /// Returns a [CompilerOutput] including the name of the file containing the
+  /// compilation result and a number of errors
+  Future<CompilerOutput> compileExpressionToJs(
+    String libraryUri,
+    int line,
+    int column,
+    Map<String, String> jsModules,
+    Map<String, String> jsFrameValues,
+    String moduleName,
+    String expression,
+  );
+
   /// Should be invoked when results of compilation are accepted by the client.
   ///
   /// Either [accept] or [reject] should be called after every [recompile] call.
@@ -779,6 +834,52 @@
   }
 
   @override
+  Future<CompilerOutput> compileExpressionToJs(
+    String libraryUri,
+    int line,
+    int column,
+    Map<String, String> jsModules,
+    Map<String, String> jsFrameValues,
+    String moduleName,
+    String expression,
+  ) {
+    if (!_controller.hasListener) {
+      _controller.stream.listen(_handleCompilationRequest);
+    }
+
+    final Completer<CompilerOutput> completer = Completer<CompilerOutput>();
+    _controller.add(
+        _CompileExpressionToJsRequest(
+            completer, libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression)
+    );
+    return completer.future;
+  }
+
+  Future<CompilerOutput> _compileExpressionToJs(_CompileExpressionToJsRequest request) async {
+    _stdoutHandler.reset(suppressCompilerMessages: true, expectSources: false);
+
+    // 'compile-expression-to-js' should be invoked after compiler has been started,
+    // program was compiled.
+    if (_server == null) {
+      return null;
+    }
+
+    final String inputKey = Uuid().generateV4();
+    _server.stdin.writeln('compile-expression-to-js $inputKey');
+    _server.stdin.writeln(request.libraryUri ?? '');
+    _server.stdin.writeln(request.line);
+    _server.stdin.writeln(request.column);
+    request.jsModules?.forEach((String k, String v) { _server.stdin.writeln('$k:$v'); });
+    _server.stdin.writeln(inputKey);
+    request.jsFrameValues?.forEach((String k, String v) { _server.stdin.writeln('$k:$v'); });
+    _server.stdin.writeln(inputKey);
+    _server.stdin.writeln(request.moduleName ?? '');
+    _server.stdin.writeln(request.expression ?? '');
+
+    return _stdoutHandler.compilerOutput.future;
+  }
+
+  @override
   void accept() {
     if (_compileRequestNeedsConfirmation) {
       _server.stdin.writeln('accept');
diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml
index 27a7736..590e377 100644
--- a/packages/flutter_tools/pubspec.yaml
+++ b/packages/flutter_tools/pubspec.yaml
@@ -11,7 +11,7 @@
   # To update these, use "flutter update-packages --force-upgrade".
   archive: 2.0.13
   args: 1.6.0
-  dwds: 3.0.1
+  dwds: 3.0.2
   completion: 0.2.2
   coverage: 0.13.9
   crypto: 2.1.4
@@ -112,4 +112,4 @@
   # Exclude this package from the hosted API docs.
   nodoc: true
 
-# PUBSPEC CHECKSUM: 65c1
+# PUBSPEC CHECKSUM: 53c2
diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
index b4adf27..d7c1abf 100644
--- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
@@ -373,6 +373,7 @@
       enableDwds: false,
       entrypoint: Uri.base,
       testMode: true,
+      expressionCompiler: null,
     );
     webDevFS.requireJS.createSync(recursive: true);
     webDevFS.stackTraceMapper.createSync(recursive: true);
diff --git a/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart b/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart
new file mode 100644
index 0000000..f362026
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/expression_evaluation_web_test.dart
@@ -0,0 +1,178 @@
+// Copyright 2014 The Flutter Authors. 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 'dart:io';
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+
+import 'package:vm_service/vm_service.dart';
+
+import '../src/common.dart';
+import 'test_data/basic_project.dart';
+import 'test_data/tests_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void batch1() {
+  final BasicProject _project = BasicProject();
+  Directory tempDir;
+  FlutterRunTestDriver _flutter;
+
+  Future<void> initProject() async {
+    tempDir = createResolvedTempDirectorySync('run_expression_eval_test.');
+    await _project.setUpIn(tempDir);
+    _flutter = FlutterRunTestDriver(tempDir);
+  }
+
+  Future<void> cleanProject() async {
+    await _flutter.stop();
+    tryToDelete(tempDir);
+  }
+
+  Future<void> breakInBuildMethod(FlutterTestDriver flutter) async {
+    await _flutter.breakAt(
+      _project.buildMethodBreakpointUri,
+      _project.buildMethodBreakpointLine,
+    );
+  }
+
+  Future<void> breakInTopLevelFunction(FlutterTestDriver flutter) async {
+    await _flutter.breakAt(
+      _project.topLevelFunctionBreakpointUri,
+      _project.topLevelFunctionBreakpointLine,
+    );
+  }
+
+  test('flutter run expression evaluation - can evaluate trivial expressions in top level function', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInTopLevelFunction(_flutter);
+    await evaluateTrivialExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter run expression evaluation - can evaluate trivial expressions in build method', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInBuildMethod(_flutter);
+    await evaluateTrivialExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter run expression evaluation - can evaluate complex expressions in top level function', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInTopLevelFunction(_flutter);
+    await evaluateComplexExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter run expression evaluation - can evaluate complex expressions in build method', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInBuildMethod(_flutter);
+    await evaluateComplexExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter run expression evaluation - can evaluate expressions returning complex objects in top level function', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInTopLevelFunction(_flutter);
+    await evaluateComplexReturningExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'Evaluate on objects is not supported for web yet');
+
+  test('flutter run expression evaluation - can evaluate expressions returning complex objects in build method', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true);
+    await breakInBuildMethod(_flutter);
+    await evaluateComplexReturningExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'Evaluate on objects is not supported for web yet');
+}
+
+void batch2() {
+  final TestsProject _project = TestsProject();
+  Directory tempDir;
+  FlutterRunTestDriver _flutter;
+
+  Future<void> initProject() async {
+    tempDir = createResolvedTempDirectorySync('test_expression_eval_test.');
+    await _project.setUpIn(tempDir);
+    _flutter = FlutterRunTestDriver(tempDir);
+  }
+
+  Future<void> cleanProject() async {
+    await _flutter.stop();
+    tryToDelete(tempDir);
+  }
+
+  Future<void> breakInMethod(FlutterTestDriver flutter) async {
+    await _flutter.breakAt(
+      _project.breakpointAppUri,
+      _project.breakpointLine,
+    );
+  }
+
+  test('flutter test expression evaluation - can evaluate trivial expressions in a test', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+    await breakInMethod(_flutter);
+    await evaluateTrivialExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter test expression evaluation - can evaluate complex expressions in a test', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+    await breakInMethod(_flutter);
+    await evaluateComplexExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'CI not setup for web tests'); // https://github.com/flutter/flutter/issues/53779
+
+  test('flutter test expression evaluation - can evaluate expressions returning complex objects in a test', () async {
+    await initProject();
+    await _flutter.run(withDebugger: true, chrome: true, script: _project.testFilePath);
+    await breakInMethod(_flutter);
+    await evaluateComplexReturningExpressions(_flutter);
+    await cleanProject();
+  }, skip: 'Evaluate on objects is not supported for web yet');
+}
+
+Future<void> evaluateTrivialExpressions(FlutterTestDriver flutter) async {
+  InstanceRef res;
+
+  res = await flutter.evaluateInFrame('"test"');
+  expect(res.kind == InstanceKind.kString && res.valueAsString == 'test', isTrue);
+
+  res = await flutter.evaluateInFrame('1');
+  expect(res.kind == InstanceKind.kDouble && res.valueAsString == 1.toString(), isTrue);
+
+  res = await flutter.evaluateInFrame('true');
+  expect(res.kind == InstanceKind.kBool && res.valueAsString == true.toString(), isTrue);
+}
+
+Future<void> evaluateComplexExpressions(FlutterTestDriver flutter) async {
+  final InstanceRef res = await flutter.evaluateInFrame('new DateTime.now().year');
+  expect(res.kind == InstanceKind.kDouble && res.valueAsString == DateTime.now().year.toString(), isTrue);
+}
+
+Future<void> evaluateComplexReturningExpressions(FlutterTestDriver flutter) async {
+  final DateTime now = DateTime.now();
+  final InstanceRef resp = await flutter.evaluateInFrame('new DateTime.now()');
+  expect(resp.classRef.name, equals('DateTime'));
+  // Ensure we got a reasonable approximation. The more accurate we try to
+  // make this, the more likely it'll fail due to differences in the time
+  // in the remote VM and the local VM at the time the code runs.
+  final InstanceRef res = await flutter.evaluate(resp.id, r'"$year-$month-$day"');
+  expect(res.valueAsString, equals('${now.year}-${now.month}-${now.day}'));
+}
+
+void main() {
+  batch1();
+  batch2();
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
index c452bb6..566c052 100644
--- a/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
+++ b/packages/flutter_tools/test/integration.shard/test_data/tests_project.dart
@@ -49,6 +49,7 @@
   String get testFilePath => globals.fs.path.join(dir.path, 'test', 'test.dart');
 
   Uri get breakpointUri => Uri.file(testFilePath);
+  Uri get breakpointAppUri => Uri.parse('org-dartlang-app:///test.dart');
 
   int get breakpointLine => lineContaining(testContent, '// BREAKPOINT');
 }
diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart
index c8193e7..0c3e0c9 100644
--- a/packages/flutter_tools/test/integration.shard/test_driver.dart
+++ b/packages/flutter_tools/test/integration.shard/test_driver.dart
@@ -436,6 +436,7 @@
     bool pauseOnExceptions = false,
     bool chrome = false,
     File pidFile,
+    String script,
   }) async {
     await _setupProcess(
       <String>[
@@ -453,6 +454,7 @@
       startPaused: startPaused,
       pauseOnExceptions: pauseOnExceptions,
       pidFile: pidFile,
+      script: script,
     );
   }
 
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 5a8d2d7..ec57c94 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -646,6 +646,20 @@
   ) async {
     return null;
   }
+
+  @override
+  Future<CompilerOutput> compileExpressionToJs(
+    String libraryUri,
+    int line,
+    int column,
+    Map<String, String> jsModules,
+    Map<String, String> jsFrameValues,
+    String moduleName,
+    String expression,
+  ) async {
+    return null;
+  }
+
   @override
   Future<CompilerOutput> recompile(String mainPath, List<Uri> invalidatedFiles, { String outputPath, String packagesFilePath }) async {
     globals.fs.file(outputPath).createSync(recursive: true);