Extend dartdoc options to begin to handle experiment flags (#1884)

* Add basic support for experiment flags

* dartfmt

* Add test for throw

* Synchronize analyzer and dartdoc experiment implementations

* dartfmt
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index 15d7a47..bb95737 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -20,6 +20,7 @@
 import 'package:analyzer/dart/element/element.dart';
 import 'package:args/args.dart';
 import 'package:dartdoc/dartdoc.dart';
+import 'package:dartdoc/src/experiment_options.dart';
 import 'package:dartdoc/src/io_utils.dart';
 import 'package:dartdoc/src/tool_runner.dart';
 import 'package:dartdoc/src/tuple.dart';
@@ -489,8 +490,8 @@
   /// and requires that one of [isDir] or [isFile] is set.
   final bool mustExist;
 
-  DartdocOption._(this.name, this.defaultsTo, this.help, this.isDir,
-      this.isFile, this.mustExist, this._convertYamlToType) {
+  DartdocOption(this.name, this.defaultsTo, this.help, this.isDir, this.isFile,
+      this.mustExist, this._convertYamlToType) {
     assert(!(isDir && isFile));
     if (isDir || isFile) assert(_isString || _isListString || _isMapString);
     if (mustExist) {
@@ -674,7 +675,7 @@
       bool isFile = false,
       bool parentDirOverridesChild,
       T Function(YamlMap, pathLib.Context) convertYamlToType})
-      : super._(name, null, help, isDir, isFile, mustExist, convertYamlToType) {
+      : super(name, null, help, isDir, isFile, mustExist, convertYamlToType) {
     _parentDirOverridesChild = parentDirOverridesChild;
   }
 
@@ -721,7 +722,7 @@
       bool isFile = false,
       bool negatable = false,
       bool splitCommas})
-      : super._(name, null, help, isDir, isFile, mustExist, null) {
+      : super(name, null, help, isDir, isFile, mustExist, null) {
     _hide = hide;
     _negatable = negatable;
     _splitCommas = splitCommas;
@@ -767,7 +768,7 @@
       String help = '',
       bool isDir = false,
       bool isFile = false})
-      : super._(name, null, help, isDir, isFile, mustExist, null);
+      : super(name, null, help, isDir, isFile, mustExist, null);
 }
 
 abstract class DartdocSyntheticOption<T> implements DartdocOption<T> {
@@ -801,7 +802,7 @@
 /// A [DartdocOption] that only contains other [DartdocOption]s and is not an option itself.
 class DartdocOptionSet extends DartdocOption<Null> {
   DartdocOptionSet(String name)
-      : super._(name, null, null, false, false, false, null);
+      : super(name, null, null, false, false, false, null);
 
   /// Asynchronous factory that is the main entry point to initialize Dartdoc
   /// options for use.
@@ -852,7 +853,7 @@
       bool isFile = false,
       bool negatable = false,
       bool splitCommas})
-      : super._(name, defaultsTo, help, isDir, isFile, mustExist, null) {
+      : super(name, defaultsTo, help, isDir, isFile, mustExist, null) {
     _hide = hide;
     _negatable = negatable;
     _splitCommas = splitCommas;
@@ -888,7 +889,7 @@
       bool negatable = false,
       bool parentDirOverridesChild: false,
       bool splitCommas})
-      : super._(name, defaultsTo, help, isDir, isFile, mustExist, null) {
+      : super(name, defaultsTo, help, isDir, isFile, mustExist, null) {
     _abbr = abbr;
     _hide = hide;
     _negatable = negatable;
@@ -938,7 +939,7 @@
       bool isFile = false,
       bool parentDirOverridesChild: false,
       T Function(YamlMap, pathLib.Context) convertYamlToType})
-      : super._(name, defaultsTo, help, isDir, isFile, mustExist,
+      : super(name, defaultsTo, help, isDir, isFile, mustExist,
             convertYamlToType) {
     _parentDirOverridesChild = parentDirOverridesChild;
   }
@@ -1258,12 +1259,22 @@
   }
 }
 
+/// All DartdocOptionContext mixins should implement this, as well as any other
+/// DartdocOptionContext mixins they use for calculating synthetic options.
+abstract class DartdocOptionContextBase {
+  DartdocOptionSet get optionSet;
+  Directory get context;
+}
+
 /// An [DartdocOptionSet] wrapped in nice accessors specific to Dartdoc, which
 /// automatically passes in the right directory for a given context.  Usually,
 /// a single [ModelElement], [Package], [Category] and so forth has a single context
 /// and so this can be made a member variable of those structures.
-class DartdocOptionContext {
+class DartdocOptionContext extends DartdocOptionContextBase
+    with DartdocExperimentOptionContext {
+  @override
   final DartdocOptionSet optionSet;
+  @override
   Directory context;
 
   // TODO(jcollins-g): Allow passing in structured data to initialize a
@@ -1561,5 +1572,7 @@
             'exist. Executables for different platforms are specified by '
             'giving the platform name as a key, and a list of strings as the '
             'command.'),
-  ];
+    // TODO(jcollins-g): refactor so there is a single static "create" for
+    // each DartdocOptionContext that traverses the inheritance tree itself.
+  ]..addAll(await createExperimentOptions());
 }
diff --git a/lib/src/experiment_options.dart b/lib/src/experiment_options.dart
new file mode 100644
index 0000000..0d9fa2f
--- /dev/null
+++ b/lib/src/experiment_options.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, 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.
+
+///
+/// Implementation of Dart language experiment option handling for dartdoc.
+/// See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md.
+///
+library dartdoc.experiment_options;
+
+import 'package:analyzer/src/dart/analysis/experiments.dart';
+import 'package:dartdoc/src/dartdoc_options.dart';
+
+abstract class DartdocExperimentOptionContext
+    implements DartdocOptionContextBase {
+  List<String> get enableExperiment =>
+      optionSet['enable-experiment'].valueAt(context);
+  ExperimentStatus get experimentStatus =>
+      optionSet['experimentStatus'].valueAt(context);
+}
+
+// TODO(jcollins-g): Implement YAML parsing for these flags and generation
+// of [DartdocExperimentOptionContext], once a YAML file is available.
+Future<List<DartdocOption>> createExperimentOptions() async {
+  return <DartdocOption>[
+    // TODO(jcollins-g): Consider loading experiment values from dartdoc_options.yaml?
+    new DartdocOptionArgOnly<List<String>>('enable-experiment', [],
+        help: 'Enable or disable listed experiments.\n' +
+            ExperimentStatus.knownFeatures.values
+                .where((e) => e.documentation != null)
+                .map((e) =>
+                    '    [no-]${e.enableString}: ${e.documentation} (default: ${e.isEnabledByDefault})')
+                .join('\n')),
+    new DartdocOptionSyntheticOnly<ExperimentStatus>(
+        'experimentStatus',
+        (option, dir) => new ExperimentStatus.fromStrings(
+            option.parent['enable-experiment'].valueAt(dir))),
+  ];
+}
diff --git a/lib/src/logging.dart b/lib/src/logging.dart
index 103be7e..c52b05f 100644
--- a/lib/src/logging.dart
+++ b/lib/src/logging.dart
@@ -111,7 +111,7 @@
   }
 }
 
-abstract class LoggingContext implements DartdocOptionContext {
+abstract class LoggingContext implements DartdocOptionContextBase {
   bool get json => optionSet['json'].valueAt(context);
   bool get showProgress => optionSet['showProgress'].valueAt(context);
 }
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 13bace5..b72ee5b 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -6458,6 +6458,9 @@
       AnalysisDriverScheduler scheduler = new AnalysisDriverScheduler(log);
       AnalysisOptionsImpl options = new AnalysisOptionsImpl();
 
+      // TODO(jcollins-g): pass in an ExperimentStatus instead?
+      options.enabledExperiments = config.enableExperiment;
+
       // TODO(jcollins-g): Make use of currently not existing API for managing
       //                   many AnalysisDrivers
       // TODO(jcollins-g): make use of DartProject isApi()
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index 0e405e8..ab02d48 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -154,9 +154,15 @@
 
       test('help prints command line args', () async {
         List<String> outputLines = [];
-        await subprocessLauncher.runStreamed(Platform.resolvedExecutable, [dartdocPath, '--help'], perLine: outputLines.add);
-        expect(outputLines, contains('Generate HTML documentation for Dart libraries.'));
-        expect(outputLines.join('\n'), contains(new RegExp('^-h, --help[ ]+Show command help.', multiLine: true))) ;
+        await subprocessLauncher.runStreamed(
+            Platform.resolvedExecutable, [dartdocPath, '--help'],
+            perLine: outputLines.add);
+        expect(outputLines,
+            contains('Generate HTML documentation for Dart libraries.'));
+        expect(
+            outputLines.join('\n'),
+            contains(new RegExp('^-h, --help[ ]+Show command help.',
+                multiLine: true)));
       });
 
       test('Validate missing FLUTTER_ROOT exception is clean', () async {
diff --git a/test/experiment_options_test.dart b/test/experiment_options_test.dart
new file mode 100644
index 0000000..6772bfd
--- /dev/null
+++ b/test/experiment_options_test.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2018, 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.
+
+/// Unit tests for lib/src/experiment_options.dart.
+library dartdoc.experiment_options_test;
+
+import 'dart:io';
+
+import 'package:dartdoc/src/dartdoc_options.dart';
+import 'package:dartdoc/src/experiment_options.dart';
+import 'package:path/path.dart' as pathLib;
+import 'package:test/test.dart';
+
+class DartdocExperimentOptionContextTester extends DartdocOptionContext {
+  DartdocExperimentOptionContextTester(
+      DartdocOptionSet optionSet, FileSystemEntity entity)
+      : super(optionSet, entity);
+}
+
+void main() {
+  DartdocOptionSet experimentOptions;
+  Directory tempDir;
+  File optionsFile;
+
+  setUp(() async {
+    experimentOptions = await DartdocOptionSet.fromOptionGenerators(
+        'dartdoc', [createExperimentOptions]);
+  });
+
+  setUpAll(() {
+    tempDir = Directory.systemTemp.createTempSync('experiment_options_test');
+    optionsFile = new File(pathLib.join(tempDir.path, 'dartdoc_options.yaml'))
+      ..createSync();
+    optionsFile.writeAsStringSync('''
+dartdoc:
+  enable-experiment:
+    - constant-update-2018
+    - fake-experiment
+    - no-fake-experiment-on
+''');
+  });
+
+  tearDownAll(() {
+    tempDir.deleteSync(recursive: true);
+  });
+
+  group('Experimental options test', () {
+    test('Defaults work for all options', () {
+      experimentOptions.parseArguments([]);
+      DartdocExperimentOptionContextTester tester =
+          new DartdocExperimentOptionContextTester(
+              experimentOptions, Directory.current);
+      expect(tester.experimentStatus.constant_update_2018, isFalse);
+      expect(tester.experimentStatus.set_literals, isFalse);
+    });
+
+    test('Overriding defaults works via args', () {
+      experimentOptions.parseArguments(
+          ['--enable-experiment', 'constant-update-2018,set-literals']);
+      DartdocExperimentOptionContextTester tester =
+          new DartdocExperimentOptionContextTester(
+              experimentOptions, Directory.current);
+      expect(tester.experimentStatus.constant_update_2018, isTrue);
+      expect(tester.experimentStatus.set_literals, isTrue);
+    });
+  });
+}