Proposed new API for tracking feature opt in/out

Change-Id: I777094da1301b525cd92cab1f9bf49d2f70a911b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/99784
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analyzer/lib/dart/analysis/features.dart b/pkg/analyzer/lib/dart/analysis/features.dart
new file mode 100644
index 0000000..81d1b81
--- /dev/null
+++ b/pkg/analyzer/lib/dart/analysis/features.dart
@@ -0,0 +1,86 @@
+// Copyright (c) 2019, 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 'package:analyzer/src/dart/analysis/experiments.dart';
+import 'package:meta/meta.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+/// Information about a single language feature whose presence or absence
+/// depends on the supported Dart SDK version, and possibly on the presence of
+/// experimental flags.
+abstract class Feature {
+  /// Feature information for the 2018 constant update.
+  static const constant_update_2018 = ExperimentalFeatures.constant_update_2018;
+
+  /// Feature information for non-nullability by default.
+  static const non_nullable = ExperimentalFeatures.non_nullable;
+
+  /// Feature information for control flow collections.
+  static const control_flow_collections =
+      ExperimentalFeatures.control_flow_collections;
+
+  /// Feature information for spread collections.
+  static const spread_collections = ExperimentalFeatures.spread_collections;
+
+  /// Feature information for set literals.
+  static const set_literals = ExperimentalFeatures.set_literals;
+
+  /// Feature information for the triple-shift operator.
+  static const triple_shift = ExperimentalFeatures.triple_shift;
+
+  /// If the feature may be enabled or disabled on the command line, the
+  /// experimental flag that may be used to enable it.  Otherwise `null`.
+  ///
+  /// Should be `null` if [status] is `current` or `abandoned`.
+  String get experimentalFlag;
+
+  /// If [status] is not `future`, the first version of the Dart SDK in which
+  /// the given feature was supported.  Otherwise `null`.
+  Version get firstSupportedVersion;
+
+  /// The status of the feature.
+  FeatureStatus get status;
+}
+
+/// An unordered collection of [Feature] objects.
+abstract class FeatureSet {
+  /// Computes a set of features for use in a unit test.  Computes the set of
+  /// features enabled in [sdkVersion], plus any specified [additionalFeatures].
+  ///
+  /// If [sdkVersion] is not supplied (or is `null`), then the current set of
+  /// enabled features is used as the starting point.
+  @visibleForTesting
+  factory FeatureSet.forTesting(
+          {String sdkVersion, List<Feature> additionalFeatures}) =
+      // ignore: invalid_use_of_visible_for_testing_member
+      ExperimentStatus.forTesting;
+
+  /// Computes the set of features implied by the given set of experimental
+  /// enable flags.
+  factory FeatureSet.fromEnableFlags(List<String> flags) =
+      ExperimentStatus.fromStrings;
+
+  /// Computes a subset of this FeatureSet by removing any features that weren't
+  /// available in the given Dart SDK version.
+  FeatureSet restrictToVersion(Version version);
+}
+
+/// Information about the status of a language feature.
+enum FeatureStatus {
+  /// The language feature has not yet shipped.  It may not be used unless an
+  /// experimental flag is used to enable it.
+  future,
+
+  /// The language feature has not yet shipped, but we are testing the effect of
+  /// enabling it by default.  It may be used in any library with an appopriate
+  /// version constraint, unless an experimental flag is used to disable it.
+  provisional,
+
+  /// The language feature has been shipped.  It may be used in any library with
+  /// an appropriate version constraint.
+  current,
+
+  /// The language feature is no longer planned.  It may not be used.
+  abandoned,
+}
diff --git a/pkg/analyzer/lib/src/dart/analysis/experiments.dart b/pkg/analyzer/lib/src/dart/analysis/experiments.dart
index 54978bd..ebd988d 100644
--- a/pkg/analyzer/lib/src/dart/analysis/experiments.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/experiments.dart
@@ -2,21 +2,11 @@
 // 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.
 
-// Note: the plan is to generate this file from a YAML representation somewhere
-// in the SDK repo.  Please do not add any code to this file that can't be
-// easily code generated based on a knowledge of the current set of experimental
-// flags and their status.
-// TODO(paulberry,kmillikin): once code generation is implemented, replace this
-// notice with a notice that this file is generated and a pointer to the source
-// YAML file and the regeneration tool.
-
-// Note: to demonstrate how code is supposed to be generated for expired flags,
-// this file contains bogus expired flags called "bogus-enabled" and
-// "bogus-disabled".  They are not used and can be removed at the time that code
-// generation is implemented.
-
+import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/src/dart/analysis/experiments_impl.dart';
+import 'package:analyzer/src/generated/utilities_general.dart';
 import 'package:meta/meta.dart';
+import 'package:pub_semver/src/version.dart';
 
 export 'package:analyzer/src/dart/analysis/experiments_impl.dart'
     show
@@ -62,59 +52,83 @@
   static const String bogus_enabled = 'bogus-enabled';
 }
 
+class ExperimentalFeatures {
+  static const constant_update_2018 = const ExperimentalFeature(
+      0,
+      EnableString.constant_update_2018,
+      IsEnabledByDefault.constant_update_2018,
+      IsExpired.constant_update_2018,
+      'Q4 2018 Constant Update');
+
+  static const non_nullable = const ExperimentalFeature(
+      1,
+      EnableString.non_nullable,
+      IsEnabledByDefault.non_nullable,
+      IsExpired.non_nullable,
+      'Non Nullable');
+
+  static const control_flow_collections = const ExperimentalFeature(
+      null,
+      EnableString.control_flow_collections,
+      IsEnabledByDefault.control_flow_collections,
+      IsExpired.control_flow_collections,
+      'Control Flow Collections',
+      firstSupportedVersion: '2.2.2');
+
+  static const spread_collections = const ExperimentalFeature(
+      null,
+      EnableString.spread_collections,
+      IsEnabledByDefault.spread_collections,
+      IsExpired.spread_collections,
+      'Spread Collections',
+      firstSupportedVersion: '2.2.2');
+
+  static const set_literals = const ExperimentalFeature(
+      null,
+      EnableString.set_literals,
+      IsEnabledByDefault.set_literals,
+      IsExpired.set_literals,
+      'Set Literals',
+      firstSupportedVersion: '2.2.0');
+
+  static const triple_shift = const ExperimentalFeature(
+      2,
+      EnableString.triple_shift,
+      IsEnabledByDefault.triple_shift,
+      IsExpired.triple_shift,
+      'Triple-shift operator');
+
+  static const bogus_disabled = const ExperimentalFeature(
+      null,
+      EnableString.bogus_disabled,
+      IsEnabledByDefault.bogus_disabled,
+      IsExpired.bogus_disabled,
+      null);
+
+  static const bogus_enabled = const ExperimentalFeature(
+      null,
+      EnableString.bogus_enabled,
+      IsEnabledByDefault.bogus_enabled,
+      IsExpired.bogus_enabled,
+      null,
+      firstSupportedVersion: '1.0');
+}
+
 /// A representation of the set of experiments that are active and whether they
 /// are enabled.
-class ExperimentStatus {
+class ExperimentStatus implements FeatureSet {
   /// A map containing information about all known experimental flags.
   static const knownFeatures = <String, ExperimentalFeature>{
-    EnableString.constant_update_2018: const ExperimentalFeature(
-        0,
-        EnableString.constant_update_2018,
-        IsEnabledByDefault.constant_update_2018,
-        IsExpired.constant_update_2018,
-        'Q4 2018 Constant Update'),
-    EnableString.non_nullable: const ExperimentalFeature(
-        1,
-        EnableString.non_nullable,
-        IsEnabledByDefault.non_nullable,
-        IsExpired.non_nullable,
-        'Non Nullable'),
-    EnableString.control_flow_collections: const ExperimentalFeature(
-        null,
-        EnableString.control_flow_collections,
-        IsEnabledByDefault.control_flow_collections,
-        IsExpired.control_flow_collections,
-        'Control Flow Collections'),
-    EnableString.spread_collections: const ExperimentalFeature(
-        null,
-        EnableString.spread_collections,
-        IsEnabledByDefault.spread_collections,
-        IsExpired.spread_collections,
-        'Spread Collections'),
-    EnableString.set_literals: const ExperimentalFeature(
-        null,
-        EnableString.set_literals,
-        IsEnabledByDefault.set_literals,
-        IsExpired.set_literals,
-        'Set Literals'),
-    EnableString.triple_shift: const ExperimentalFeature(
-        2,
-        EnableString.triple_shift,
-        IsEnabledByDefault.triple_shift,
-        IsExpired.triple_shift,
-        'Triple-shift operator'),
-    EnableString.bogus_disabled: const ExperimentalFeature(
-        null,
-        EnableString.bogus_disabled,
-        IsEnabledByDefault.bogus_disabled,
-        IsExpired.bogus_disabled,
-        null),
-    EnableString.bogus_enabled: const ExperimentalFeature(
-        null,
-        EnableString.bogus_enabled,
-        IsEnabledByDefault.bogus_enabled,
-        IsExpired.bogus_enabled,
-        null),
+    EnableString.constant_update_2018:
+        ExperimentalFeatures.constant_update_2018,
+    EnableString.non_nullable: ExperimentalFeatures.non_nullable,
+    EnableString.control_flow_collections:
+        ExperimentalFeatures.control_flow_collections,
+    EnableString.spread_collections: ExperimentalFeatures.spread_collections,
+    EnableString.set_literals: ExperimentalFeatures.set_literals,
+    EnableString.triple_shift: ExperimentalFeatures.triple_shift,
+    EnableString.bogus_disabled: ExperimentalFeatures.bogus_disabled,
+    EnableString.bogus_enabled: ExperimentalFeatures.bogus_enabled,
   };
 
   final List<bool> _enableFlags;
@@ -134,6 +148,17 @@
           triple_shift ?? IsEnabledByDefault.triple_shift,
         ];
 
+  /// Computes a set of features for use in a unit test.  Computes the set of
+  /// features enabled in [sdkVersion], plus any specified [additionalFeatures].
+  ///
+  /// If [sdkVersion] is not supplied (or is `null`), then the current set of
+  /// enabled features is used as the starting point.
+  @visibleForTesting
+  ExperimentStatus.forTesting(
+      {String sdkVersion, List<Feature> additionalFeatures: const []})
+      : this._(enableFlagsForTesting(
+            sdkVersion: sdkVersion, additionalFeatures: additionalFeatures));
+
   /// Decodes the strings given in [flags] into a representation of the set of
   /// experiments that should be enabled.
   ///
@@ -156,6 +181,15 @@
   /// Current state for the flag "control_flow_collections"
   bool get control_flow_collections => true;
 
+  @override
+  int get hashCode {
+    int hash = 0;
+    for (var flag in _enableFlags) {
+      hash = JenkinsSmiHash.combine(hash, flag.hashCode);
+    }
+    return JenkinsSmiHash.finish(hash);
+  }
+
   /// Current state for the flag "non-nullable"
   bool get non_nullable => _enableFlags[1];
 
@@ -168,11 +202,27 @@
   /// Current state for the flag "triple_shift"
   bool get triple_shift => _enableFlags[2];
 
+  @override
+  operator ==(Object other) {
+    if (other is ExperimentStatus) {
+      if (_enableFlags.length != other._enableFlags.length) return false;
+      for (int i = 0; i < _enableFlags.length; i++) {
+        if (_enableFlags[i] != other._enableFlags[i]) return false;
+      }
+      return true;
+    }
+    return false;
+  }
+
   /// Queries whether the given [feature] is enabled or disabled.
   bool isEnabled(ExperimentalFeature feature) => feature.isExpired
       ? feature.isEnabledByDefault
       : _enableFlags[feature.index];
 
+  @override
+  FeatureSet restrictToVersion(Version version) =>
+      ExperimentStatus._(restrictEnableFlagsToVersion(_enableFlags, version));
+
   /// Returns a list of strings suitable for passing to
   /// [ExperimentStatus.fromStrings].
   List<String> toStringList() => experimentStatusToStringList(this);
diff --git a/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart b/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
index 64d4b7e..9def415 100644
--- a/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/experiments_impl.dart
@@ -2,8 +2,10 @@
 // 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 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/src/dart/analysis/experiments.dart';
 import 'package:meta/meta.dart';
+import 'package:pub_semver/pub_semver.dart';
 
 /// The same as [ExperimentStatus.knownFeatures], except when a call to
 /// [overrideKnownFeatures] is in progress.
@@ -32,6 +34,28 @@
   return decodedFlags;
 }
 
+/// Computes a set of features for use in a unit test.  Computes the set of
+/// features enabled in [sdkVersion], plus any specified [additionalFeatures].
+///
+/// If [sdkVersion] is not supplied (or is `null`), then the current set of
+/// enabled features is used as the starting point.
+List<bool> enableFlagsForTesting(
+    {String sdkVersion, List<Feature> additionalFeatures: const []}) {
+  var flags = decodeFlags([]);
+  if (sdkVersion != null) {
+    flags = restrictEnableFlagsToVersion(flags, Version.parse(sdkVersion));
+  }
+  for (ExperimentalFeature feature in additionalFeatures) {
+    if (feature.isExpired) {
+      // At the moment we can't enable features in the "expired" state.
+      // TODO(paulberry): fix this by including such features in enable flags.
+      continue;
+    }
+    flags[feature.index] = true;
+  }
+  return flags;
+}
+
 /// Converts the flags in [status] to a list of strings suitable for
 /// passing to [_decodeFlags].
 List<String> experimentStatusToStringList(ExperimentStatus status) {
@@ -64,6 +88,24 @@
   }
 }
 
+/// Computes a new set of enable flags based on [flags], but with any features
+/// that were not present in [version] set to `false`.
+List<bool> restrictEnableFlagsToVersion(List<bool> flags, Version version) {
+  flags = List.from(flags);
+  for (var feature in _knownFeatures.values) {
+    if (feature.isExpired) {
+      // At the moment we can't disable features in the "expired" state.
+      // TODO(paulberry): fix this by including such features in enable flags.
+      continue;
+    }
+    if (!feature.isEnabledByDefault ||
+        feature.firstSupportedVersion > version) {
+      flags[feature.index] = false;
+    }
+  }
+  return flags;
+}
+
 /// Validates whether there are any disagreements between the strings given in
 /// [flags1] and the strings given in [flags2].
 ///
@@ -205,7 +247,7 @@
 
 /// Information about a single experimental flag that the user might use to
 /// request that a feature be enabled (or disabled).
-class ExperimentalFeature {
+class ExperimentalFeature implements Feature {
   /// Index of the flag in the private data structure maintained by
   /// [ExperimentStatus].
   ///
@@ -231,13 +273,49 @@
   /// Documentation for the feature, if known.  `null` for expired flags.
   final String documentation;
 
+  final String _firstSupportedVersion;
+
   const ExperimentalFeature(this.index, this.enableString,
-      this.isEnabledByDefault, this.isExpired, this.documentation)
-      : assert(isExpired ? index == null : index != null);
+      this.isEnabledByDefault, this.isExpired, this.documentation,
+      {String firstSupportedVersion})
+      : _firstSupportedVersion = firstSupportedVersion,
+        assert(isExpired ? index == null : index != null),
+        assert(isEnabledByDefault
+            ? firstSupportedVersion != null
+            : firstSupportedVersion == null);
 
   /// The string to disable the feature.
   String get disableString => 'no-$enableString';
 
+  @override
+  String get experimentalFlag => isExpired ? null : enableString;
+
+  @override
+  Version get firstSupportedVersion {
+    if (_firstSupportedVersion == null) {
+      return null;
+    } else {
+      return Version.parse(_firstSupportedVersion);
+    }
+  }
+
+  @override
+  FeatureStatus get status {
+    if (isExpired) {
+      if (isEnabledByDefault) {
+        return FeatureStatus.current;
+      } else {
+        return FeatureStatus.abandoned;
+      }
+    } else {
+      if (isEnabledByDefault) {
+        return FeatureStatus.provisional;
+      } else {
+        return FeatureStatus.future;
+      }
+    }
+  }
+
   /// Retrieves the string to enable or disable the feature, depending on
   /// [value].
   String stringForValue(bool value) => value ? enableString : disableString;
diff --git a/pkg/analyzer/lib/src/generated/engine.dart b/pkg/analyzer/lib/src/generated/engine.dart
index 07563fc..8f4c1b3 100644
--- a/pkg/analyzer/lib/src/generated/engine.dart
+++ b/pkg/analyzer/lib/src/generated/engine.dart
@@ -6,6 +6,7 @@
 import 'dart:collection';
 import 'dart:typed_data';
 
+import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element.dart';
@@ -803,6 +804,9 @@
   /// see if it is complaint with Chrome OS.
   bool get chromeOsManifestChecks;
 
+  /// The set of features that are globally enabled for this context.
+  FeatureSet get contextFeatures;
+
   /// Return `true` if analysis is to generate dart2js related hint results.
   bool get dart2jsHint;
 
@@ -994,8 +998,7 @@
 
   List<String> _enabledExperiments = const <String>[];
 
-  /// Parsed [enabledExperiments].
-  ExperimentStatus _experimentStatus = ExperimentStatus();
+  ExperimentStatus _contextFeatures = ExperimentStatus();
 
   @override
   List<String> enabledPluginNames = const <String>[];
@@ -1145,6 +1148,14 @@
     _analyzeFunctionBodiesPredicate = value;
   }
 
+  @override
+  FeatureSet get contextFeatures => _contextFeatures;
+
+  set contextFeatures(FeatureSet featureSet) {
+    _contextFeatures = featureSet;
+    _enabledExperiments = _contextFeatures.toStringList();
+  }
+
   @deprecated
   @override
   bool get enableAssertInitializer => true;
@@ -1177,7 +1188,7 @@
 
   set enabledExperiments(List<String> enabledExperiments) {
     _enabledExperiments = enabledExperiments;
-    _experimentStatus = ExperimentStatus.fromStrings(enabledExperiments);
+    _contextFeatures = ExperimentStatus.fromStrings(enabledExperiments);
   }
 
   @override
@@ -1230,7 +1241,7 @@
   }
 
   /// The set of enabled experiments.
-  ExperimentStatus get experimentStatus => _experimentStatus;
+  ExperimentStatus get experimentStatus => _contextFeatures;
 
   /// Return `true` to enable mixin declarations.
   /// https://github.com/dart-lang/language/issues/12
diff --git a/pkg/analyzer/test/src/dart/analysis/experiments_test.dart b/pkg/analyzer/test/src/dart/analysis/experiments_test.dart
index aa83160..597430b 100644
--- a/pkg/analyzer/test/src/dart/analysis/experiments_test.dart
+++ b/pkg/analyzer/test/src/dart/analysis/experiments_test.dart
@@ -51,7 +51,8 @@
 
   test_fromStrings_default_values() {
     knownFeatures['a'] = ExperimentalFeature(0, 'a', false, false, 'a');
-    knownFeatures['b'] = ExperimentalFeature(1, 'b', true, false, 'b');
+    knownFeatures['b'] = ExperimentalFeature(1, 'b', true, false, 'b',
+        firstSupportedVersion: '1.0');
     expect(getFlags(fromStrings([])), [false, true]);
   }
 
@@ -61,7 +62,8 @@
   }
 
   test_fromStrings_disable_enabled_feature() {
-    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a');
+    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a',
+        firstSupportedVersion: '1.0');
     expect(getFlags(fromStrings(['no-a'])), [false]);
   }
 
@@ -71,13 +73,15 @@
   }
 
   test_fromStrings_enable_enabled_feature() {
-    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a');
+    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a',
+        firstSupportedVersion: '1.0');
     expect(getFlags(fromStrings(['a'])), [true]);
   }
 
   test_fromStrings_illegal_use_of_expired_flag_disable() {
     // Expired flags are ignored even if they would fail validation.
-    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a');
+    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a',
+        firstSupportedVersion: '1.0');
     expect(getFlags(fromStrings(['no-a'])), []);
   }
 
@@ -95,7 +99,8 @@
 
   test_fromStrings_unnecessary_use_of_expired_flag_enable() {
     // Expired flags are ignored.
-    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a');
+    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a',
+        firstSupportedVersion: '1.0');
     expect(getFlags(fromStrings(['a'])), []);
   }
 
@@ -162,7 +167,8 @@
   }
 
   test_validateFlags_ignore_redundant_disable_flags() {
-    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a');
+    knownFeatures['a'] = ExperimentalFeature(0, 'a', true, false, 'a',
+        firstSupportedVersion: '1.0');
     expect(getValidationResult(['no-a', 'no-a']), isEmpty);
   }
 
@@ -172,7 +178,8 @@
   }
 
   test_validateFlags_illegal_use_of_expired_flag_disable() {
-    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a');
+    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a',
+        firstSupportedVersion: '1.0');
     var validationResult = getValidationResult(['no-a']);
     expect(validationResult, hasLength(1));
     var error = validationResult[0] as IllegalUseOfExpiredFlag;
@@ -205,7 +212,8 @@
   }
 
   test_validateFlags_unnecessary_use_of_expired_flag_enable() {
-    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a');
+    knownFeatures['a'] = ExperimentalFeature(null, 'a', true, true, 'a',
+        firstSupportedVersion: '1.0');
     var validationResult = getValidationResult(['a']);
     expect(validationResult, hasLength(1));
     var error = validationResult[0] as UnnecessaryUseOfExpiredFlag;