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;