Version 3.11.0-34.0.dev

Merge bf3e8e923ae30426c20e7cc0c20ba6cfea9d3bfe into dev
diff --git a/pkg/analysis_server/lib/src/computer/computer_highlights.dart b/pkg/analysis_server/lib/src/computer/computer_highlights.dart
index 5e2f93d..bd7bdcb 100644
--- a/pkg/analysis_server/lib/src/computer/computer_highlights.dart
+++ b/pkg/analysis_server/lib/src/computer/computer_highlights.dart
@@ -19,7 +19,6 @@
     show SemanticTokenInfo;
 import 'package:analysis_server/src/lsp/semantic_tokens/mapping.dart'
     show highlightRegionTokenModifiers, highlightRegionTokenTypes;
-import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/token.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element.dart';
@@ -30,6 +29,7 @@
 import 'package:analyzer/src/dart/element/extensions.dart';
 import 'package:analyzer/src/utilities/extensions/ast.dart';
 import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element;
+import 'package:collection/collection.dart';
 
 /// A computer for [HighlightRegion]s and LSP [SemanticTokenInfo] in a Dart [CompilationUnit].
 class DartUnitHighlightsComputer {
@@ -674,16 +674,42 @@
   /// Returns whether [nameToken] is a reference to the `call` method on
   /// a function.
   bool _isCallMethod(AstNode parent, Token nameToken) {
+    late bool enclosingInstanceFunction =
+        switch (parent.enclosingInstanceElement) {
+          ExtensionElement(:var extendedType) => extendedType.isFunction,
+          _ => false,
+        };
+    Expression? expression() => switch (parent) {
+      ExpressionFunctionBody(:var expression) ||
+      ExpressionStatement(:var expression) => expression,
+      ReturnStatement(:var expression) => expression,
+      ArgumentList(:var arguments) => arguments.firstWhereOrNull((argument) {
+        return argument is SimpleIdentifier && argument.token == nameToken;
+      }),
+      AssignmentExpression(:var rightHandSide) => rightHandSide,
+      VariableDeclaration(:var initializer) => initializer,
+      _ => null,
+    };
     return // Invocation
-    parent is MethodInvocation &&
+    (parent is MethodInvocation &&
             parent.methodName.token == nameToken &&
             parent.methodName.name == MethodElement.CALL_METHOD_NAME &&
-            parent.realTarget?.staticType is FunctionType ||
+            ((parent.realTarget?.staticType).isFunction ||
+                enclosingInstanceFunction)) ||
         // Tearoff
         (parent is PrefixedIdentifier &&
             parent.identifier.token == nameToken &&
             parent.identifier.name == MethodElement.CALL_METHOD_NAME &&
-            parent.prefix.staticType is FunctionType);
+            parent.prefix.staticType.isFunction) ||
+        // Property access
+        (parent is PropertyAccess &&
+            parent.propertyName.token == nameToken &&
+            parent.propertyName.name == MethodElement.CALL_METHOD_NAME &&
+            parent.realTarget.staticType.isFunction) ||
+        // Special cases for extension methods
+        (expression() is SimpleIdentifier &&
+            nameToken.lexeme == MethodElement.CALL_METHOD_NAME &&
+            enclosingInstanceFunction);
   }
 
   void _reset() {
@@ -2084,3 +2110,8 @@
     (false, false, false) => Quote.Double,
   };
 }
+
+extension on DartType? {
+  bool get isFunction =>
+      this is FunctionType || (this?.isDartCoreFunction ?? false);
+}
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/create_class.dart b/pkg/analysis_server/lib/src/services/correction/dart/create_class.dart
index 71ea8bc..d0e59a0 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/create_class.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/create_class.dart
@@ -7,6 +7,7 @@
 import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
@@ -178,6 +179,23 @@
 
   @override
   Future<void> compute(ChangeBuilder builder) async {
+    Expression? expression;
+    if (_targetNode is Expression) {
+      expression = _targetNode;
+    } else if (_targetNode.parent case Expression parent) {
+      expression = parent;
+    }
+    if (expression != null) {
+      var fieldType = inferUndefinedExpressionType(expression);
+      if (fieldType is InvalidType) {
+        return;
+      }
+      if (fieldType != null &&
+          (!typeSystem.isAssignableTo(fieldType, typeProvider.typeType) ||
+              !typeSystem.isSubtypeOf(fieldType, typeProvider.objectType))) {
+        return;
+      }
+    }
     // prepare environment
     LibraryFragment targetUnit;
     var offset = -1;
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/create_method_or_function.dart b/pkg/analysis_server/lib/src/services/correction/dart/create_method_or_function.dart
index d9b2cfe..957e69c 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/create_method_or_function.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/create_method_or_function.dart
@@ -146,7 +146,6 @@
   }) async {
     // build method source
     await builder.addDartFileEdit(targetFile, (builder) {
-      var eol = builder.eol;
       builder.addInsertion(insertOffset, (builder) {
         if (leadingEol) {
           builder.writeln();
@@ -175,7 +174,7 @@
           builder.write(' async');
         }
         // close method
-        builder.write(' {$eol$prefix}');
+        builder.write(' {}');
         if (trailingEol) {
           builder.writeln();
         }
diff --git a/pkg/analysis_server/test/lsp/semantic_tokens_test.dart b/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
index 6aee1a5..943b097 100644
--- a/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
+++ b/pkg/analysis_server/test/lsp/semantic_tokens_test.dart
@@ -1213,6 +1213,148 @@
     await _initializeAndVerifyTokensInRange(content, expected);
   }
 
+  Future<void> test_function_callMethod_invocation_extension() async {
+    var content = r'''
+extension on void Function() {
+  m() => [!call()!];
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_function_callMethod_propertyAccess() async {
+    var content = r'''
+extension on void Function()? {
+  m() => [!this?.call!];
+}
+''';
+
+    var expected = <_Token>[
+      _Token('this', SemanticTokenTypes.keyword),
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_function_callMethod_simpleIdentifier() async {
+    var content = r'''
+extension on void Function() {
+  m() {
+    [!call!];
+  }
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_function_callMethod_simpleIdentifier_argument() async {
+    var content = r'''
+extension on void Function() {
+  m(void Function() f) {
+    m([!call!]);
+  }
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_function_callMethod_simpleIdentifier_assignment() async {
+    var content = r'''
+extension on void Function() {
+  m() {
+    var a;
+    a = [!call!];
+  }
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void>
+  test_function_callMethod_simpleIdentifier_expressionFunctionBody() async {
+    var content = r'''
+extension on void Function() {
+  m() => [!call!];
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_function_callMethod_simpleIdentifier_return() async {
+    var content = r'''
+extension on void Function() {
+  m() {
+    return [!call!];
+  }
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void>
+  test_function_callMethod_simpleIdentifier_variableDeclaration() async {
+    var content = r'''
+extension on void Function() {
+  m() {
+    var _ = [!call!];
+  }
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
   Future<void> test_function_callMethod_tearOff() async {
     var content = r'''
 f(void Function(int) x) {
@@ -1230,6 +1372,39 @@
     await _initializeAndVerifyTokensInRange(content, expected);
   }
 
+  Future<void> test_functionType_callMethod_invocation_extension() async {
+    var content = r'''
+extension on Function {
+  m() => [!call()!];
+}
+''';
+
+    var expected = <_Token>[
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
+  Future<void> test_functionType_callMethod_tearOff() async {
+    var content = r'''
+f(Function x) {
+  [!x.call!];
+}
+''';
+
+    var expected = [
+      _Token('x', SemanticTokenTypes.parameter),
+      _Token('call', SemanticTokenTypes.method, [
+        CustomSemanticTokenModifiers.instance,
+      ]),
+    ];
+
+    await _initializeAndVerifyTokensInRange(content, expected);
+  }
+
   /// Verify that sending a semantic token request immediately after an overlay
   /// update (with no delay) does not result in corrupt semantic tokens because
   /// the previous file content was used.
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_class_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_class_test.dart
index 31a87a6..3b09117 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_class_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_class_test.dart
@@ -25,6 +25,18 @@
   @override
   FixKind get kind => DartFixKind.createClassLowercase;
 
+  Future<void> test_ifNull_notType() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertNoFix();
+  }
+
   Future<void> test_instanceMethod_noFix() async {
     await resolveTestCode('''
 class C {}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
index ee7c875..5753bf2 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_extension_member_test.dart
@@ -815,6 +815,27 @@
 ''');
   }
 
+  Future<void> test_ifNull() async {
+    await resolveTestCode('''
+extension E on Object {
+  int Function()? get _f => null;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertHasFix('''
+extension E on Object {
+  int Function()? get _f => null;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+
+  int _defaultF() {}
+}
+''');
+  }
+
   Future<void> test_main_part() async {
     var partPath = join(testPackageLibPath, 'part.dart');
     newFile(partPath, '''
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_field_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_field_test.dart
index e01ef6b..e25b29f 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_field_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_field_test.dart
@@ -555,6 +555,27 @@
 ''');
   }
 
+  Future<void> test_ifNull() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  int Function()? _f;
+
+  int Function() _defaultF;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+  }
+
   Future<void> test_importType() async {
     newFile('$testPackageLibPath/a.dart', r'''
 class A {}
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_function_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_function_test.dart
index 27ac2f0..c614ffd 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_function_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_function_test.dart
@@ -28,8 +28,7 @@
     await assertHasFix('''
 bool Function() f = g;
 
-bool g() {
-}
+bool g() {}
 ''');
   }
 
@@ -60,8 +59,7 @@
   a..ma().useFunction(test);
 }
 
-int test(double a, String b) {
-}
+int test(double a, String b) {}
 ''');
   }
 
@@ -78,8 +76,7 @@
 }
 useFunction({Function? g}) {}
 
-test() {
-}
+test() {}
 ''');
   }
 
@@ -96,8 +93,7 @@
 }
 useFunction(int g(a, b)) {}
 
-int test(a, b) {
-}
+int test(a, b) {}
 ''');
   }
 
@@ -114,8 +110,7 @@
 }
 useFunction(int g(double a, String b)) {}
 
-int test(double a, String b) {
-}
+int test(double a, String b) {}
 ''');
   }
 
@@ -132,8 +127,7 @@
 }
 useFunction({int g(double a, String b)?}) {}
 
-int test(double a, String b) {
-}
+int test(double a, String b) {}
 ''');
   }
 
@@ -152,8 +146,7 @@
 
 void f2(int Function() f) {}
 
-int f3() {
-}
+int f3() {}
 ''');
   }
 
@@ -181,8 +174,7 @@
   useFunction(test);
 }
 
-int test(A a) {
-}
+int test(A a) {}
 ''');
   }
 
@@ -201,8 +193,7 @@
 
 void f2(int Function(int) f) {}
 
-int f3(int p1) {
-}
+int f3(int p1) {}
 ''');
   }
 
@@ -221,8 +212,7 @@
 
 void f2(int Function(int) f) {}
 
-int f3(int p1) {
-}
+int f3(int p1) {}
 ''');
   }
 
@@ -241,8 +231,7 @@
 
 void f2((int, int Function(int)) f) {}
 
-int f3(int p1) {
-}
+int f3(int p1) {}
 ''');
   }
 
@@ -261,9 +250,25 @@
 
 void f2(({int Function(int) f}) f) {}
 
-int f3(int p1) {
+int f3(int p1) {}
+''');
+  }
+
+  Future<void> test_ifNull() async {
+    await resolveTestCode('''
+int Function()? _f;
+int Function() get f {
+  return _f ?? _defaultF;
 }
 ''');
+    await assertHasFix('''
+int Function()? _f;
+int Function() get f {
+  return _f ?? _defaultF;
+}
+
+int _defaultF() {}
+''');
   }
 
   Future<void> test_record_tearoff() async {
@@ -273,8 +278,7 @@
     await assertHasFix('''
 (bool Function(),) f = (g,);
 
-bool g() {
-}
+bool g() {}
 ''');
   }
 
@@ -297,8 +301,7 @@
   f(test);
 }
 
-A<int> test() {
-}
+A<int> test() {}
 ''');
   }
 }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
index fc4fc1c..49c4ac9 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_getter_test.dart
@@ -302,6 +302,94 @@
 ''');
   }
 
+  Future<void> test_ifNull_LHS() async {
+    await resolveTestCode('''
+class A {
+  late int Function() _f;
+
+  void f() {
+    _defaultF ?? _f;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  late int Function() _f;
+
+  int Function()? get _defaultF => null;
+
+  void f() {
+    _defaultF ?? _f;
+  }
+}
+''');
+  }
+
+  Future<void> test_ifNull_LHS_return() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _defaultF ?? _f;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _defaultF ?? _f;
+  }
+
+  int Function()? get _defaultF => null;
+}
+''');
+  }
+
+  Future<void> test_ifNull_RHS() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+
+  void f() {
+    _f ?? _defaultF;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  int Function()? _f;
+
+  int Function()? get _defaultF => null;
+
+  void f() {
+    _f ?? _defaultF;
+  }
+}
+''');
+  }
+
+  Future<void> test_ifNull_RHS_return() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+
+  int Function() get _defaultF => null;
+}
+''');
+  }
+
   Future<void> test_inExtensionGetter_class() async {
     await resolveTestCode('''
 class A {
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_method_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_method_test.dart
index e690591..ed2c455 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_method_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_method_test.dart
@@ -252,8 +252,7 @@
     useFunction(test);
   }
 
-  static int test(double a, String b) {
-  }
+  static int test(double a, String b) {}
 }
 
 useFunction(int g(double a, String b)) {}
@@ -277,8 +276,7 @@
 }
 
 mixin M {
-  int test(double a, String b) {
-  }
+  int test(double a, String b) {}
 }
 
 useFunction(int g(double a, String b)) {}
@@ -304,8 +302,7 @@
 part of 'test.dart';
 
 mixin M {
-  void myUndefinedMethod() {
-  }
+  void myUndefinedMethod() {}
 }
 ''', target: partPath);
   }
@@ -329,8 +326,7 @@
 part 'test.dart';
 
 mixin M {
-  void myUndefinedMethod() {
-  }
+  void myUndefinedMethod() {}
 }
 ''', target: mainPath);
   }
@@ -358,8 +354,7 @@
 part of 'main.dart';
 
 mixin M {
-  void myUndefinedMethod() {
-  }
+  void myUndefinedMethod() {}
 }
 ''', target: part1Path);
   }
@@ -387,8 +382,7 @@
 enum E {
   e1,
   e2;
-  int bar() {
-  }
+  int bar() {}
 }
 
 void g(int Function() f) {}
@@ -416,8 +410,7 @@
 enum E {
   e1,
   e2;
-  static int bar() {
-  }
+  static int bar() {}
 }
 
 void g(int Function() f) {}
@@ -441,8 +434,7 @@
 ''');
     await assertHasFix('''
 extension type E(int i) {
-  int bar() {
-  }
+  int bar() {}
 }
 
 void g(int Function() f) {}
@@ -466,8 +458,7 @@
 ''');
     await assertHasFix('''
 extension type E(int i) {
-  static int bar() {
-  }
+  static int bar() {}
 }
 
 void g(int Function() f) {}
@@ -496,8 +487,7 @@
 
   void m2(int Function(int) f) {}
 
-  int m3(int p1) {
-  }
+  int m3(int p1) {}
 }
 ''');
   }
@@ -517,8 +507,7 @@
     useFunction(test);
   }
 
-  static int test(double a, String b) {
-  }
+  static int test(double a, String b) {}
 }
 useFunction(int g(double a, String b)) {}
 ''');
@@ -537,8 +526,7 @@
   var f;
   A() : f = useFunction(test);
 
-  static int test(double a, String b) {
-  }
+  static int test(double a, String b) {}
 }
 useFunction(int g(double a, String b)) {}
 ''');
@@ -562,8 +550,7 @@
 
   void m2(int Function() f) {}
 
-  int m3() {
-  }
+  int m3() {}
 }
 ''');
   }
@@ -587,8 +574,7 @@
 
   void m2(int Function(int) f) {}
 
-  int m3(int p1) {
-  }
+  int m3(int p1) {}
 }
 ''');
   }
@@ -612,8 +598,7 @@
 
   void m2(int Function(int) f) {}
 
-  int m3(int p1) {
-  }
+  int m3(int p1) {}
 }
 ''');
   }
@@ -636,8 +621,7 @@
 
   void m2((int Function(int),) f) {}
 
-  int m3(int p1) {
-  }
+  int m3(int p1) {}
 }
 ''');
   }
@@ -661,8 +645,7 @@
 
   void m2(({int Function(int) f}) f) {}
 
-  int m3(int p1) {
-  }
+  int m3(int p1) {}
 }
 ''');
   }
@@ -681,8 +664,7 @@
   useFunction(a.test);
 }
 class A {
-  int test(double a, String b) {
-  }
+  int test(double a, String b) {}
 }
 useFunction(int g(double a, String b)) {}
 ''');
@@ -705,13 +687,33 @@
 class A {
   m() {}
 
-  int test(double a, String b) {
-  }
+  int test(double a, String b) {}
 }
 useFunction(int g(double a, String b)) {}
 ''');
   }
 
+  Future<void> test_ifNull() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertHasFix('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+
+  int _defaultF() {}
+}
+''');
+  }
+
   Future<void> test_record_named_tearoff() async {
     await resolveTestCode('''
 class A {
@@ -722,8 +724,7 @@
 class A {
   ({bool Function() fn,}) m() => (fn: m2,);
 
-  bool m2() {
-  }
+  bool m2() {}
 }
 ''');
   }
@@ -738,8 +739,7 @@
 class A {
   (bool Function(),) m() => (m2,);
 
-  bool m2() {
-  }
+  bool m2() {}
 }
 ''');
   }
@@ -763,8 +763,7 @@
 Future<T> g<T>(Future<T> Function() foo) => foo();
 
 class C {
-  static Future<int> foo() async {
-  }
+  static Future<int> foo() async {}
 }
 ''');
   }
@@ -792,8 +791,7 @@
 Future<T> g<T>(Future<S> Function<S>() foo) => foo();
 
 class C {
-  static Future<S> foo<S>() async {
-  }
+  static Future<S> foo<S>() async {}
 }
 ''');
   }
@@ -821,8 +819,7 @@
 Future<T> g<T>(Future<S> Function<S extends num>() foo) => foo();
 
 class C {
-  static Future<S> foo<S extends num>() async {
-  }
+  static Future<S> foo<S extends num>() async {}
 }
 ''');
   }
@@ -850,8 +847,7 @@
 Future<T> g<T extends num>(Future<S> Function<S extends T>() foo) => foo();
 
 class C {
-  static Future<S> foo<S extends num>() async {
-  }
+  static Future<S> foo<S extends num>() async {}
 }
 ''');
   }
@@ -879,8 +875,7 @@
 Future<T> g<T>(Future<S> Function<S>() foo) => foo();
 
 class C<S> {
-  static Future<S> foo<S>() async {
-  }
+  static Future<S> foo<S>() async {}
 }
 ''');
   }
diff --git a/pkg/analysis_server/test/src/services/correction/fix/create_mixin_test.dart b/pkg/analysis_server/test/src/services/correction/fix/create_mixin_test.dart
index 59b04a2..89d9f12 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/create_mixin_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/create_mixin_test.dart
@@ -25,6 +25,18 @@
   @override
   FixKind get kind => DartFixKind.createMixinLowercase;
 
+  Future<void> test_ifNull_notType() async {
+    await resolveTestCode('''
+class A {
+  int Function()? _f;
+  int Function() get f {
+    return _f ?? _defaultF;
+  }
+}
+''');
+    await assertNoFix();
+  }
+
   Future<void> test_lowercaseAssignment() async {
     await resolveTestCode('''
 newName? a;
diff --git a/pkg/analysis_server/test/src/services/correction/fix/ignore_diagnostic_test.dart b/pkg/analysis_server/test/src/services/correction/fix/ignore_diagnostic_test.dart
index 4417b81..e70f8bb 100644
--- a/pkg/analysis_server/test/src/services/correction/fix/ignore_diagnostic_test.dart
+++ b/pkg/analysis_server/test/src/services/correction/fix/ignore_diagnostic_test.dart
@@ -4,6 +4,7 @@
 
 import 'package:analysis_server_plugin/src/correction/ignore_diagnostic.dart';
 import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
+import 'package:linter/src/lint_names.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import 'fix_processor.dart';
@@ -219,7 +220,7 @@
     await assertHasFix('''
 // Copyright header.
 
-// ignore_for_file: referenced_before_declaration, unused_local_variable
+// ignore_for_file: unused_local_variable, referenced_before_declaration
 
 // Some other header.
 
@@ -339,7 +340,7 @@
 ''');
     await assertHasFix('''
 void f() {
-  // ignore: undefined_identifier, unused_local_variable
+  // ignore: unused_local_variable, undefined_identifier
   var a = b;
 }
 ''');
@@ -359,6 +360,22 @@
     await assertNoFix();
   }
 
+  Future<void> test_unknown_error_code() async {
+    createAnalysisOptionsFile(lints: [LintNames.always_specify_types]);
+    await resolveTestCode('''
+void f() {
+  // ignore: unused_local_variable, some text
+  var a = 1;
+}
+''');
+    await assertHasFix('''
+void f() {
+  // ignore: always_specify_types, unused_local_variable, some text
+  var a = 1;
+}
+''');
+  }
+
   Future<void> test_unusedCode() async {
     await resolveTestCode('''
 void f() {
diff --git a/pkg/analysis_server_plugin/lib/edit/dart/correction_producer.dart b/pkg/analysis_server_plugin/lib/edit/dart/correction_producer.dart
index 2f480d8..4f01338 100644
--- a/pkg/analysis_server_plugin/lib/edit/dart/correction_producer.dart
+++ b/pkg/analysis_server_plugin/lib/edit/dart/correction_producer.dart
@@ -644,15 +644,28 @@
         }
       }
     }
-    // `v + myFunction();`.
     if (parent is BinaryExpression) {
       var binary = parent;
       var method = binary.element;
+      // `v + myFunction();`.
       if (method != null) {
         if (binary.rightOperand == expression) {
           var parameters = method.formalParameters;
           return parameters.length == 1 ? parameters[0].type : null;
         }
+      } else if (binary.operator.type == TokenType.QUESTION_QUESTION) {
+        // `v ?? myFunction();`.
+        // This handles when the expression is being assigned somewhere.
+        var type = inferUndefinedExpressionType(binary);
+        if (binary.rightOperand == expression) {
+          return type ?? binary.leftOperand.staticType;
+        } else if (binary.leftOperand == expression) {
+          type ??= binary.rightOperand.staticType;
+          return switch (type) {
+            TypeImpl type => type.withNullability(NullabilitySuffix.question),
+            _ => null,
+          };
+        }
       }
     }
     // `foo( myFunction() );`.
diff --git a/pkg/analysis_server_plugin/lib/src/correction/ignore_diagnostic.dart b/pkg/analysis_server_plugin/lib/src/correction/ignore_diagnostic.dart
index e98ae01..ccaf603 100644
--- a/pkg/analysis_server_plugin/lib/src/correction/ignore_diagnostic.dart
+++ b/pkg/analysis_server_plugin/lib/src/correction/ignore_diagnostic.dart
@@ -134,7 +134,6 @@
     if (_isCodeUnignorable) return;
 
     await builder.addDartFileEdit(file, (builder) {
-      var eol = builder.eol;
       var source = unitResult.content;
 
       // Look for the last blank line in any leading comments (to insert after
@@ -153,21 +152,24 @@
       for (var lineNumber = 0; lineNumber < lineCount - 1; lineNumber++) {
         lineStart = unitResult.lineInfo.getOffsetOfLine(lineNumber);
         var nextLineStart = unitResult.lineInfo.getOffsetOfLine(lineNumber + 1);
-        var line = source.substring(lineStart, nextLineStart).trim();
+        var line = source.substring(lineStart, nextLineStart);
+        var trimmedLine = line.trim();
 
-        if (line.startsWith('// $commentPrefix:')) {
-          // Found an existing ignore; insert at the end of this line.
-          builder.addSimpleInsertion(nextLineStart - eol.length, ', $_code');
+        if (trimmedLine.startsWith(IgnoreInfo.ignoreForFileMatcher)) {
+          // Found an existing ignore; insert after `// ignore_for_file: `
+          // before any existing codes.
+          var insertOffset = lineStart + line.indexOf(':') + 1;
+          builder.addSimpleInsertion(insertOffset, ' $_code,');
           return;
         }
 
-        if (line.isEmpty) {
+        if (trimmedLine.isEmpty) {
           // Track last blank line, as we will insert there.
           lastBlankLineOffset = lineStart;
           continue;
         }
 
-        if (line.startsWith('#!') || line.startsWith('//')) {
+        if (trimmedLine.startsWith('#!') || trimmedLine.startsWith('//')) {
           // Skip comment/hash-bang.
           continue;
         }
@@ -201,7 +203,6 @@
     if (_isCodeUnignorable) return;
 
     await builder.addDartFileEdit(file, (builder) {
-      var eol = builder.eol;
       var offset = diagnostic.problemMessage.offset;
       var lineNumber = unitResult.lineInfo.getLocation(offset).lineNumber - 1;
 
@@ -216,12 +217,12 @@
         lineNumber - 1,
       );
       var lineStart = unitResult.lineInfo.getOffsetOfLine(lineNumber);
-      var line = unitResult.content
-          .substring(previousLineStart, lineStart)
-          .trim();
+      var line = unitResult.content.substring(previousLineStart, lineStart);
 
-      if (line.startsWith(IgnoreInfo.ignoreMatcher)) {
-        builder.addSimpleInsertion(lineStart - eol.length, ', $_code');
+      if (line.trim().startsWith(IgnoreInfo.ignoreMatcher)) {
+        // Add after the `// ignore: ` before any existing codes.
+        var insertOffset = previousLineStart + line.indexOf(':') + 1;
+        builder.addSimpleInsertion(insertOffset, ' $_code,');
       } else {
         insertAt(builder, lineStart);
       }
diff --git a/pkg/dart2wasm/benchmark/self_compile_benchmark.dart b/pkg/dart2wasm/benchmark/self_compile_benchmark.dart
index 6639785..7b37b69 100644
--- a/pkg/dart2wasm/benchmark/self_compile_benchmark.dart
+++ b/pkg/dart2wasm/benchmark/self_compile_benchmark.dart
@@ -11,7 +11,7 @@
 Future main(List<String> args) async {
   final sw = Stopwatch()..start();
   final fileSystem = WasmCompilerFileSystem();
-  final result = await compile(
+  final result = await compileBenchmark(
       fileSystem, 'pkg/dart2wasm/benchmark/self_compile_benchmark.dart');
   print('Dart2WasmSelfCompile(RunTimeRaw): ${sw.elapsed.inMilliseconds} ms.');
 
@@ -22,7 +22,7 @@
   }
 }
 
-Future<CompilationSuccess> compile(
+Future<CodegenResult> compileBenchmark(
     WasmCompilerFileSystem fileSystem, String mainFile) async {
   // Avoid CFE self-detecting whether `stdout`/`stderr` is terminal and supports
   // colors (as we don't have `dart:io` available when we run dart2wasm in a
@@ -35,12 +35,12 @@
   options.librariesSpecPath =
       Uri.file('${fileSystem.sdkRoot}/sdk/lib/libraries.json');
 
-  final result = await compileToModule(
+  final result = await compile(
       options, fileSystem, (mod) => Uri.parse('$mod.maps'), (diag) {
     print('Diagnostics: ${diag.severity} ${diag.plainTextFormatted}');
   });
   if (result is! CompilationSuccess) {
     throw 'Compilation Failed: $result';
   }
-  return result;
+  return result as CodegenResult;
 }
diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart
index f442f06..08f0a24 100644
--- a/pkg/dart2wasm/lib/compile.dart
+++ b/pkg/dart2wasm/lib/compile.dart
@@ -16,9 +16,12 @@
         kernelForProgram,
         CfeSeverity;
 import 'package:kernel/ast.dart';
+import 'package:kernel/binary/ast_from_binary.dart'
+    show BinaryBuilderWithMetadata;
 import 'package:kernel/class_hierarchy.dart';
 import 'package:kernel/core_types.dart';
-import 'package:kernel/kernel.dart' show writeComponentToText;
+import 'package:kernel/kernel.dart'
+    show writeComponentToText, loadComponentFromBytes;
 import 'package:kernel/library_index.dart';
 import 'package:kernel/text/ast_to_text.dart';
 import 'package:kernel/type_environment.dart';
@@ -41,6 +44,7 @@
 import 'dry_run.dart';
 import 'dynamic_module_kernel_metadata.dart';
 import 'dynamic_modules.dart';
+import 'js/method_collector.dart' show JSMethods;
 import 'js/runtime_generator.dart' as js;
 import 'modules.dart';
 import 'record_class_generator.dart';
@@ -48,6 +52,7 @@
 import 'target.dart' as wasm show Mode;
 import 'target.dart' hide Mode;
 import 'translator.dart';
+import 'util.dart';
 
 sealed class CompilationResult {}
 
@@ -57,12 +62,40 @@
 
 class CompilationDryRunSuccess extends CompilationDryRunResult {}
 
-class CompilationSuccess extends CompilationResult {
+sealed class CompilationSuccess extends CompilationResult {}
+
+class CfeResult extends CompilationSuccess {
+  final Component component;
+  final CoreTypes coreTypes;
+
+  CfeResult(this.component, this.coreTypes);
+}
+
+class TfaResult extends CompilationSuccess {
+  final Component component;
+  final CoreTypes coreTypes;
+  final LibraryIndex libraryIndex;
+  final ModuleStrategy moduleStrategy;
+  final MainModuleMetadata mainModuleMetadata;
+  final JSMethods jsInteropMethods;
+  final Map<RecordShape, Class> recordClasses;
+
+  TfaResult(
+      this.component,
+      this.coreTypes,
+      this.libraryIndex,
+      this.moduleStrategy,
+      this.mainModuleMetadata,
+      this.jsInteropMethods,
+      this.recordClasses);
+}
+
+class CodegenResult extends CompilationSuccess {
   final Map<String, ({Uint8List moduleBytes, String? sourceMap})> wasmModules;
   final String jsRuntime;
   final String supportJs;
 
-  CompilationSuccess(this.wasmModules, this.jsRuntime, this.supportJs);
+  CodegenResult(this.wasmModules, this.jsRuntime, this.supportJs);
 }
 
 abstract class CompilationError extends CompilationResult {}
@@ -124,20 +157,12 @@
 /// This value will be added to the Wasm module in `sourceMappingURL` section.
 /// When this argument is null the code generator does not generate source
 /// mappings.
-Future<CompilationResult> compileToModule(
+Future<CompilationResult> compile(
     compiler.WasmCompilerOptions options,
     FileSystem fileSystem,
     Uri Function(String moduleName)? sourceMapUrlGenerator,
     void Function(CfeDiagnosticMessage) handleDiagnosticMessage,
     {void Function(String, String)? writeFile}) async {
-  var hadCompileTimeError = false;
-  void diagnosticMessageHandler(CfeDiagnosticMessage message) {
-    if (message.severity == CfeSeverity.error) {
-      hadCompileTimeError = true;
-    }
-    handleDiagnosticMessage(message);
-  }
-
   final wasm.Mode mode;
   if (options.translatorOptions.jsCompatibility) {
     mode = wasm.Mode.jsCompatibility;
@@ -150,6 +175,68 @@
           options.translatorOptions.enableExperimentalWasmInterop,
       removeAsserts: !options.translatorOptions.enableAsserts,
       mode: mode);
+
+  if (options.multiRootScheme != null) {
+    fileSystem = MultiRootFileSystem(
+        options.multiRootScheme!,
+        options.multiRoots.isEmpty ? [Uri.base] : options.multiRoots,
+        fileSystem);
+  }
+
+  CfeResult? cfeResult;
+  TfaResult? tfaResult;
+  CompilationResult? lastResult;
+
+  for (final phase in options.phases) {
+    switch (phase) {
+      case compiler.CompilerPhase.cfe:
+        lastResult = await _runCfePhase(
+            options, target, fileSystem, handleDiagnosticMessage);
+        if (lastResult is! CfeResult) return lastResult;
+        cfeResult = lastResult;
+
+      case compiler.CompilerPhase.tfa:
+        lastResult = await _runTfaPhase(
+            cfeResult ?? await _loadCfeResult(options),
+            options,
+            target,
+            fileSystem);
+        if (lastResult is! TfaResult) return lastResult;
+        tfaResult = lastResult;
+
+      case compiler.CompilerPhase.codegen:
+        lastResult = await _runCodegenPhase(
+            tfaResult ?? await _loadTfaResult(options, target, fileSystem),
+            options,
+            fileSystem,
+            sourceMapUrlGenerator);
+    }
+  }
+
+  return lastResult!;
+}
+
+Future<CfeResult> _loadCfeResult(compiler.WasmCompilerOptions options) async {
+  final component =
+      loadComponentFromBytes(await File.fromUri(options.mainUri).readAsBytes());
+  final coreTypes = CoreTypes(component);
+  return CfeResult(component, coreTypes);
+}
+
+Future<CompilationResult> _runCfePhase(
+    compiler.WasmCompilerOptions options,
+    WasmTarget target,
+    FileSystem fileSystem,
+    void Function(CfeDiagnosticMessage) handleDiagnosticMessage,
+    {void Function(String, String)? writeFile}) async {
+  var hadCompileTimeError = false;
+  void diagnosticMessageHandler(CfeDiagnosticMessage message) {
+    if (message.severity == CfeSeverity.error) {
+      hadCompileTimeError = true;
+    }
+    handleDiagnosticMessage(message);
+  }
+
   CompilerOptions compilerOptions = CompilerOptions()
     ..target = target
     // This is a dummy directory that always exists. This option should be
@@ -169,21 +256,6 @@
     ..verbose = false
     ..onDiagnostic = diagnosticMessageHandler
     ..fileSystem = fileSystem;
-  if (options.multiRootScheme != null) {
-    compilerOptions.fileSystem = MultiRootFileSystem(
-        options.multiRootScheme!,
-        options.multiRoots.isEmpty ? [Uri.base] : options.multiRoots,
-        compilerOptions.fileSystem);
-  }
-
-  Future<Uri?> resolveUri(Uri? uri) async {
-    if (uri == null) return null;
-    var fileSystemEntity = compilerOptions.fileSystem.entityForUri(uri);
-    if (fileSystemEntity is MultiRootFileSystemEntity) {
-      fileSystemEntity = await fileSystemEntity.delegate;
-    }
-    return fileSystemEntity.uri;
-  }
 
   if (options.platformPath != null) {
     compilerOptions.sdkSummary = options.platformPath;
@@ -191,10 +263,8 @@
     compilerOptions.compileSdk = true;
   }
 
-  final dynamicMainModuleUri = await resolveUri(options.dynamicMainModuleUri);
-  final dynamicInterfaceUri = await resolveUri(options.dynamicInterfaceUri);
-  final isDynamicMainModule =
-      options.dynamicModuleType == DynamicModuleType.main;
+  final dynamicMainModuleUri =
+      await _resolveUri(fileSystem, options.dynamicMainModuleUri);
   final isDynamicSubmodule =
       options.dynamicModuleType == DynamicModuleType.submodule;
   if (isDynamicSubmodule) {
@@ -227,19 +297,72 @@
   if (hadCompileTimeError) {
     return CFECompileTimeErrors(compilerResult?.component);
   }
-  assert(compilerResult != null);
-
-  Component component = compilerResult!.component!;
-  CoreTypes coreTypes = compilerResult.coreTypes!;
-
-  ClosedWorldClassHierarchy classHierarchy =
-      ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
-  LibraryIndex libraryIndex = LibraryIndex(component, _librariesToIndex);
+  final component = compilerResult!.component!;
 
   if (options.dumpKernelAfterCfe != null && writeFile != null) {
     writeFile(options.dumpKernelAfterCfe!, writeComponentToString(component));
   }
 
+  return CfeResult(component, compilerResult.coreTypes!);
+}
+
+Future<TfaResult> _loadTfaResult(compiler.WasmCompilerOptions options,
+    WasmTarget target, FileSystem fileSystem) async {
+  final component = createEmptyComponent();
+  final recordClassesRepository = _RecordClassesRepository();
+  final interopMethodsRepository = _InteropMethodsRepository();
+  component.addMetadataRepository(recordClassesRepository);
+  component.addMetadataRepository(interopMethodsRepository);
+
+  BinaryBuilderWithMetadata(await File.fromUri(options.mainUri).readAsBytes())
+      .readComponent(component);
+  final coreTypes = CoreTypes(component);
+  final libraryIndex = LibraryIndex(component, _librariesToIndex);
+  final classHierarchy = ClassHierarchy(component, coreTypes);
+  final dynamicMainModuleUri =
+      await _resolveUri(fileSystem, options.dynamicMainModuleUri);
+  final dynamicInterfaceUri =
+      await _resolveUri(fileSystem, options.dynamicInterfaceUri);
+
+  final moduleStrategy = _createModuleStrategy(options, component, coreTypes,
+      target, classHierarchy, dynamicMainModuleUri, dynamicInterfaceUri);
+
+  final recordClasses = <RecordShape, Class>{};
+  recordClassesRepository.mapping.forEach((cls, shape) {
+    recordClasses[shape] = cls;
+  });
+
+  final isDynamicMainModule =
+      options.dynamicModuleType == DynamicModuleType.main;
+  final isDynamicSubmodule =
+      options.dynamicModuleType == DynamicModuleType.submodule;
+  MainModuleMetadata mainModuleMetadata =
+      MainModuleMetadata.empty(options.translatorOptions, options.environment);
+
+  if (isDynamicSubmodule) {
+    mainModuleMetadata =
+        await deserializeMainModuleMetadata(component, options);
+    mainModuleMetadata.verifyDynamicSubmoduleOptions(options);
+  } else if (isDynamicMainModule) {
+    MainModuleMetadata.verifyMainModuleOptions(options);
+  }
+
+  return TfaResult(component, coreTypes, libraryIndex, moduleStrategy,
+      mainModuleMetadata, interopMethodsRepository.mapping, recordClasses);
+}
+
+Future<CompilationResult> _runTfaPhase(
+    CfeResult cfeResult,
+    compiler.WasmCompilerOptions options,
+    WasmTarget target,
+    FileSystem fileSystem,
+    {void Function(String, String)? writeFile}) async {
+  var CfeResult(:component, :coreTypes) = cfeResult;
+
+  ClosedWorldClassHierarchy classHierarchy =
+      ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
+  LibraryIndex libraryIndex = LibraryIndex(component, _librariesToIndex);
+
   if (options.deleteToStringPackageUri.isNotEmpty) {
     to_string_transformer.transformComponent(
         component, options.deleteToStringPackageUri);
@@ -250,6 +373,15 @@
       coreTypes,
       classHierarchy);
 
+  final dynamicMainModuleUri =
+      await _resolveUri(fileSystem, options.dynamicMainModuleUri);
+  final dynamicInterfaceUri =
+      await _resolveUri(fileSystem, options.dynamicInterfaceUri);
+  final isDynamicMainModule =
+      options.dynamicModuleType == DynamicModuleType.main;
+  final isDynamicSubmodule =
+      options.dynamicModuleType == DynamicModuleType.submodule;
+
   if (isDynamicSubmodule) {
     // Join the submodule libraries with the TFAed component from the main
     // module compilation. JS interop transformer must be run before this since
@@ -280,26 +412,8 @@
     writeFile(options.dumpKernelBeforeTfa!, writeComponentToString(component));
   }
 
-  ModuleStrategy moduleStrategy;
-  if (options.translatorOptions.enableDeferredLoading) {
-    moduleStrategy =
-        DeferredLoadingModuleStrategy(component, options, target, coreTypes);
-  } else if (options.translatorOptions.enableMultiModuleStressTestMode) {
-    moduleStrategy = StressTestModuleStrategy(
-        component, coreTypes, options, target, classHierarchy);
-  } else if (isDynamicMainModule) {
-    moduleStrategy = DynamicMainModuleStrategy(
-        component,
-        coreTypes,
-        options,
-        File.fromUri(dynamicInterfaceUri!).readAsStringSync(),
-        options.dynamicInterfaceUri!);
-  } else if (isDynamicSubmodule) {
-    moduleStrategy = DynamicSubmoduleStrategy(
-        component, options, target, coreTypes, dynamicMainModuleUri!);
-  } else {
-    moduleStrategy = DefaultModuleStrategy(component, options);
-  }
+  final moduleStrategy = _createModuleStrategy(options, component, coreTypes,
+      target, classHierarchy, dynamicMainModuleUri, dynamicInterfaceUri);
 
   // DynamicMainModuleStrategy.prepareComponent() includes
   // dynamic_interface_annotator transformation which annotates AST nodes with
@@ -337,9 +451,19 @@
         useRapidTypeAnalysis: false);
   }
 
-  if (options.dumpKernelAfterTfa != null) {
-    writeComponentToText(component,
-        path: options.dumpKernelAfterTfa!, showMetadata: true);
+  if (options.phases.last == compiler.CompilerPhase.tfa) {
+    // Store metadata needed for codegen so that it can be serialized.
+    final recordClassesRepo = _RecordClassesRepository();
+    recordClasses.forEach((shape, cls) {
+      recordClassesRepo.mapping[cls] = shape;
+    });
+    component.addMetadataRepository(recordClassesRepo);
+
+    final interopMethodsRepo = _InteropMethodsRepository();
+    jsInteropMethods.forEach((method, info) {
+      interopMethodsRepo.mapping[method] = info;
+    });
+    component.addMetadataRepository(interopMethodsRepo);
   }
 
   assert(() {
@@ -348,6 +472,29 @@
     return true;
   }());
 
+  if (options.dumpKernelAfterTfa != null) {
+    writeComponentToText(component,
+        path: options.dumpKernelAfterTfa!, showMetadata: true);
+  }
+
+  return TfaResult(component, coreTypes, libraryIndex, moduleStrategy,
+      mainModuleMetadata, jsInteropMethods, recordClasses);
+}
+
+Future<CompilationResult> _runCodegenPhase(
+    TfaResult tfaSuccess,
+    compiler.WasmCompilerOptions options,
+    FileSystem fileSystem,
+    Uri Function(String moduleName)? sourceMapUrlGenerator) async {
+  final TfaResult(
+    :component,
+    :coreTypes,
+    :moduleStrategy,
+    :libraryIndex,
+    :recordClasses,
+    :mainModuleMetadata,
+    :jsInteropMethods
+  ) = tfaSuccess;
   await moduleStrategy.processComponentAfterTfa();
 
   final moduleOutputData = moduleStrategy.buildModuleOutputData();
@@ -359,8 +506,8 @@
 
   String? depFile = options.depFile;
   if (depFile != null) {
-    writeDepfile(compilerOptions.fileSystem, component.uriToSource.keys,
-        options.outputFile, depFile);
+    writeDepfile(
+        fileSystem, component.uriToSource.keys, options.outputFile, depFile);
   }
 
   final generateSourceMaps = options.translatorOptions.generateSourceMaps;
@@ -380,6 +527,13 @@
 
   final jsRuntimeFinalizer = js.RuntimeFinalizer(jsInteropMethods);
 
+  final dynamicMainModuleUri =
+      await _resolveUri(fileSystem, options.dynamicMainModuleUri);
+  final isDynamicMainModule =
+      options.dynamicModuleType == DynamicModuleType.main;
+  final isDynamicSubmodule =
+      options.dynamicModuleType == DynamicModuleType.submodule;
+
   final jsRuntime = isDynamicSubmodule
       ? jsRuntimeFinalizer.generateDynamicSubmodule(
           translator.functions.translatedProcedures,
@@ -400,7 +554,38 @@
         optimized: true);
   }
 
-  return CompilationSuccess(wasmModules, jsRuntime, supportJs);
+  return CodegenResult(wasmModules, jsRuntime, supportJs);
+}
+
+ModuleStrategy _createModuleStrategy(
+    compiler.WasmCompilerOptions options,
+    Component component,
+    CoreTypes coreTypes,
+    WasmTarget target,
+    ClassHierarchy classHierarchy,
+    Uri? dynamicMainModuleUri,
+    Uri? dynamicInterfaceUri) {
+  final isDynamicMainModule =
+      options.dynamicModuleType == DynamicModuleType.main;
+  final isDynamicSubmodule =
+      options.dynamicModuleType == DynamicModuleType.submodule;
+  if (options.translatorOptions.enableDeferredLoading) {
+    return DeferredLoadingModuleStrategy(component, options, target, coreTypes);
+  } else if (options.translatorOptions.enableMultiModuleStressTestMode) {
+    return StressTestModuleStrategy(
+        component, coreTypes, options, target, classHierarchy);
+  } else if (isDynamicMainModule) {
+    return DynamicMainModuleStrategy(
+        component,
+        coreTypes,
+        options,
+        File.fromUri(dynamicInterfaceUri!).readAsStringSync(),
+        options.dynamicInterfaceUri!);
+  } else if (isDynamicSubmodule) {
+    return DynamicSubmoduleStrategy(
+        component, options, target, coreTypes, dynamicMainModuleUri!);
+  }
+  return DefaultModuleStrategy(component, options);
 }
 
 // Patches `dart:_internal`s `mainTearOff{0,1,2}` getters.
@@ -434,6 +619,69 @@
   if (mainHasType(mainArg0Type)) return patchToReturnMainTearOff(mainTearOff0);
 }
 
+Future<Uri?> _resolveUri(FileSystem fileSystem, Uri? uri) async {
+  if (uri == null) return null;
+  var fileSystemEntity = fileSystem.entityForUri(uri);
+  if (fileSystemEntity is MultiRootFileSystemEntity) {
+    fileSystemEntity = await fileSystemEntity.delegate;
+  }
+  return fileSystemEntity.uri;
+}
+
+class _RecordClassesRepository extends MetadataRepository<RecordShape> {
+  static const String _tag = 'dart2wasm.recordClasses';
+  @override
+  final Map<Class, RecordShape> mapping = {};
+
+  @override
+  RecordShape readFromBinary(Node node, BinarySource source) {
+    final positionals = source.readUInt30();
+    final namesLength = source.readUInt30();
+    final names = namesLength == 0 ? const <String>[] : <String>[];
+    for (int i = 0; i < namesLength; i++) {
+      names.add(source.readStringReference());
+    }
+    return RecordShape(positionals, names);
+  }
+
+  @override
+  String get tag => _tag;
+
+  @override
+  void writeToBinary(RecordShape metadata, Node node, BinarySink sink) {
+    sink.writeUInt30(metadata.positionals);
+    sink.writeUInt30(metadata.names.length);
+    for (final name in metadata.names) {
+      sink.writeStringReference(name);
+    }
+  }
+}
+
+class _InteropMethodsRepository
+    extends MetadataRepository<({String importName, String jsCode})> {
+  static const String _tag = 'dart2wasm.interopMethods';
+  @override
+  final Map<Procedure, ({String importName, String jsCode})> mapping = {};
+
+  @override
+  ({String importName, String jsCode}) readFromBinary(
+      Node node, BinarySource source) {
+    final importName = source.readStringReference();
+    final jsCode = source.readStringReference();
+    return (importName: importName, jsCode: jsCode);
+  }
+
+  @override
+  String get tag => _tag;
+
+  @override
+  void writeToBinary(({String importName, String jsCode}) metadata, Node node,
+      BinarySink sink) {
+    sink.writeStringReference(metadata.importName);
+    sink.writeStringReference(metadata.jsCode);
+  }
+}
+
 String _generateSupportJs(TranslatorOptions options) {
   // Copied from
   // https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js
diff --git a/pkg/dart2wasm/lib/compiler_options.dart b/pkg/dart2wasm/lib/compiler_options.dart
index e36ff6c..dcb81c7 100644
--- a/pkg/dart2wasm/lib/compiler_options.dart
+++ b/pkg/dart2wasm/lib/compiler_options.dart
@@ -3,10 +3,33 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:front_end/src/api_unstable/vm.dart' as fe;
+import 'package:path/path.dart' as path;
 
 import 'dynamic_modules.dart' show DynamicModuleType;
 import 'translator.dart';
 
+/// Represents a discrete phase of dart2wasm's compilation process.
+///
+/// cfe: Runs the common frontend and applies any modular transforms as part of
+/// that process.
+///
+/// tfa: Runs global transforms on kernel including, but not limited to, TFA.
+///
+/// codegen: Runs the main dart2wasm translation process converting the kernel
+/// into WASM modules.
+enum CompilerPhase {
+  cfe,
+  tfa,
+  codegen;
+
+  static CompilerPhase parse(String name) {
+    for (final phase in values) {
+      if (phase.name == name) return phase;
+    }
+    throw ArgumentError('Invalid compiler phase name: $name');
+  }
+}
+
 class WasmCompilerOptions {
   final TranslatorOptions translatorOptions = TranslatorOptions();
 
@@ -31,6 +54,11 @@
   String? dumpKernelBeforeTfa;
   String? dumpKernelAfterTfa;
   bool dryRun = false;
+  List<CompilerPhase> phases = const [
+    CompilerPhase.cfe,
+    CompilerPhase.tfa,
+    CompilerPhase.codegen
+  ];
 
   factory WasmCompilerOptions.defaultOptions() =>
       WasmCompilerOptions(mainUri: Uri(), outputFile: '');
@@ -64,5 +92,57 @@
             "compiling dynamic modules.");
       }
     }
+
+    _validatePhases();
+  }
+
+  void _validatePhases() {
+    if (phases.isEmpty) {
+      throw ArgumentError('--phases must contain at least one phase.');
+    }
+
+    CompilerPhase? previousPhase;
+    for (final phase in phases) {
+      // Ensure phases are consecutive
+      if (previousPhase != null && previousPhase.index != phase.index - 1) {
+        throw ArgumentError('--phases must contain consecutive phases.');
+      }
+      previousPhase = phase;
+    }
+
+    // Ensure correct input file type
+    final inputExtension = path.extension(mainUri.path);
+    switch (phases.first) {
+      case CompilerPhase.cfe:
+        if (inputExtension != '.dart') {
+          throw ArgumentError('Input to cfe phase must be a .dart file.');
+        }
+      case CompilerPhase.tfa:
+        if (inputExtension != '.dill') {
+          throw ArgumentError('Input to tfa phase must be a .dill file.');
+        }
+      case CompilerPhase.codegen:
+        if (inputExtension != '.dill') {
+          throw ArgumentError('Input to codegen phase must be a .dill file.');
+        }
+    }
+
+    // Ensure correct output file type
+    final outputExtension = path.extension(outputFile);
+    switch (phases.last) {
+      case CompilerPhase.cfe:
+        if (outputExtension != '.dill') {
+          throw ArgumentError('Output from cfe phase must be a .dill file.');
+        }
+      case CompilerPhase.tfa:
+        if (outputExtension != '.dill') {
+          throw ArgumentError('Output from tfa phase must be a .dill file.');
+        }
+      case CompilerPhase.codegen:
+        if (outputExtension != '.wasm') {
+          throw ArgumentError(
+              'Output from codegen phase must be a .wasm file.');
+        }
+    }
   }
 }
diff --git a/pkg/dart2wasm/lib/dart2wasm.dart b/pkg/dart2wasm/lib/dart2wasm.dart
index 9c49b4e..8050c1f 100644
--- a/pkg/dart2wasm/lib/dart2wasm.dart
+++ b/pkg/dart2wasm/lib/dart2wasm.dart
@@ -27,6 +27,10 @@
   Flag("minify", (o, value) => o.translatorOptions.minify = value,
       defaultsTo: _d.translatorOptions.minify),
   Flag("dry-run", (o, value) => o.dryRun = value, defaultsTo: _d.dryRun),
+  StringMultiOption(
+      "phases",
+      (o, values) => o.phases = [...values.map(CompilerPhase.parse)]
+        ..sort((a, b) => a.index.compareTo(b.index))),
   Flag("polymorphic-specialization",
       (o, value) => o.translatorOptions.polymorphicSpecialization = value,
       defaultsTo: _d.translatorOptions.polymorphicSpecialization),
diff --git a/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart
index f28a6c3..e92ce58 100644
--- a/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart
+++ b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart
@@ -13,15 +13,6 @@
     show writeComponentToBinary, writeComponentToBytes;
 import 'package:kernel/library_index.dart';
 import 'package:path/path.dart' as path;
-import 'package:vm/metadata/direct_call.dart' show DirectCallMetadataRepository;
-import 'package:vm/metadata/inferred_type.dart'
-    show
-        InferredArgTypeMetadataRepository,
-        InferredReturnTypeMetadataRepository,
-        InferredTypeMetadataRepository;
-import 'package:vm/metadata/procedure_attributes.dart'
-    show ProcedureAttributesMetadataRepository;
-import 'package:vm/metadata/table_selector.dart';
 
 import 'class_info.dart';
 import 'compiler_options.dart';
@@ -30,6 +21,7 @@
 import 'js/method_collector.dart' show JSMethods;
 import 'serialization.dart';
 import 'translator.dart';
+import 'util.dart';
 
 const String dynamicMainModuleProcedureAttributeMetadataTag =
     'dynMod:procedureAttributes';
@@ -494,15 +486,9 @@
   concatenatedComponentBytes.setAll(0, optimizedMainComponentBytes);
   concatenatedComponentBytes.setAll(
       optimizedMainComponentBytes.length, submoduleComponentBytes);
-  final newComponent = Component()
+  final newComponent = createEmptyComponent()
     ..addMetadataRepository(DynamicModuleGlobalIdRepository())
-    ..addMetadataRepository(DynamicModuleConstantRepository())
-    ..addMetadataRepository(ProcedureAttributesMetadataRepository())
-    ..addMetadataRepository(TableSelectorMetadataRepository())
-    ..addMetadataRepository(DirectCallMetadataRepository())
-    ..addMetadataRepository(InferredTypeMetadataRepository())
-    ..addMetadataRepository(InferredReturnTypeMetadataRepository())
-    ..addMetadataRepository(InferredArgTypeMetadataRepository());
+    ..addMetadataRepository(DynamicModuleConstantRepository());
   BinaryBuilderWithMetadata(concatenatedComponentBytes)
       .readComponent(newComponent);
 
diff --git a/pkg/dart2wasm/lib/generate_wasm.dart b/pkg/dart2wasm/lib/generate_wasm.dart
index 0b7022f..2bb6b0b 100644
--- a/pkg/dart2wasm/lib/generate_wasm.dart
+++ b/pkg/dart2wasm/lib/generate_wasm.dart
@@ -7,6 +7,7 @@
 import 'package:front_end/src/api_prototype/standard_file_system.dart'
     show StandardFileSystem;
 import 'package:front_end/src/api_unstable/vm.dart' show printDiagnosticMessage;
+import 'package:kernel/kernel.dart';
 import 'package:path/path.dart' as path;
 
 import 'compile.dart';
@@ -18,6 +19,7 @@
 
 Future<int> generateWasm(WasmCompilerOptions options,
     {PrintError errorPrinter = print}) async {
+  options.validate();
   final translatorOptions = options.translatorOptions;
   if (translatorOptions.verbose) {
     print('Running dart compile wasm...');
@@ -75,7 +77,7 @@
       ? moduleNameToRelativeSourceMapUri
       : null;
 
-  CompilationResult result = await compileToModule(
+  CompilationResult result = await compile(
       options, StandardFileSystem.instance, relativeSourceMapUrlMapper,
       (message) {
     if (!options.dryRun) printDiagnosticMessage(message, errorPrinter);
@@ -109,26 +111,35 @@
     return 255;
   }
 
-  final writeFutures = <Future>[];
-  result.wasmModules.forEach((moduleName, moduleInfo) {
-    final (:moduleBytes, :sourceMap) = moduleInfo;
-    final File outFile = File(moduleNameToWasmOutputFile(moduleName));
-    outFile.parent.createSync(recursive: true);
-    writeFutures.add(outFile.writeAsBytes(moduleBytes));
+  switch (result) {
+    case CfeResult(:final component):
+      await File(options.outputFile)
+          .writeAsBytes(writeComponentToBytes(component));
+    case TfaResult(:final component):
+      await File(options.outputFile)
+          .writeAsBytes(writeComponentToBytes(component));
+    case CodegenResult(:final wasmModules, :final jsRuntime, :final supportJs):
+      final writeFutures = <Future>[];
+      wasmModules.forEach((moduleName, moduleInfo) {
+        final (:moduleBytes, :sourceMap) = moduleInfo;
+        final File outFile = File(moduleNameToWasmOutputFile(moduleName));
+        outFile.parent.createSync(recursive: true);
+        writeFutures.add(outFile.writeAsBytes(moduleBytes));
 
-    if (sourceMap != null) {
-      writeFutures.add(
-          File(moduleNameToSourceMapFile(moduleName)).writeAsString(sourceMap));
-    }
-  });
-  await Future.wait(writeFutures);
+        if (sourceMap != null) {
+          writeFutures.add(File(moduleNameToSourceMapFile(moduleName))
+              .writeAsString(sourceMap));
+        }
+      });
+      await Future.wait(writeFutures);
 
-  final jsFile = path.setExtension(options.outputFile, '.mjs');
-  final jsRuntime = result.jsRuntime;
-  await File(jsFile).writeAsString(jsRuntime);
+      final jsFile = path.setExtension(options.outputFile, '.mjs');
+      await File(jsFile).writeAsString(jsRuntime);
 
-  final supportJsFile = path.setExtension(options.outputFile, '.support.js');
-  await File(supportJsFile).writeAsString(result.supportJs);
+      final supportJsFile =
+          path.setExtension(options.outputFile, '.support.js');
+      await File(supportJsFile).writeAsString(supportJs);
+  }
 
   return 0;
 }
diff --git a/pkg/dart2wasm/lib/util.dart b/pkg/dart2wasm/lib/util.dart
index 0526c3a..88ea5ff 100644
--- a/pkg/dart2wasm/lib/util.dart
+++ b/pkg/dart2wasm/lib/util.dart
@@ -6,6 +6,16 @@
 
 import 'package:kernel/ast.dart';
 import 'package:kernel/core_types.dart';
+import 'package:vm/metadata/direct_call.dart' show DirectCallMetadataRepository;
+import 'package:vm/metadata/inferred_type.dart'
+    show
+        InferredTypeMetadataRepository,
+        InferredReturnTypeMetadataRepository,
+        InferredArgTypeMetadataRepository;
+import 'package:vm/metadata/procedure_attributes.dart'
+    show ProcedureAttributesMetadataRepository;
+import 'package:vm/metadata/table_selector.dart'
+    show TableSelectorMetadataRepository;
 
 bool hasPragma(CoreTypes coreTypes, Annotatable node, String name) {
   return getPragma(coreTypes, node, name, defaultValue: '') != null;
@@ -67,3 +77,13 @@
 }
 
 String intToBase64(int i) => base64.encode(_intToLittleEndianBytes(i));
+
+Component createEmptyComponent() {
+  return Component()
+    ..addMetadataRepository(ProcedureAttributesMetadataRepository())
+    ..addMetadataRepository(TableSelectorMetadataRepository())
+    ..addMetadataRepository(DirectCallMetadataRepository())
+    ..addMetadataRepository(InferredTypeMetadataRepository())
+    ..addMetadataRepository(InferredReturnTypeMetadataRepository())
+    ..addMetadataRepository(InferredArgTypeMetadataRepository());
+}
diff --git a/pkg/dart2wasm/test/deferred_loading/load_ids_test.dart b/pkg/dart2wasm/test/deferred_loading/load_ids_test.dart
index 362f9b9..e48807e 100644
--- a/pkg/dart2wasm/test/deferred_loading/load_ids_test.dart
+++ b/pkg/dart2wasm/test/deferred_loading/load_ids_test.dart
@@ -9,19 +9,11 @@
 import 'package:expect/expect.dart';
 import 'package:path/path.dart' as path;
 
+import '../util.dart';
+
 const String helperJsLoadIdLookupToken = 'LOAD_ID_LOOKUP';
 const String helperJsModuleDirToken = 'MODULE_DIR';
 
-final dartAotExecutable = Uri.parse(Platform.resolvedExecutable)
-    .resolve('dartaotruntime')
-    .toFilePath();
-final dart2wasmSnapshot = Uri.parse(Platform.resolvedExecutable)
-    .resolve('snapshots/dart2wasm_product.snapshot')
-    .toFilePath();
-final platformDill = Uri.parse(Platform.resolvedExecutable)
-    .resolve('../lib/_internal/dart2wasm_platform.dill')
-    .toFilePath();
-
 final String goldenPath =
     '${path.dirname(Platform.script.path)}/data/deferred_load_ids.golden.json';
 final String mainDart = '${path.dirname(Platform.script.path)}/data/main.dart';
@@ -33,10 +25,10 @@
 
   final argsResult = parser.parse(args);
 
-  final tmpDir = await Directory.systemTemp.createTemp('wasm-load-ids');
-  final loadIdsUri = tmpDir.uri.resolve('deferred_load_ids.json');
-  final outFilename = '${tmpDir.path}/out.wasm';
-  try {
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+    final loadIdsUri = tmpDir.uri.resolve('deferred_load_ids.json');
+    final outFilename = '${tmpDir.path}/out.wasm';
     // Compile the test
     await run([
       dartAotExecutable,
@@ -82,18 +74,5 @@
     // Load the helper JS and run the compiled code
     await run(
         ['pkg/dart2wasm/tool/run_benchmark', helperFile.path, outFilename]);
-  } finally {
-    await tmpDir.delete(recursive: true);
-  }
-}
-
-Future<void> run(List<String> command) async {
-  print('Running: ${command.join(' ')}');
-  final result = await Process.run(command.first, command.skip(1).toList());
-  if (result.exitCode != 0) {
-    print('-> Failed with exit code ${result.exitCode}');
-    print('-> stdout:\n${result.stdout}');
-    print('-> stderr:\n${result.stderr}');
-    throw 'Subprocess failed';
-  }
+  });
 }
diff --git a/pkg/dart2wasm/test/ir_test.dart b/pkg/dart2wasm/test/ir_test.dart
index a92c125..25dd4f8 100644
--- a/pkg/dart2wasm/test/ir_test.dart
+++ b/pkg/dart2wasm/test/ir_test.dart
@@ -13,7 +13,7 @@
 import 'package:wasm_builder/src/serialize/deserializer.dart';
 import 'package:wasm_builder/src/serialize/printer.dart';
 
-import 'self_compile_test.dart' show withTempDir;
+import 'util.dart';
 
 void main(List<String> args) async {
   final result = argParser.parse(args);
diff --git a/pkg/dart2wasm/test/phases/data/main.dart b/pkg/dart2wasm/test/phases/data/main.dart
new file mode 100644
index 0000000..454b085
--- /dev/null
+++ b/pkg/dart2wasm/test/phases/data/main.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2025, 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.
+
+void main() {
+  print('hello world');
+}
diff --git a/pkg/dart2wasm/test/phases/phases_test.dart b/pkg/dart2wasm/test/phases/phases_test.dart
new file mode 100644
index 0000000..9ccb297
--- /dev/null
+++ b/pkg/dart2wasm/test/phases/phases_test.dart
@@ -0,0 +1,244 @@
+// Copyright (c) 2025, 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:io';
+
+import 'package:expect/expect.dart';
+import 'package:path/path.dart' as path;
+
+import '../util.dart';
+
+final String mainDart = '${path.dirname(Platform.script.path)}/data/main.dart';
+final String cfeDillName = 'main.cfe.dill';
+final String tfaDillName = 'main.tfa.dill';
+final String wasmOutName = 'main.wasm';
+
+Future<void> main() async {
+  await testSuccessCases();
+  await testFailureCases();
+}
+
+Future<void> testSuccessCases() async {
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+
+    final cfeDill = File.fromUri(tmpDir.uri.resolve(cfeDillName));
+    final tfaDill = File.fromUri(tmpDir.uri.resolve(tfaDillName));
+    final wasmOut = File.fromUri(tmpDir.uri.resolve(wasmOutName));
+
+    // Run CFE and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe',
+      mainDart,
+      cfeDill.path,
+    ]);
+    Expect.isTrue(await cfeDill.exists());
+    Expect.isTrue((await cfeDill.stat()).size > 0);
+
+    // Run TFA and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=tfa',
+      cfeDill.path,
+      tfaDill.path,
+    ]);
+    Expect.isTrue(await tfaDill.exists());
+    Expect.isTrue((await tfaDill.stat()).size > 0);
+
+    // Run codegen and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=codegen',
+      tfaDill.path,
+      wasmOut.path,
+    ]);
+    Expect.isTrue(await wasmOut.exists());
+    Expect.isTrue((await wasmOut.stat()).size > 0);
+  });
+
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+
+    final tfaDill = File.fromUri(tmpDir.uri.resolve(tfaDillName));
+    final wasmOut = File.fromUri(tmpDir.uri.resolve(wasmOutName));
+
+    // Run CFE & TFA and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe,tfa',
+      mainDart,
+      tfaDill.path,
+    ]);
+    Expect.isTrue(await tfaDill.exists());
+    Expect.isTrue((await tfaDill.stat()).size > 0);
+
+    // Run codegen and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=codegen',
+      tfaDill.path,
+      wasmOut.path,
+    ]);
+    Expect.isTrue(await wasmOut.exists());
+    Expect.isTrue((await wasmOut.stat()).size > 0);
+  });
+
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+
+    final cfeDill = File.fromUri(tmpDir.uri.resolve(tfaDillName));
+    final wasmOut = File.fromUri(tmpDir.uri.resolve(wasmOutName));
+
+    // Run CFE and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe',
+      mainDart,
+      cfeDill.path,
+    ]);
+    Expect.isTrue(await cfeDill.exists());
+    Expect.isTrue((await cfeDill.stat()).size > 0);
+
+    // Run TFA & codegen and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=tfa,codegen',
+      cfeDill.path,
+      wasmOut.path,
+    ]);
+    Expect.isTrue(await wasmOut.exists());
+    Expect.isTrue((await wasmOut.stat()).size > 0);
+  });
+
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+
+    final wasmOut = File.fromUri(tmpDir.uri.resolve(wasmOutName));
+
+    // Run CFE & TFA & codegen and expect output
+    await run([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe,tfa,codegen',
+      mainDart,
+      wasmOut.path,
+    ]);
+    Expect.isTrue(await wasmOut.exists());
+    Expect.isTrue((await wasmOut.stat()).size > 0);
+  });
+}
+
+Future<void> testFailureCases() async {
+  await withTempDir((tmpDirPath) async {
+    final tmpDir = File(tmpDirPath);
+
+    final cfeDill = File.fromUri(tmpDir.uri.resolve(cfeDillName));
+    final tfaDill = File.fromUri(tmpDir.uri.resolve(tfaDillName));
+    final wasmOut = File.fromUri(tmpDir.uri.resolve(wasmOutName));
+
+    // CFE checks
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe',
+      tfaDill.path,
+      cfeDill.path
+    ], 'Input to cfe phase must be a .dart file');
+
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe',
+      mainDart,
+      wasmOut.path
+    ], 'Output from cfe phase must be a .dill file');
+
+    // TFA checks
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--platform=$platformDill',
+      '--phases=tfa',
+      mainDart,
+      tfaDill.path
+    ], 'Input to tfa phase must be a .dill file');
+
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=tfa',
+      cfeDill.path,
+      wasmOut.path
+    ], 'Output from tfa phase must be a .dill file');
+
+    // Codegen checks
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--platform=$platformDill',
+      '--phases=codegen',
+      mainDart,
+      wasmOut.path
+    ], 'Input to codegen phase must be a .dill file');
+
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=codegen',
+      tfaDill.path,
+      cfeDill.path
+    ], 'Output from codegen phase must be a .wasm file');
+
+    // Other checks
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=cfe,codegen',
+      mainDart,
+      wasmOut.path
+    ], 'must contain consecutive phases');
+
+    await expectFailedRun([
+      dartAotExecutable,
+      dart2wasmSnapshot,
+      '--platform=$platformDill',
+      '--phases=notReal',
+      mainDart,
+      cfeDill.path
+    ], 'Invalid compiler phase name');
+  });
+}
+
+Future<void> expectFailedRun(
+    List<String> command, String expectedSubstring) async {
+  try {
+    await run(command, throwOutputOnFailure: true);
+    Expect.fail('Expected dart2wasm error.');
+  } catch (e) {
+    Expect.contains(expectedSubstring, '$e');
+  }
+}
diff --git a/pkg/dart2wasm/test/self_compile_test.dart b/pkg/dart2wasm/test/self_compile_test.dart
index cdb8097..d84ba4c 100644
--- a/pkg/dart2wasm/test/self_compile_test.dart
+++ b/pkg/dart2wasm/test/self_compile_test.dart
@@ -7,6 +7,8 @@
 
 import 'package:path/path.dart' as path;
 
+import 'util.dart';
+
 Future main() async {
   if (!Platform.isLinux && !Platform.isMacOS) return;
 
@@ -44,17 +46,6 @@
   });
 }
 
-Future run(List<String> command) async {
-  print('Running: ${command.join(' ')}');
-  final result = await Process.run(command.first, command.skip(1).toList());
-  if (result.exitCode != 0) {
-    print('-> Failed with exit code ${result.exitCode}');
-    print('-> stdout:\n${result.stdout}');
-    print('-> stderr:\n${result.stderr}');
-    throw 'Subprocess failed';
-  }
-}
-
 void expectEqualBytes(Uint8List a, Uint8List b) {
   if (a.length != b.length) {
     throw 'Mismatch in length ${a.length} vs ${b.length}';
@@ -65,18 +56,3 @@
     }
   }
 }
-
-Future withTempDir(Future Function(String directory) fun) async {
-  final dir = Directory.systemTemp.createTempSync('dart2wasm_self_compile');
-  try {
-    print('Running with temporary directory: ${dir.path}');
-    return await fun(dir.path);
-  } finally {
-    if (!keepTemporaryDirectory) {
-      dir.deleteSync(recursive: true);
-    }
-  }
-}
-
-final bool keepTemporaryDirectory =
-    (Platform.environment['KEEP_TEMPORARY_DIRECTORIES'] ?? 'false') != 'false';
diff --git a/pkg/dart2wasm/test/util.dart b/pkg/dart2wasm/test/util.dart
new file mode 100644
index 0000000..3952b7c
--- /dev/null
+++ b/pkg/dart2wasm/test/util.dart
@@ -0,0 +1,46 @@
+// Copyright (c) 2025, 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:io';
+
+final dartAotExecutable = Uri.parse(Platform.resolvedExecutable)
+    .resolve('dartaotruntime')
+    .toFilePath();
+final dart2wasmSnapshot = Uri.parse(Platform.resolvedExecutable)
+    .resolve('snapshots/dart2wasm_product.snapshot')
+    .toFilePath();
+final platformDill = Uri.parse(Platform.resolvedExecutable)
+    .resolve('../lib/_internal/dart2wasm_platform.dill')
+    .toFilePath();
+
+Future<void> run(List<String> command,
+    {bool throwOutputOnFailure = false}) async {
+  print('Running: ${command.join(' ')}');
+  final result = await Process.run(command.first, command.skip(1).toList());
+  if (result.exitCode != 0) {
+    if (throwOutputOnFailure) {
+      throw '${result.stdout}\n${result.stderr}';
+    }
+
+    print('-> Failed with exit code ${result.exitCode}');
+    print('-> stdout:\n${result.stdout}');
+    print('-> stderr:\n${result.stderr}');
+    throw 'Subprocess failed';
+  }
+}
+
+Future withTempDir(Future Function(String directory) fun) async {
+  final dir = Directory.systemTemp.createTempSync('dart2wasm_self_compile');
+  try {
+    print('Running with temporary directory: ${dir.path}');
+    return await fun(dir.path);
+  } finally {
+    if (!keepTemporaryDirectory) {
+      dir.deleteSync(recursive: true);
+    }
+  }
+}
+
+final bool keepTemporaryDirectory =
+    (Platform.environment['KEEP_TEMPORARY_DIRECTORIES'] ?? 'false') != 'false';
diff --git a/pkg/dart2wasm/test/wasm2wat_test.dart b/pkg/dart2wasm/test/wasm2wat_test.dart
index 94fb59d..bda08cf 100644
--- a/pkg/dart2wasm/test/wasm2wat_test.dart
+++ b/pkg/dart2wasm/test/wasm2wat_test.dart
@@ -8,7 +8,7 @@
 import 'package:path/path.dart' as path;
 import 'package:wasm_builder/wasm_builder.dart';
 
-import 'self_compile_test.dart' show withTempDir, run;
+import 'util.dart';
 
 Future main() async {
   if (!Platform.isLinux && !Platform.isMacOS) return;
diff --git a/pkg/dart2wasm/test/wasm_read_write_test.dart b/pkg/dart2wasm/test/wasm_read_write_test.dart
index a005f79..55b7ccd 100644
--- a/pkg/dart2wasm/test/wasm_read_write_test.dart
+++ b/pkg/dart2wasm/test/wasm_read_write_test.dart
@@ -8,7 +8,8 @@
 import 'package:path/path.dart' as path;
 import 'package:wasm_builder/wasm_builder.dart';
 
-import 'self_compile_test.dart' show withTempDir, run, expectEqualBytes;
+import 'self_compile_test.dart' show expectEqualBytes;
+import 'util.dart';
 
 Future main() async {
   if (!Platform.isLinux && !Platform.isMacOS) return;
diff --git a/pkg/dart2wasm/tool/compile_benchmark b/pkg/dart2wasm/tool/compile_benchmark
index 79fcbf1..9c111b5 100755
--- a/pkg/dart2wasm/tool/compile_benchmark
+++ b/pkg/dart2wasm/tool/compile_benchmark
@@ -81,7 +81,7 @@
 DART2WASM_ARGS=("--require-js-string-builtin")
 ADDITIONAL_BINARYEN_FLAGS=()
 DART_FILE=""
-WASM_FILE=""
+OUTPUT_FILE=""
 while [ $# -gt 0 ]; do
     case "$1" in
       --compile-benchmark=*)
@@ -176,7 +176,7 @@
 
     -o)
       shift
-      WASM_FILE="$1"
+      OUTPUT_FILE="$1"
       shift
       ;;
 
@@ -190,8 +190,8 @@
         DART_FILE="$1"
         shift
       else
-        if [ -z "$WASM_FILE" ]; then
-          WASM_FILE="$1"
+        if [ -z "$OUTPUT_FILE" ]; then
+          OUTPUT_FILE="$1"
           shift
         else
           echo "Unexpected argument $1"
@@ -203,10 +203,10 @@
 done
 
 if [ $GENERATE_SOURCE_MAP -eq 1 ]; then
-  BINARYEN_FLAGS+=("-ism" "${WASM_FILE}.map" "-osm" "${WASM_FILE}.map")
+  BINARYEN_FLAGS+=("-ism" "${OUTPUT_FILE}.map" "-osm" "${OUTPUT_FILE}.map")
 fi
 
-if [ -z "$DART_FILE" -o -z "$WASM_FILE" ]; then
+if [ -z "$DART_FILE" -o -z "$OUTPUT_FILE" ]; then
   echo "Expected <file.dart> <file.wasm>"
   exit 1
 fi
@@ -237,7 +237,7 @@
 }
 
 function run_if_binaryen_enabled() {
-  if [ $RUN_BINARYEN -eq 1 ]; then
+  if [ $RUN_BINARYEN -eq 1 ] && [[ $OUTPUT_FILE == *.wasm ]]; then
     $@
   fi
 }
@@ -247,9 +247,9 @@
 
 function run_compiler() {
   if [ $RUN_SRC -eq 1 ]; then
-    dart2wasm_command=("$DART" "${VM_ARGS[@]}" "$DART2WASM_SRC" "$LIBRARIES_JSON_ARG" "${DART2WASM_ARGS[@]}" "$DART_FILE" "$WASM_FILE")
+    dart2wasm_command=("$DART" "${VM_ARGS[@]}" "$DART2WASM_SRC" "$LIBRARIES_JSON_ARG" "${DART2WASM_ARGS[@]}" "$DART_FILE" "$OUTPUT_FILE")
   else
-    dart2wasm_command=("$DART_AOT_RUNTIME" "${VM_ARGS[@]}" "$DART2WASM_AOT_SNAPSHOT" "$PLATFORM_ARG" "${DART2WASM_ARGS[@]}" "$DART_FILE" "$WASM_FILE")
+    dart2wasm_command=("$DART_AOT_RUNTIME" "${VM_ARGS[@]}" "$DART2WASM_AOT_SNAPSHOT" "$PLATFORM_ARG" "${DART2WASM_ARGS[@]}" "$DART_FILE" "$OUTPUT_FILE")
   fi
 
   if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
@@ -257,18 +257,18 @@
     COMPILER_TIME=$TIME
     COMPILER_MEMORY=$MEMORY
 
-    measure_size ${WASM_FILE%.wasm}.mjs
+    measure_size ${OUTPUT_FILE%.wasm}.mjs
     MJS_SIZE=$SIZE
     MJS_GZIP_SIZE=$GZIP_SIZE
 
     if [ $MULTI_MODULE -eq 1 ]; then
-      for WASM_FILE in "${WASM_FILE%.wasm}"*.wasm; do
-        measure_size $WASM_FILE
+      for OUTPUT_FILE in "${OUTPUT_FILE%.wasm}"*.wasm; do
+        measure_size $OUTPUT_FILE
         (( COMPILER_SIZE+=$SIZE ))
         (( COMPILER_GZIP_SIZE+=$GZIP_SIZE ))
       done
     else
-      measure_size $WASM_FILE
+      measure_size $OUTPUT_FILE
       COMPILER_SIZE=$SIZE
       COMPILER_GZIP_SIZE=$GZIP_SIZE
     fi
@@ -283,13 +283,13 @@
 BINARYEN_GZIP_SIZE=0
 
 function run_binaryen_single() {
-  binaryen_command=("$BINARYEN" "$@" "${ADDITIONAL_BINARYEN_FLAGS[@]}" "$WASM_FILE" -o "$WASM_FILE")
+  binaryen_command=("$BINARYEN" "$@" "${ADDITIONAL_BINARYEN_FLAGS[@]}" "$OUTPUT_FILE" -o "$OUTPUT_FILE")
   if [ -n "$COMPILE_BENCHMARK_BASE_NAME" ]; then
     # If we're measuring run each binaryen command sequentially.
     measure ${binaryen_command[@]}
     BINARYEN_TIME=$(echo "$BINARYEN_TIME + $TIME" | bc)
     BINARYEN_MEMORY=$(($BINARYEN_MEMORY > $MEMORY ? $BINARYEN_MEMORY : $MEMORY ))
-    measure_size $WASM_FILE
+    measure_size $OUTPUT_FILE
     BINARYEN_SIZE=$(echo "$BINARYEN_SIZE + $SIZE" | bc)
     BINARYEN_GZIP_SIZE=$(echo "$BINARYEN_GZIP_SIZE + $GZIP_SIZE" | bc)
   else
@@ -301,7 +301,7 @@
   if [ $MULTI_MODULE -eq 1 ]; then
     # Iterate over all matching wasm files and optimize them concurrently in
     # different processes.
-    for WASM_FILE in "${WASM_FILE%.wasm}"*.wasm; do
+    for OUTPUT_FILE in "${OUTPUT_FILE%.wasm}"*.wasm; do
       run_binaryen_single "${BINARYEN_FLAGS_DEFERRED_LOADING[@]}"
     done
   else
diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart
index 542e278..2a6adab 100644
--- a/pkg/dartdev/lib/src/commands/compile.dart
+++ b/pkg/dartdev/lib/src/commands/compile.dart
@@ -949,13 +949,7 @@
       outputFile = '$inputWithoutDart.wasm';
     }
 
-    if (!outputFile.endsWith('.wasm')) {
-      log.stderr(
-          'Error: The output file "$outputFile" does not end with ".wasm"');
-      return 255;
-    }
-    final outputFileBasename =
-        outputFile.substring(0, outputFile.length - '.wasm'.length);
+    final outputFileBasename = path.withoutExtension(outputFile);
 
     final packages = args.option(packagesOption.flag);
     final defines = args.multiOption(defineOption.flag);
@@ -974,7 +968,9 @@
         extraCompilerOptions
             .any((e) => e.contains('enable-multi-module-stress-test'));
     final optimizationLevel = int.parse(args.option('optimization-level')!);
-    final runWasmOpt = optimizationLevel >= 1;
+
+    final runWasmOpt =
+        optimizationLevel >= 1 && path.extension(outputFile) == '.wasm';
 
     if (runWasmOpt && !checkArtifactExists(sdk.wasmOpt)) {
       return 255;
diff --git a/pkg/dartdev/test/commands/compile_test.dart b/pkg/dartdev/test/commands/compile_test.dart
index 4667ed4..f0176da 100644
--- a/pkg/dartdev/test/commands/compile_test.dart
+++ b/pkg/dartdev/test/commands/compile_test.dart
@@ -815,24 +815,6 @@
     );
   }, skip: isRunningOnIA32);
 
-  test('Compile wasm with wrong output filename', () async {
-    final p = project(mainSrc: 'void main() {}');
-    final inFile = path.canonicalize(path.join(p.dirPath, p.relativeFilePath));
-    final result = await p.run(
-      [
-        'compile',
-        'wasm',
-        '-o',
-        'foo',
-        inFile,
-      ],
-    );
-
-    expect(result.stderr,
-        contains('Error: The output file "foo" does not end with ".wasm"'));
-    expect(result.exitCode, genericErrorExitCode);
-  }, skip: isRunningOnIA32);
-
   test('Compile wasm with error', () async {
     final p = project(mainSrc: '''
 void main() {
diff --git a/pkg/wasm_builder/lib/src/ir/module.dart b/pkg/wasm_builder/lib/src/ir/module.dart
index 112e514..000e3d9 100644
--- a/pkg/wasm_builder/lib/src/ir/module.dart
+++ b/pkg/wasm_builder/lib/src/ir/module.dart
@@ -119,7 +119,8 @@
     SourceMapSection(sourceMapUrl).serialize(s);
   }
 
-  static Module deserialize(Deserializer d) {
+  static (Map<int, List<Deserializer>>, Map<String, List<Deserializer>>)
+      _deserializeTopLevel(Deserializer d) {
     final preamble = d.readBytes(8);
     if (preamble[0] != 0x00 ||
         preamble[1] != 0x61 ||
@@ -151,6 +152,12 @@
       }
     }
 
+    return (sections, customSections);
+  }
+
+  static Module deserialize(Deserializer d) {
+    final (sections, customSections) = _deserializeTopLevel(d);
+
     final Module module = Module.uninitialized();
 
     // We read the sections in the order they should be in the binary.
@@ -235,6 +242,14 @@
       );
   }
 
+  /// Deserialize just the `sourceMapUrl` section of a module as a [Uri].
+  static Uri? deserializeSourceMapUrl(Deserializer d) {
+    final (sections, customSections) = _deserializeTopLevel(d);
+    final sourceMapUrl = SourceMapSection.deserialize(
+        customSections[SourceMapSection.customSectionName]?.single);
+    return sourceMapUrl;
+  }
+
   String printAsWat() {
     final mp = ModulePrinter(this);
 
diff --git a/tools/VERSION b/tools/VERSION
index d610def..6a0006f 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 3
 MINOR 11
 PATCH 0
-PRERELEASE 33
+PRERELEASE 34
 PRERELEASE_PATCH 0