[native_assets] Disable experiment on stable and beta channel

We want to avoid users passing `--enable-experiment=native-assets` on
stable and beta, as we'd like to move fast and break things on the
experiment. This aligns the experiment with how the experiment is
working in Flutter: main and dev branch only.

Before this CL, dartdev did not check experiment flags. Unknown
experiments would fail in the VM. After this CL, dartdev checks the
experiment flags and errors out early.

Change-Id: I875ea3272f4b67342da19ea2e4be329a4b380573
Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-linux-release-try,pkg-mac-release-arm64-try,pkg-mac-release-try,pkg-win-release-arm64-try,pkg-win-release-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/406660
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
diff --git a/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart b/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
index 7f78b84..8e6d831 100644
--- a/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
@@ -187,6 +187,7 @@
     documentation: 'Augmentations - enhancing declarations from outside',
     experimentalReleaseVersion: Version.parse('3.6.0'),
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final class_modifiers = ExperimentalFeature(
@@ -197,6 +198,7 @@
     documentation: 'Class modifiers',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final const_functions = ExperimentalFeature(
@@ -208,6 +210,7 @@
         'Allow more of the Dart language to be executed in const expressions.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final constant_update_2018 = ExperimentalFeature(
@@ -218,6 +221,7 @@
     documentation: 'Enhanced constant expressions',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final constructor_tearoffs = ExperimentalFeature(
@@ -229,6 +233,7 @@
         'Allow constructor tear-offs and explicit generic instantiations.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.15.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final control_flow_collections = ExperimentalFeature(
@@ -239,6 +244,7 @@
     documentation: 'Control Flow Collections',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final digit_separators = ExperimentalFeature(
@@ -249,6 +255,7 @@
     documentation: 'Number literals with digit separators.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.6.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final enhanced_enums = ExperimentalFeature(
@@ -259,6 +266,7 @@
     documentation: 'Enhanced Enums',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.17.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final enhanced_parts = ExperimentalFeature(
@@ -269,6 +277,7 @@
     documentation: 'Generalize parts to be nested and have exports/imports.',
     experimentalReleaseVersion: Version.parse('3.6.0'),
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final enum_shorthands = ExperimentalFeature(
@@ -279,6 +288,7 @@
     documentation: 'Shorter dot syntax for enum values.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final extension_methods = ExperimentalFeature(
@@ -289,6 +299,7 @@
     documentation: 'Extension Methods',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.6.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final generic_metadata = ExperimentalFeature(
@@ -300,6 +311,7 @@
         'Allow annotations to accept type arguments; also allow generic function types as type arguments.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.14.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final getter_setter_error = ExperimentalFeature(
@@ -311,6 +323,7 @@
         'Stop reporting errors about mismatching types in a getter/setter pair.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inference_update_1 = ExperimentalFeature(
@@ -322,6 +335,7 @@
         'Horizontal type inference for function expressions passed to generic invocations.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.18.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inference_update_2 = ExperimentalFeature(
@@ -332,6 +346,7 @@
     documentation: 'Type promotion for fields',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.2.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inference_update_3 = ExperimentalFeature(
@@ -343,6 +358,7 @@
         'Better handling of conditional expressions, and switch expressions.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.4.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inference_update_4 = ExperimentalFeature(
@@ -353,6 +369,7 @@
     documentation: 'A bundle of updates to type inference.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inference_using_bounds = ExperimentalFeature(
@@ -364,6 +381,7 @@
         'Use type parameter bounds more extensively in type inference.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.7.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final inline_class = ExperimentalFeature(
@@ -374,6 +392,7 @@
     documentation: 'Extension Types',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.3.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final macros = ExperimentalFeature(
@@ -384,6 +403,7 @@
     documentation: 'Static meta-programming',
     experimentalReleaseVersion: Version.parse('3.3.0'),
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final named_arguments_anywhere = ExperimentalFeature(
@@ -394,6 +414,7 @@
     documentation: 'Named Arguments Anywhere',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.17.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final native_assets = ExperimentalFeature(
@@ -404,6 +425,7 @@
     documentation: 'Compile and bundle native assets.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["main", "dev"],
   );
 
   static final non_nullable = ExperimentalFeature(
@@ -414,6 +436,7 @@
     documentation: 'Non Nullable by default',
     experimentalReleaseVersion: Version.parse('2.10.0'),
     releaseVersion: Version.parse('2.12.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final nonfunction_type_aliases = ExperimentalFeature(
@@ -424,6 +447,7 @@
     documentation: 'Type aliases define a <type>, not just a <functionType>',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.13.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final null_aware_elements = ExperimentalFeature(
@@ -434,6 +458,7 @@
     documentation: 'Null-aware elements and map entries in collections.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final patterns = ExperimentalFeature(
@@ -444,6 +469,7 @@
     documentation: 'Patterns',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final record_use = ExperimentalFeature(
@@ -454,6 +480,7 @@
     documentation: 'Output arguments used by static functions.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["main", "dev"],
   );
 
   static final records = ExperimentalFeature(
@@ -464,6 +491,7 @@
     documentation: 'Records',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final sealed_class = ExperimentalFeature(
@@ -474,6 +502,7 @@
     documentation: 'Sealed class',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final set_literals = ExperimentalFeature(
@@ -484,6 +513,7 @@
     documentation: 'Set Literals',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final spread_collections = ExperimentalFeature(
@@ -494,6 +524,7 @@
     documentation: 'Spread Collections',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.0.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final super_parameters = ExperimentalFeature(
@@ -504,6 +535,7 @@
     documentation: 'Super-Initializer Parameters',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.17.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final test_experiment = ExperimentalFeature(
@@ -515,6 +547,7 @@
         'Has no effect. Can be used for testing the --enable-experiment command line functionality.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final triple_shift = ExperimentalFeature(
@@ -525,6 +558,7 @@
     documentation: 'Triple-shift operator',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.14.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final unnamed_libraries = ExperimentalFeature(
@@ -535,6 +569,7 @@
     documentation: 'Unnamed libraries',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('2.19.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final unquoted_imports = ExperimentalFeature(
@@ -545,6 +580,7 @@
     documentation: 'Shorter import syntax.',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final variance = ExperimentalFeature(
@@ -555,6 +591,7 @@
     documentation: 'Sound variance',
     experimentalReleaseVersion: null,
     releaseVersion: null,
+    channels: ["stable", "beta", "dev", "main"],
   );
 
   static final wildcard_variables = ExperimentalFeature(
@@ -566,6 +603,7 @@
         'Local declarations and parameters named `_` are non-binding.',
     experimentalReleaseVersion: null,
     releaseVersion: Version.parse('3.7.0'),
+    channels: ["stable", "beta", "dev", "main"],
   );
 }
 
diff --git a/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart b/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
index 2b3e4c9..18dc81a 100644
--- a/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
@@ -317,6 +317,12 @@
   @override
   final Version? releaseVersion;
 
+  /// The channels on which this experiment is available.
+  ///
+  /// Valid channels are "stable", "beta", and "main". The "dev" channel in Dart
+  /// is implied by main.
+  final List<String> channels;
+
   ExperimentalFeature({
     required this.index,
     required this.enableString,
@@ -325,6 +331,7 @@
     required this.documentation,
     required this.experimentalReleaseVersion,
     required this.releaseVersion,
+    this.channels = const [],
   }) : assert(isEnabledByDefault
             ? releaseVersion != null
             : releaseVersion == null);
diff --git a/pkg/analyzer/tool/experiments/generate.dart b/pkg/analyzer/tool/experiments/generate.dart
index 3304ccc..1bb584c 100644
--- a/pkg/analyzer/tool/experiments/generate.dart
+++ b/pkg/analyzer/tool/experiments/generate.dart
@@ -155,6 +155,10 @@
       var experimentalReleaseVersion =
           (features[key] as YamlMap)['experimentalReleaseVersion'];
       var enabledIn = (features[key] as YamlMap)['enabledIn'];
+      var channels =
+          (((features[key] as YamlMap)['channels']) as List?)?.cast<String>();
+      channels ??= ['stable', 'beta', 'dev', 'main'];
+      var channelsLiteral = '[${channels.map((e) => '"$e"').join(', ')}]';
       out.write('''
 
       static final $id = ExperimentalFeature(
@@ -180,6 +184,7 @@
       } else {
         out.write("releaseVersion: null,");
       }
+      out.write("channels: $channelsLiteral,");
       out.writeln(');');
       ++index;
     }
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index bc33817..aad08c8 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -223,6 +223,13 @@
       log = Logger.verbose(ansi: ansi);
     }
 
+    late final List<String> experimentErrors =
+        validateExperiments(vmEnabledExperiments);
+    if (experimentErrors.isNotEmpty) {
+      experimentErrors.forEach(io.stderr.writeln);
+      return 254;
+    }
+
     var command = topLevelResults.command;
     final commandNames = [];
     while (command != null) {
diff --git a/pkg/dartdev/lib/src/experiments.dart b/pkg/dartdev/lib/src/experiments.dart
index 311e3d5..d13ffef 100644
--- a/pkg/dartdev/lib/src/experiments.dart
+++ b/pkg/dartdev/lib/src/experiments.dart
@@ -7,6 +7,7 @@
 import 'package:analyzer/src/dart/analysis/experiments.dart';
 import 'package:args/args.dart';
 import 'package:collection/collection.dart' show IterableExtension;
+import 'package:dartdev/src/sdk.dart';
 
 const experimentFlagName = 'enable-experiment';
 
@@ -101,3 +102,28 @@
 
 bool recordUseEnabled(List<String> vmEnabledExperiments) =>
     vmEnabledExperiments.contains(ExperimentalFeatures.record_use.enableString);
+
+List<String> validateExperiments(List<String> vmEnabledExperiments) {
+  final errors = <String>[];
+  for (final enabledExperiment in vmEnabledExperiments) {
+    final experiment = experimentalFeatures.firstWhereOrNull(
+        (feature) => feature.enableString == enabledExperiment);
+    if (experiment == null) {
+      errors.add('Unknown experiment: $enabledExperiment');
+    } else if (!_availableOnCurrentChannel(experiment.channels)) {
+      final availableChannels = experiment.channels.join(', ');
+      final s = experiment.channels.length >= 2 ? 's' : '';
+      errors.add(
+        'Unavailable experiment: ${experiment.enableString} (this experiment '
+        'is only available on the $availableChannels channel$s, '
+        'this current channel is ${Runtime.runtime.channel})',
+      );
+    }
+  }
+  return errors;
+}
+
+bool _availableOnCurrentChannel(List<String> channels) {
+  final channel = Runtime.runtime.channel;
+  return channels.contains(channel);
+}
diff --git a/pkg/dartdev/lib/src/sdk.dart b/pkg/dartdev/lib/src/sdk.dart
index 6f105a4..a49a282 100644
--- a/pkg/dartdev/lib/src/sdk.dart
+++ b/pkg/dartdev/lib/src/sdk.dart
@@ -240,7 +240,7 @@
   /// The SDK's semantic versioning version (x.y.z-a.b.channel).
   final String version;
 
-  /// The SDK's release channel (`be`, `dev`, `beta`, `stable`).
+  /// The SDK's release channel (`main`, `dev`, `beta`, `stable`).
   ///
   /// May be null if [Platform.version] does not have the expected format.
   final String? channel;
diff --git a/pkg/dartdev/test/commands/create_integration_test.dart b/pkg/dartdev/test/commands/create_integration_test.dart
index bbb3115..fe4262d 100644
--- a/pkg/dartdev/test/commands/create_integration_test.dart
+++ b/pkg/dartdev/test/commands/create_integration_test.dart
@@ -36,6 +36,8 @@
         templateId,
         projectName,
       ]);
+      printOnFailure(createResult.stdout);
+      printOnFailure(createResult.stderr);
       expect(createResult.exitCode, 0, reason: createResult.stderr);
 
       // Validate that the project analyzes cleanly.
diff --git a/pkg/dartdev/test/experiments_test.dart b/pkg/dartdev/test/experiments_test.dart
index 0e73046..681eafe 100644
--- a/pkg/dartdev/test/experiments_test.dart
+++ b/pkg/dartdev/test/experiments_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:dartdev/src/experiments.dart';
+import 'package:dartdev/src/sdk.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -14,5 +15,29 @@
         contains('test-experiment'),
       );
     });
+
+    test('unknown experiment', () {
+      final errors = validateExperiments(['foo']);
+      expect(errors, equals(['Unknown experiment: foo']));
+    });
+
+    test('native assets experiment', () {
+      final errors = validateExperiments(['native-assets']);
+      final channel = Runtime.runtime.channel!;
+      switch (channel) {
+        case 'stable':
+        case 'beta':
+          expect(
+            errors,
+            equals([
+              'Unavailable experiment: native-assets (this experiment is only '
+                  'available on the main, dev channels, this current channel is $channel)',
+            ]),
+          );
+
+        default:
+          expect(errors, isEmpty);
+      }
+    });
   });
 }
diff --git a/pkg/dartdev/test/native_assets/build_test.dart b/pkg/dartdev/test/native_assets/build_test.dart
index a813eaa..c1ca339 100644
--- a/pkg/dartdev/test/native_assets/build_test.dart
+++ b/pkg/dartdev/test/native_assets/build_test.dart
@@ -22,6 +22,10 @@
 String targetOSMessage(String targetOS) => 'Target OS: $targetOS';
 
 void main(List<String> args) async {
+  if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
+    return;
+  }
+
   final bool fromDartdevSource = args.contains('--source');
   final hostOS = Platform.operatingSystem;
   final crossOS = Platform.isLinux ? 'macos' : 'linux';
diff --git a/pkg/dartdev/test/native_assets/compile_test.dart b/pkg/dartdev/test/native_assets/compile_test.dart
index 8050aaf..0c69fa5 100644
--- a/pkg/dartdev/test/native_assets/compile_test.dart
+++ b/pkg/dartdev/test/native_assets/compile_test.dart
@@ -12,6 +12,10 @@
 import 'helpers.dart';
 
 void main() async {
+  if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
+    return;
+  }
+
   test('dart compile not supported', timeout: longTimeout, () async {
     await nativeAssetsTest('dart_app', (dartAppUri) async {
       final result = await runDart(
diff --git a/pkg/dartdev/test/native_assets/helpers.dart b/pkg/dartdev/test/native_assets/helpers.dart
index 3e4efab..267b2e0 100644
--- a/pkg/dartdev/test/native_assets/helpers.dart
+++ b/pkg/dartdev/test/native_assets/helpers.dart
@@ -5,6 +5,8 @@
 import 'dart:async';
 import 'dart:io';
 
+import 'package:analyzer/src/dart/analysis/experiments.dart';
+import 'package:dartdev/src/sdk.dart';
 import 'package:file/local.dart';
 import 'package:logging/logging.dart';
 import 'package:native_assets_builder/src/utils/run_process.dart'
@@ -247,3 +249,7 @@
   }
   return result;
 }
+
+final nativeAssetsExperimentAvailableOnCurrentChannel = ExperimentalFeatures
+    .native_assets.channels
+    .contains(Runtime.runtime.channel);
diff --git a/pkg/dartdev/test/native_assets/run_test.dart b/pkg/dartdev/test/native_assets/run_test.dart
index a7f92d2..cfd0fb3 100644
--- a/pkg/dartdev/test/native_assets/run_test.dart
+++ b/pkg/dartdev/test/native_assets/run_test.dart
@@ -10,6 +10,30 @@
 import 'helpers.dart';
 
 void main(List<String> args) async {
+  if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
+    test('dart run', timeout: longTimeout, () async {
+      await nativeAssetsTest('dart_app', (dartAppUri) async {
+        final result = await runDart(
+          arguments: [
+            '--enable-experiment=native-assets',
+            'run',
+          ],
+          workingDirectory: dartAppUri,
+          logger: logger,
+          expectExitCodeZero: false,
+        );
+        expect(result.exitCode, 254);
+        expect(
+            result.stderr,
+            stringContainsInOrder(
+              ['Unavailable experiment: native-assets'],
+            ));
+      });
+    });
+
+    return;
+  }
+
   // No --source option, `dart run` from source does not output target program
   // stdout.
 
diff --git a/pkg/dartdev/test/native_assets/test_test.dart b/pkg/dartdev/test/native_assets/test_test.dart
index f85c69c..c83e956 100644
--- a/pkg/dartdev/test/native_assets/test_test.dart
+++ b/pkg/dartdev/test/native_assets/test_test.dart
@@ -10,6 +10,10 @@
 import 'helpers.dart';
 
 void main(List<String> args) async {
+  if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
+    return;
+  }
+
   // No --source option, `dart run` from source does not output target program
   // stdout.
 
diff --git a/tools/experimental_features.yaml b/tools/experimental_features.yaml
index da18eec..4f87f64 100644
--- a/tools/experimental_features.yaml
+++ b/tools/experimental_features.yaml
@@ -125,9 +125,11 @@
 
   native-assets:
     help: "Compile and bundle native assets."
+    channels: [ "main", "dev" ]
 
   record-use:
     help: "Output arguments used by static functions."
+    channels: [ "main", "dev" ]
 
   null-aware-elements:
     help: "Null-aware elements and map entries in collections."