| /// This file contains code to generate experimental flags | |
| /// based on the information in tools/experimental_features.yaml. | |
| library; | |
| import 'dart:io'; | |
| import 'package:_fe_analyzer_shared/src/util/text_conversion.dart'; | |
| import 'package:analyzer_testing/package_root.dart' as pkg_root; | |
| import 'package:analyzer_utilities/tools.dart'; | |
| import 'package:path/path.dart'; | |
| import 'package:yaml/yaml.dart' show YamlMap, loadYaml; | |
| void main() async { | |
| await GeneratedContent.generateAll(pkg_root.packageRoot, allTargets); | |
| } | |
| List<GeneratedContent> get allTargets { | |
| var experimentsYaml = | |
| loadYaml( | |
| File( | |
| join( | |
| normalize(join(pkg_root.packageRoot, '../tools')), | |
| 'experimental_features.yaml', | |
| ), | |
| ).readAsStringSync(), | |
| ) | |
| as Map; | |
| return <GeneratedContent>[ | |
| GeneratedFile('analyzer/lib/src/dart/analysis/experiments.g.dart', ( | |
| pkgRoot, | |
| ) async { | |
| var generator = _ExperimentsGenerator(experimentsYaml); | |
| generator.generateFormatCode(); | |
| return generator.out.toString(); | |
| }), | |
| ]; | |
| } | |
| class _ExperimentsGenerator { | |
| final Map experimentsYaml; | |
| late List<String> keysSorted; | |
| final out = StringBuffer(''' | |
| // | |
| // THIS FILE IS GENERATED. DO NOT EDIT. | |
| // | |
| // Instead modify 'tools/experimental_features.yaml' and run | |
| // 'dart pkg/analyzer/tool/experiments/generate.dart' to update. | |
| part of 'experiments.dart'; | |
| // We allow some snake_case and SCREAMING_SNAKE_CASE identifiers in generated | |
| // code, as they match names declared in the source configuration files. | |
| // ignore_for_file: constant_identifier_names | |
| '''); | |
| Map<String, dynamic>? _features; | |
| _ExperimentsGenerator(this.experimentsYaml); | |
| Map<String, dynamic> get features { | |
| var features = _features; | |
| if (features != null) return features; | |
| features = <String, dynamic>{}; | |
| var yamlFeatures = experimentsYaml['features'] as Map; | |
| for (MapEntry entry in yamlFeatures.entries) { | |
| var category = | |
| (entry.value as YamlMap)['category'] as String? ?? 'language'; | |
| if (category != "language") { | |
| // Skip a feature with a category that's not language. In the future | |
| // possibly allow e.g. 'analyzer' etc. | |
| continue; | |
| } | |
| features[entry.key as String] = entry.value; | |
| } | |
| return _features = features; | |
| } | |
| void generateFormatCode() { | |
| keysSorted = features.keys.toList()..sort(); | |
| generateSection_CurrentVersion(); | |
| generateSection_KnownFeatures(); | |
| generateSection_FromSharedExperimentalFlag(); | |
| generateSection_EnableString(); | |
| generateSection_ExperimentalFeature(); | |
| generateSection_IsEnabledByDefault(); | |
| generateSection_IsExpired(); | |
| generateSection_CurrentState(); | |
| } | |
| void generateSection_CurrentState() { | |
| out.write(''' | |
| mixin _CurrentState { | |
| '''); | |
| for (var key in keysSorted) { | |
| var id = kebabCaseToSnakeCase(key); | |
| out.write(''' | |
| /// Current state for the flag "$key" | |
| bool get $id => isEnabled(ExperimentalFeatures.$id); | |
| '''); | |
| } | |
| out.write(''' | |
| bool isEnabled(covariant ExperimentalFeature feature); | |
| }'''); | |
| } | |
| void generateSection_CurrentVersion() { | |
| var version = _versionNumberAsString(experimentsYaml['current-version']); | |
| out.write(''' | |
| /// The current version of the Dart language (or, for non-stable releases, the | |
| /// version of the language currently in the process of being developed). | |
| const _currentVersion = '$version'; | |
| '''); | |
| } | |
| void generateSection_EnableString() { | |
| out.write(''' | |
| /// Constant strings for enabling each of the currently known experimental | |
| /// flags. | |
| class EnableString { | |
| '''); | |
| for (var key in keysSorted) { | |
| out.write(''' | |
| /// String to enable the experiment "$key" | |
| static const String ${kebabCaseToSnakeCase(key)} = '$key'; | |
| '''); | |
| } | |
| out.write(''' | |
| }'''); | |
| } | |
| void generateSection_ExperimentalFeature() { | |
| out.write(''' | |
| class ExperimentalFeatures { | |
| '''); | |
| int index = 0; | |
| for (var key in keysSorted) { | |
| var id = kebabCaseToSnakeCase(key); | |
| var help = (features[key] as YamlMap)['help'] ?? ''; | |
| 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( | |
| index: $index, | |
| enableString: EnableString.$id, | |
| isEnabledByDefault: IsEnabledByDefault.$id, | |
| isExpired: IsExpired.$id, | |
| documentation: '$help', | |
| '''); | |
| if (experimentalReleaseVersion != null) { | |
| experimentalReleaseVersion = _versionNumberAsString( | |
| experimentalReleaseVersion, | |
| ); | |
| out.write("experimentalReleaseVersion: "); | |
| out.write("Version.parse('$experimentalReleaseVersion'),"); | |
| } else { | |
| out.write("experimentalReleaseVersion: null,"); | |
| } | |
| if (enabledIn != null) { | |
| enabledIn = _versionNumberAsString(enabledIn); | |
| out.write("releaseVersion: Version.parse('$enabledIn'),"); | |
| } else { | |
| out.write("releaseVersion: null,"); | |
| } | |
| out.write("channels: $channelsLiteral,"); | |
| out.writeln(');'); | |
| ++index; | |
| } | |
| out.write(''' | |
| }'''); | |
| } | |
| void generateSection_FromSharedExperimentalFlag() { | |
| out.write(''' | |
| /// Returns the [Feature] corresponding to the shared experimental [flag]. | |
| Feature fromSharedExperimentalFlags(shared.ExperimentalFlag flag) => | |
| switch (flag) { | |
| '''); | |
| for (var key in keysSorted) { | |
| var snakeCase = kebabCaseToSnakeCase(key); | |
| var camelCase = kebabCaseToCamelCase(key); | |
| out.write(''' | |
| shared.ExperimentalFlag.$camelCase => ExperimentalFeatures.$snakeCase, | |
| '''); | |
| } | |
| out.write(''' | |
| }; | |
| '''); | |
| } | |
| void generateSection_IsEnabledByDefault() { | |
| out.write(''' | |
| /// Constant bools indicating whether each experimental flag is currently | |
| /// enabled by default. | |
| class IsEnabledByDefault { | |
| '''); | |
| for (var key in keysSorted) { | |
| var entry = features[key] as YamlMap; | |
| bool shipped = entry['enabledIn'] != null; | |
| out.write(''' | |
| /// Default state of the experiment "$key" | |
| static const bool ${kebabCaseToSnakeCase(key)} = $shipped; | |
| '''); | |
| } | |
| out.write(''' | |
| }'''); | |
| } | |
| void generateSection_IsExpired() { | |
| out.write(''' | |
| /// Constant bools indicating whether each experimental flag is currently | |
| /// expired (meaning its enable/disable status can no longer be altered from the | |
| /// value in [IsEnabledByDefault]). | |
| class IsExpired { | |
| '''); | |
| for (var key in keysSorted) { | |
| var entry = features[key] as YamlMap; | |
| bool shipped = entry['enabledIn'] != null; | |
| var expired = entry['expired'] as bool?; | |
| out.write(''' | |
| /// Expiration status of the experiment "$key" | |
| static const bool ${kebabCaseToSnakeCase(key)} = ${expired == true}; | |
| '''); | |
| if (shipped && expired == false) { | |
| throw 'Cannot mark shipped feature as "expired: false"'; | |
| } | |
| } | |
| out.write(''' | |
| }'''); | |
| } | |
| void generateSection_KnownFeatures() { | |
| out.write(''' | |
| /// A map containing information about all known experimental flags. | |
| final _knownFeatures = <String, ExperimentalFeature>{ | |
| '''); | |
| for (var key in keysSorted) { | |
| var id = kebabCaseToSnakeCase(key); | |
| out.write(''' | |
| EnableString.$id: ExperimentalFeatures.$id, | |
| '''); | |
| } | |
| out.write(''' | |
| }; | |
| '''); | |
| } | |
| String _versionNumberAsString(dynamic enabledIn) { | |
| if (enabledIn is double) { | |
| return '$enabledIn.0'; | |
| } else { | |
| return enabledIn.toString(); | |
| } | |
| } | |
| } |