Refine detection for NNBD cases and tag generated pages with "Null safety" (#2221)

* Beginnings

* wow, this mostly works

* Very simple SDK NNBD detection

* Basic templates and tagging for nnbd interfaces

* dartfmt, update tests

* Fix duplicitous comment

* Update with review comments
diff --git a/lib/resources/styles.css b/lib/resources/styles.css
index 711fde7..8f81e1c 100644
--- a/lib/resources/styles.css
+++ b/lib/resources/styles.css
@@ -460,6 +460,22 @@
   vertical-align: middle;
 }
 
+.feature {
+  display: inline-block;
+  background: white;
+  border: 1px solid #0175c2;
+  border-radius: 20px;
+  color: #0175c2;
+
+  font-size: 12px;
+  padding: 1px 6px;
+  margin: 0 8px 0 0;
+}
+
+h1 .feature {
+  vertical-align: middle;
+}
+
 .source-link {
   padding: 18px 4px;
   vertical-align: middle;
diff --git a/lib/src/element_type.dart b/lib/src/element_type.dart
index 14728c2..fe684cb 100644
--- a/lib/src/element_type.dart
+++ b/lib/src/element_type.dart
@@ -74,8 +74,23 @@
 
   String get name;
 
+  /// Name with generics and nullability indication.
   String get nameWithGenerics;
 
+  /// Return a dartdoc nullability suffix for this type.
+  String get nullabilitySuffix {
+    if (library.isNNBD && !type.isVoid && !type.isBottom) {
+      /// If a legacy type appears inside the public interface of a
+      /// NNBD library, we pretend it is nullable for the purpose of
+      /// documentation (since star-types are not supposed to be public).
+      if (type.nullabilitySuffix == NullabilitySuffix.question ||
+          type.nullabilitySuffix == NullabilitySuffix.star) {
+        return '?';
+      }
+    }
+    return '';
+  }
+
   List<Parameter> get parameters => [];
 
   DartType get instantiatedType;
@@ -120,7 +135,7 @@
   }
 
   @override
-  String get nameWithGenerics => name;
+  String get nameWithGenerics => '$name${nullabilitySuffix}';
 
   /// Assume that undefined elements don't have useful bounds.
   @override
diff --git a/lib/src/generator/templates.dart b/lib/src/generator/templates.dart
index 31e5049..ec90e16 100644
--- a/lib/src/generator/templates.dart
+++ b/lib/src/generator/templates.dart
@@ -22,6 +22,7 @@
   'class',
   'constant',
   'extension',
+  'feature_set',
   'footer',
   'head',
   'library',
@@ -55,6 +56,8 @@
   'documentation',
   'extension',
   'features',
+  'feature_set',
+  'footer',
   'footer',
   'head',
   'library',
diff --git a/lib/src/model/feature_set.dart b/lib/src/model/feature_set.dart
new file mode 100644
index 0000000..acfeb1d
--- /dev/null
+++ b/lib/src/model/feature_set.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2020, 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:dartdoc/src/model/language_feature.dart';
+import 'package:dartdoc/src/model/model.dart';
+
+/// [ModelElement]s can have different language features that can alter
+/// the user interpretation of the interface.
+mixin FeatureSet {
+  PackageGraph get packageGraph;
+  Library get library;
+
+  /// A list of language features that both apply to this [ModelElement] and
+  /// make sense to display in context.
+  Iterable<LanguageFeature> get displayedLanguageFeatures sync* {
+    // TODO(jcollins-g): Implement mixed-mode handling and the tagging of
+    // legacy interfaces.
+    if (isNNBD) {
+      yield LanguageFeature(
+          'Null safety', packageGraph.rendererFactory.featureRenderer);
+    }
+  }
+
+  bool get hasFeatureSet => displayedLanguageFeatures.isNotEmpty;
+
+  // TODO(jcollins-g): This is an approximation and not strictly true for
+  // inheritance/reexports.
+  bool get isNNBD => library.isNNBD;
+}
diff --git a/lib/src/model/language_feature.dart b/lib/src/model/language_feature.dart
new file mode 100644
index 0000000..0c3036f
--- /dev/null
+++ b/lib/src/model/language_feature.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, 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:dartdoc/src/render/feature_renderer.dart';
+
+const Map<String, String> _featureDescriptions = {
+  'Null safety': 'Supports the null safety language feature.',
+};
+
+/// An abstraction for a language feature; used to render tags to notify
+/// the user that the documentation should be specially interpreted.
+class LanguageFeature {
+  String get featureDescription => _featureDescriptions[name];
+  String get featureLabel => _featureRenderer.renderFeatureLabel(this);
+
+  final String name;
+
+  final FeatureRenderer _featureRenderer;
+
+  LanguageFeature(this.name, this._featureRenderer) {
+    assert(_featureDescriptions.containsKey(name));
+  }
+}
diff --git a/lib/src/model/library.dart b/lib/src/model/library.dart
index 83d8357..407726b 100644
--- a/lib/src/model/library.dart
+++ b/lib/src/model/library.dart
@@ -107,6 +107,19 @@
 
   List<String> _allOriginalModelElementNames;
 
+  /// Return true if this library is in a package configured to be treated as
+  /// as non-nullable by default and is itself NNBD.
+  // TODO(jcollins-g): packageMeta.allowsNNBD may be redundant after package
+  // allow list support is published in analyzer
+  bool get _allowsNNBD =>
+      element.isNonNullableByDefault && packageMeta.allowsNNBD;
+
+  /// Return true if this library should be documented as non-nullable.
+  /// A library may be NNBD but not documented that way.
+  @override
+  bool get isNNBD =>
+      config.enableExperiment.contains('non-nullable') && _allowsNNBD;
+
   bool get isInSdk => element.isInSdk;
 
   final Package _package;
diff --git a/lib/src/model/model_element.dart b/lib/src/model/model_element.dart
index 3d2a9fa..fd27a53 100644
--- a/lib/src/model/model_element.dart
+++ b/lib/src/model/model_element.dart
@@ -21,6 +21,7 @@
 import 'package:dartdoc/src/dartdoc_options.dart';
 import 'package:dartdoc/src/element_type.dart';
 import 'package:dartdoc/src/logging.dart';
+import 'package:dartdoc/src/model/feature_set.dart';
 import 'package:dartdoc/src/model/model.dart';
 import 'package:dartdoc/src/model_utils.dart' as utils;
 import 'package:dartdoc/src/render/model_element_renderer.dart';
@@ -148,7 +149,14 @@
 /// ModelElement will reference itself as part of the "wrong" [Library]
 /// from the public interface perspective.
 abstract class ModelElement extends Canonicalization
-    with Privacy, Warnable, Locatable, Nameable, SourceCodeMixin, Indexable
+    with
+        Privacy,
+        Warnable,
+        Locatable,
+        Nameable,
+        SourceCodeMixin,
+        Indexable,
+        FeatureSet
     implements Comparable, Documentable {
   final Element _element;
 
diff --git a/lib/src/package_meta.dart b/lib/src/package_meta.dart
index a03a2c2..cbddb32 100644
--- a/lib/src/package_meta.dart
+++ b/lib/src/package_meta.dart
@@ -10,6 +10,7 @@
 import 'package:analyzer/dart/element/element.dart';
 import 'package:dartdoc/dartdoc.dart';
 import 'package:path/path.dart' as path;
+import 'package:pub_semver/pub_semver.dart';
 import 'package:yaml/yaml.dart';
 
 import 'logging.dart';
@@ -106,6 +107,14 @@
 
   String get homepage;
 
+  /// If true, libraries in this package can be be read as non-nullable by
+  /// default.  Whether they will be will be documented that way will depend
+  /// on the experiment flag.
+  ///
+  /// A package property, as this depends in part on the pubspec version
+  /// constraint and/or the package allow list.
+  bool get allowsNNBD;
+
   FileContents getReadmeContents();
 
   FileContents getLicenseContents();
@@ -272,6 +281,7 @@
   FileContents _license;
   FileContents _changelog;
   Map _pubspec;
+  Map _analysisOptions;
 
   _FilePackageMeta(Directory dir) : super(dir) {
     var f = File(path.join(dir.path, 'pubspec.yaml'));
@@ -280,6 +290,12 @@
     } else {
       _pubspec = {};
     }
+    f = File(path.join(dir.path, 'analysis_options.yaml'));
+    if (f.existsSync()) {
+      _analysisOptions = loadYaml(f.readAsStringSync());
+    } else {
+      _analysisOptions = {};
+    }
   }
 
   bool _setHostedAt = false;
@@ -360,6 +376,29 @@
   @override
   String get homepage => _pubspec['homepage'];
 
+  /// This is a magic range that triggers detection of [allowsNNBD].
+  static final _nullableRange =
+      VersionConstraint.parse('>=2.9.0-dev.0 <2.10.0') as VersionRange;
+
+  @override
+  bool get allowsNNBD {
+    // TODO(jcollins-g): override/add to with allow list once that exists
+    var sdkConstraint;
+    if (_pubspec?.containsKey('sdk') ?? false) {
+      // TODO(jcollins-g): VersionConstraint.parse returns [VersionRange]s right
+      // now, but the interface doesn't guarantee that.
+      sdkConstraint = VersionConstraint.parse(_pubspec['sdk']) as VersionRange;
+    }
+    if (sdkConstraint == _nullableRange &&
+        (_analysisOptions['analyzer']?.containsKey('enable-experiment') ??
+            false) &&
+        _analysisOptions['analyzer']['enable-experiment']
+            .contains('non-nullable')) {
+      return true;
+    }
+    return false;
+  }
+
   @override
   bool get requiresFlutter =>
       _pubspec['environment']?.containsKey('flutter') == true ||
@@ -423,6 +462,14 @@
     sdkReadmePath = path.join(dir.path, 'lib', 'api_readme.md');
   }
 
+  /// 2.9.0-9.0.dev is the first unforked SDK, and therefore the first version
+  /// of the SDK it makes sense to allow NNBD documentation for.
+  static final _sdkNullableRange = VersionConstraint.parse('>=2.9.0-9.0.dev');
+
+  @override
+  // TODO(jcollins-g): There should be a better way to determine this.
+  bool get allowsNNBD => _sdkNullableRange.allows(Version.parse(version));
+
   @override
   String get hostedAt => null;
 
diff --git a/lib/src/render/element_type_renderer.dart b/lib/src/render/element_type_renderer.dart
index 9ec41d4..f88e3d8 100644
--- a/lib/src/render/element_type_renderer.dart
+++ b/lib/src/render/element_type_renderer.dart
@@ -9,6 +9,13 @@
   String renderLinkedName(T elementType);
 
   String renderNameWithGenerics(T elementType) => '';
+
+  String wrapNullabilityParens(T elementType, String inner) =>
+      elementType.nullabilitySuffix.isEmpty
+          ? inner
+          : '($inner${elementType.nullabilitySuffix})';
+  String wrapNullability(T elementType, String inner) =>
+      '$inner${elementType.nullabilitySuffix}';
 }
 
 // Html implementations
@@ -24,7 +31,7 @@
     buf.write(
         ParameterRendererHtml().renderLinkedParams(elementType.parameters));
     buf.write(')</span>');
-    return buf.toString();
+    return wrapNullabilityParens(elementType, buf.toString());
   }
 
   @override
@@ -39,7 +46,7 @@
         buf.write('</span>&gt;');
       }
     }
-    return buf.toString();
+    return wrapNullability(elementType, buf.toString());
   }
 }
 
@@ -58,7 +65,7 @@
       buf.write('</span>&gt;');
       buf.write('</span>');
     }
-    return buf.toString();
+    return wrapNullability(elementType, buf.toString());
   }
 
   @override
@@ -72,7 +79,8 @@
           '</span>, <span class="type-parameter">');
       buf.write('</span>&gt;');
     }
-    return buf.toString();
+    buf.write(elementType.nullabilitySuffix);
+    return wrapNullability(elementType, buf.toString());
   }
 }
 
@@ -92,7 +100,7 @@
         .trim());
     buf.write(') → ');
     buf.write(elementType.returnType.linkedName);
-    return buf.toString();
+    return wrapNullabilityParens(elementType, buf.toString());
   }
 }
 
@@ -108,7 +116,7 @@
     buf.write('(');
     buf.write(ParameterRendererMd().renderLinkedParams(elementType.parameters));
     buf.write(')');
-    return buf.toString();
+    return wrapNullabilityParens(elementType, buf.toString());
   }
 
   @override
@@ -122,7 +130,7 @@
         buf.write('>');
       }
     }
-    return buf.toString();
+    return wrapNullabilityParens(elementType, buf.toString());
   }
 }
 
@@ -138,7 +146,7 @@
       buf.writeAll(elementType.typeArguments.map((t) => t.linkedName), ', ');
       buf.write('>');
     }
-    return buf.toString();
+    return wrapNullability(elementType, buf.toString());
   }
 
   @override
@@ -152,7 +160,8 @@
           elementType.typeArguments.map((t) => t.nameWithGenerics), ', ');
       buf.write('>');
     }
-    return buf.toString();
+    buf.write(elementType.nullabilitySuffix);
+    return wrapNullability(elementType, buf.toString());
   }
 }
 
@@ -172,6 +181,6 @@
         .trim());
     buf.write(') → ');
     buf.write(elementType.returnType.linkedName);
-    return buf.toString();
+    return wrapNullabilityParens(elementType, buf.toString());
   }
 }
diff --git a/lib/src/render/feature_renderer.dart b/lib/src/render/feature_renderer.dart
new file mode 100644
index 0000000..5cdf718
--- /dev/null
+++ b/lib/src/render/feature_renderer.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2020, 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:dartdoc/src/model/language_feature.dart';
+
+abstract class FeatureRenderer {
+  String renderFeatureLabel(LanguageFeature feature);
+}
+
+class FeatureRendererHtml extends FeatureRenderer {
+  static final FeatureRendererHtml _instance = FeatureRendererHtml._();
+  factory FeatureRendererHtml() {
+    return _instance;
+  }
+
+  FeatureRendererHtml._();
+
+  @override
+  String renderFeatureLabel(LanguageFeature feature) {
+    var spanClasses = <String>[];
+    spanClasses.add('feature');
+    spanClasses
+        .add('feature-${feature.name.split(' ').join('-').toLowerCase()}');
+
+    var buf = StringBuffer();
+    buf.write(
+        '<span class="${spanClasses.join(' ')}" title="${feature.featureDescription}">${feature.name}</span>');
+    return buf.toString();
+  }
+}
+
+class FeatureRendererMd extends FeatureRenderer {
+  static final FeatureRendererMd _instance = FeatureRendererMd._();
+  factory FeatureRendererMd() {
+    return _instance;
+  }
+
+  FeatureRendererMd._();
+  @override
+  String renderFeatureLabel(LanguageFeature feature) {
+    return '*\<${feature.name}\>*';
+  }
+}
diff --git a/lib/src/render/renderer_factory.dart b/lib/src/render/renderer_factory.dart
index eaead50..9f13aec 100644
--- a/lib/src/render/renderer_factory.dart
+++ b/lib/src/render/renderer_factory.dart
@@ -7,6 +7,7 @@
 import 'package:dartdoc/src/render/documentation_renderer.dart';
 import 'package:dartdoc/src/render/element_type_renderer.dart';
 import 'package:dartdoc/src/render/enum_field_renderer.dart';
+import 'package:dartdoc/src/render/feature_renderer.dart';
 import 'package:dartdoc/src/render/model_element_renderer.dart';
 import 'package:dartdoc/src/render/parameter_renderer.dart';
 import 'package:dartdoc/src/render/template_renderer.dart';
@@ -31,6 +32,8 @@
 
   DocumentationRenderer get documentationRenderer;
 
+  FeatureRenderer get featureRenderer;
+
   ElementTypeRenderer<FunctionTypeElementType>
       get functionTypeElementTypeRenderer;
 
@@ -96,6 +99,9 @@
 
   @override
   TypedefRenderer get typedefRenderer => TypedefRendererHtml();
+
+  @override
+  FeatureRenderer get featureRenderer => FeatureRendererHtml();
 }
 
 class MdRenderFactory extends RendererFactory {
@@ -143,4 +149,7 @@
 
   @override
   TypedefRenderer get typedefRenderer => TypedefRendererMd();
+
+  @override
+  FeatureRenderer get featureRenderer => FeatureRendererMd();
 }
diff --git a/lib/templates/html/_feature_set.html b/lib/templates/html/_feature_set.html
new file mode 100644
index 0000000..006a8c6
--- /dev/null
+++ b/lib/templates/html/_feature_set.html
@@ -0,0 +1,5 @@
+{{#hasFeatureSet}}
+  {{#displayedLanguageFeatures}}
+    {{{featureLabel}}}
+  {{/displayedLanguageFeatures}}
+{{/hasFeatureSet}}
\ No newline at end of file
diff --git a/lib/templates/html/class.html b/lib/templates/html/class.html
index 2fd0090..e0d9981 100644
--- a/lib/templates/html/class.html
+++ b/lib/templates/html/class.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-class">{{{nameWithGenerics}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-class">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#clazz}}
diff --git a/lib/templates/html/constant.html b/lib/templates/html/constant.html
index 5248e7d..d3c68ca 100644
--- a/lib/templates/html/constant.html
+++ b/lib/templates/html/constant.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-constant">{{{name}}}</span> {{kind}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-constant">{{{name}}}</span> {{kind}} {{>feature_set}}</h1></div>
     {{/self}}
 
     <section class="multi-line-signature">
diff --git a/lib/templates/html/constructor.html b/lib/templates/html/constructor.html
index 3fc1a9f..4c6a3b6 100644
--- a/lib/templates/html/constructor.html
+++ b/lib/templates/html/constructor.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-constructor">{{{nameWithGenerics}}}</span> {{kind}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-constructor">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}}</h1></div>
     {{/self}}
 
     {{#constructor}}
diff --git a/lib/templates/html/enum.html b/lib/templates/html/enum.html
index f42561c..16dfbd8 100644
--- a/lib/templates/html/enum.html
+++ b/lib/templates/html/enum.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-enum">{{{name}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-enum">{{{name}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#eNum}}
diff --git a/lib/templates/html/extension.html b/lib/templates/html/extension.html
index 7434dbf..8942e9a 100644
--- a/lib/templates/html/extension.html
+++ b/lib/templates/html/extension.html
@@ -8,7 +8,7 @@
 
 <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-    <div>{{>source_link}}<h1><span class="kind-class">{{{nameWithGenerics}}}</span> {{kind}} {{>categorization}}</h1></div>
+    <div>{{>source_link}}<h1><span class="kind-class">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#extension}}
diff --git a/lib/templates/html/function.html b/lib/templates/html/function.html
index a8d9c85..4995929 100644
--- a/lib/templates/html/function.html
+++ b/lib/templates/html/function.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-function">{{{nameWithGenerics}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-function">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#function}}
diff --git a/lib/templates/html/library.html b/lib/templates/html/library.html
index 6e08376..074f158 100644
--- a/lib/templates/html/library.html
+++ b/lib/templates/html/library.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-library">{{{name}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-library">{{{name}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#library}}
diff --git a/lib/templates/html/method.html b/lib/templates/html/method.html
index d88bc73..f0995f2 100644
--- a/lib/templates/html/method.html
+++ b/lib/templates/html/method.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-method">{{{nameWithGenerics}}}</span> {{kind}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-method">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}}</h1></div>
     {{/self}}
 
     {{#method}}
diff --git a/lib/templates/html/mixin.html b/lib/templates/html/mixin.html
index d7500f5..6864710 100644
--- a/lib/templates/html/mixin.html
+++ b/lib/templates/html/mixin.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-mixin">{{{nameWithGenerics}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-mixin">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     {{#mixin}}
diff --git a/lib/templates/html/property.html b/lib/templates/html/property.html
index 1372a37..e15ba48 100644
--- a/lib/templates/html/property.html
+++ b/lib/templates/html/property.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-property">{{name}}</span> {{kind}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-property">{{name}}</span> {{kind}} {{>feature_set}}</h1></div>
     {{/self}}
 
     {{#self}}
diff --git a/lib/templates/html/top_level_constant.html b/lib/templates/html/top_level_constant.html
index ccd3f66..73d5618 100644
--- a/lib/templates/html/top_level_constant.html
+++ b/lib/templates/html/top_level_constant.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-top-level-constant">{{{name}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-top-level-constant">{{{name}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
 
       <section class="multi-line-signature">
         {{>name_summary}}
diff --git a/lib/templates/html/top_level_property.html b/lib/templates/html/top_level_property.html
index 00bbf31..367f932 100644
--- a/lib/templates/html/top_level_property.html
+++ b/lib/templates/html/top_level_property.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-top-level-property">{{{name}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-top-level-property">{{{name}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
 
       {{#hasNoGetterSetter}}
         <section class="multi-line-signature">
diff --git a/lib/templates/html/typedef.html b/lib/templates/html/typedef.html
index 2fcbe00..31318e3 100644
--- a/lib/templates/html/typedef.html
+++ b/lib/templates/html/typedef.html
@@ -8,7 +8,7 @@
 
   <div id="dartdoc-main-content" class="col-xs-12 col-sm-9 col-md-8 main-content">
     {{#self}}
-      <div>{{>source_link}}<h1><span class="kind-typedef">{{{nameWithGenerics}}}</span> {{kind}} {{>categorization}}</h1></div>
+      <div>{{>source_link}}<h1><span class="kind-typedef">{{{nameWithGenerics}}}</span> {{kind}} {{>feature_set}} {{>categorization}}</h1></div>
     {{/self}}
 
     <section class="multi-line-signature">
diff --git a/lib/templates/md/_feature_set.md b/lib/templates/md/_feature_set.md
new file mode 100644
index 0000000..006a8c6
--- /dev/null
+++ b/lib/templates/md/_feature_set.md
@@ -0,0 +1,5 @@
+{{#hasFeatureSet}}
+  {{#displayedLanguageFeatures}}
+    {{{featureLabel}}}
+  {{/displayedLanguageFeatures}}
+{{/hasFeatureSet}}
\ No newline at end of file
diff --git a/lib/templates/md/class.md b/lib/templates/md/class.md
index e98847f..5976aab 100644
--- a/lib/templates/md/class.md
+++ b/lib/templates/md/class.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#clazz}}
diff --git a/lib/templates/md/constant.md b/lib/templates/md/constant.md
index 3f5abcb..984f7e7 100644
--- a/lib/templates/md/constant.md
+++ b/lib/templates/md/constant.md
@@ -4,6 +4,7 @@
 # {{{name}}} {{kind}}
 
 {{>source_link}}
+{{>feature_set}}
 {{/self}}
 
 {{#property}}
diff --git a/lib/templates/md/constructor.md b/lib/templates/md/constructor.md
index ce89771..8e67ab4 100644
--- a/lib/templates/md/constructor.md
+++ b/lib/templates/md/constructor.md
@@ -4,6 +4,7 @@
 # {{{nameWithGenerics}}} {{kind}}
 
 {{>source_link}}
+{{>feature_set}}
 {{/self}}
 
 {{#constructor}}
diff --git a/lib/templates/md/enum.md b/lib/templates/md/enum.md
index ce48fa3..d1a14bb 100644
--- a/lib/templates/md/enum.md
+++ b/lib/templates/md/enum.md
@@ -4,7 +4,7 @@
 # {{{name}}} {{kind}}
 
 {{>source_link}}
-{{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#eNum}}
diff --git a/lib/templates/md/extension.md b/lib/templates/md/extension.md
index be62c54..f31b5c2 100644
--- a/lib/templates/md/extension.md
+++ b/lib/templates/md/extension.md
@@ -7,6 +7,7 @@
 {{>source_link}}
 
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#extension}}
diff --git a/lib/templates/md/function.md b/lib/templates/md/function.md
index 63193ff..7937404 100644
--- a/lib/templates/md/function.md
+++ b/lib/templates/md/function.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#function}}
diff --git a/lib/templates/md/library.md b/lib/templates/md/library.md
index f338b40..a4811ab 100644
--- a/lib/templates/md/library.md
+++ b/lib/templates/md/library.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#library}}
diff --git a/lib/templates/md/method.md b/lib/templates/md/method.md
index 0d03ab4..e3e7f19 100644
--- a/lib/templates/md/method.md
+++ b/lib/templates/md/method.md
@@ -4,6 +4,7 @@
 # {{{nameWithGenerics}}} {{kind}}
 
 {{>source_link}}
+{{>feature_set}}
 {{/self}}
 
 {{#method}}
diff --git a/lib/templates/md/mixin.md b/lib/templates/md/mixin.md
index e81cdbc..b78510f 100644
--- a/lib/templates/md/mixin.md
+++ b/lib/templates/md/mixin.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#mixin}}
diff --git a/lib/templates/md/property.md b/lib/templates/md/property.md
index 351e51e..222c5e2 100644
--- a/lib/templates/md/property.md
+++ b/lib/templates/md/property.md
@@ -4,6 +4,7 @@
 # {{name}} {{kind}}
 
 {{>source_link}}
+{{>feature_set}}
 {{/self}}
 
 {{#self}}
diff --git a/lib/templates/md/top_level_constant.md b/lib/templates/md/top_level_constant.md
index 1f79709..d89d2a6 100644
--- a/lib/templates/md/top_level_constant.md
+++ b/lib/templates/md/top_level_constant.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 
 {{>name_summary}} = {{{ constantValue }}}  {{!two spaces intentional}}
 {{>features}}
diff --git a/lib/templates/md/top_level_property.md b/lib/templates/md/top_level_property.md
index 3a06ef7..43a5ffc 100644
--- a/lib/templates/md/top_level_property.md
+++ b/lib/templates/md/top_level_property.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 
 {{#hasNoGetterSetter}}
 {{{ linkedReturnType }}} {{>name_summary}}  {{!two spaces intentional}}
diff --git a/lib/templates/md/typedef.md b/lib/templates/md/typedef.md
index 3636e88..bed210a 100644
--- a/lib/templates/md/typedef.md
+++ b/lib/templates/md/typedef.md
@@ -5,6 +5,7 @@
 
 {{>source_link}}
 {{>categorization}}
+{{>feature_set}}
 {{/self}}
 
 {{#typeDef}}
diff --git a/test/model_special_cases_test.dart b/test/model_special_cases_test.dart
index 71fa09d..56387d3 100644
--- a/test/model_special_cases_test.dart
+++ b/test/model_special_cases_test.dart
@@ -35,12 +35,14 @@
   // if we haven't figured out how to switch on NNBD outside of `dev` builds
   // as specified in #2148.
   final _nnbdExperimentAllowed =
-      VersionRange(min: Version.parse('2.9.0'), includeMin: true);
+      VersionRange(min: Version.parse('2.9.0-9.0.dev'), includeMin: true);
 
   // Experimental features not yet enabled by default.  Move tests out of this block
   // when the feature is enabled by default.
   group('Experiments', () {
-    Library lateFinalWithoutInitializer, nnbdClassMemberDeclarations;
+    Library lateFinalWithoutInitializer,
+        nnbdClassMemberDeclarations,
+        optOutOfNnbd;
     Class b;
 
     setUpAll(() async {
@@ -50,10 +52,18 @@
       nnbdClassMemberDeclarations = (await utils.testPackageGraphExperiments)
           .libraries
           .firstWhere((lib) => lib.name == 'nnbd_class_member_declarations');
+      optOutOfNnbd = (await utils.testPackageGraphExperiments)
+          .libraries
+          .firstWhere((lib) => lib.name == 'opt_out_of_nnbd');
       b = nnbdClassMemberDeclarations.allClasses
           .firstWhere((c) => c.name == 'B');
     });
 
+    test('isNNBD is set correctly for libraries', () {
+      expect(lateFinalWithoutInitializer.isNNBD, isTrue);
+      expect(optOutOfNnbd.isNNBD, isFalse);
+    });
+
     test('method parameters with required', () {
       var m1 = b.instanceMethods.firstWhere((m) => m.name == 'm1');
       var p1 = m1.allParameters.firstWhere((p) => p.name == 'p1');
diff --git a/testing/test_package_experiments/lib/nnbd_class_member_declarations.dart b/testing/test_package_experiments/lib/nnbd_class_member_declarations.dart
index 6250e5b..47e989c 100644
--- a/testing/test_package_experiments/lib/nnbd_class_member_declarations.dart
+++ b/testing/test_package_experiments/lib/nnbd_class_member_declarations.dart
@@ -15,3 +15,13 @@
   m2(int sometimes, we, [String have, double optionals]);
 }
 
+/// Test nullable parameters, factories, members
+abstract class C {
+  factory C.factory1(int? param, {Object? param2}) => null;
+
+  int? testField;
+
+  List<int?> get testFieldNullableParameter;
+
+  List<Map<String, num?>>? method1();
+}
diff --git a/testing/test_package_experiments/lib/nullable_elements.dart b/testing/test_package_experiments/lib/nullable_elements.dart
new file mode 100644
index 0000000..054330d
--- /dev/null
+++ b/testing/test_package_experiments/lib/nullable_elements.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2020, 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.
+
+library nullable_elements;
+
+int? publicNullable;
+
+String? get nullableGetter => null;
+
+String? _nullableSetter;
+
+void set nullableSetter(String? value) {
+  _nullableSetter = value;
+}
+
+void some(int? nullable, String? parameters) {}
+
+class NullableMembers {
+  final Map<String, Map>? initialized;
+
+  /// Nullable constructor parameters.
+  NullableMembers(this.initialized);
+
+  Iterable<BigInt>? nullableField;
+
+  operator *(NullableMembers? nullableOther) => this;
+
+  int? methodWithNullables(String? foo) => foo?.length;
+}
\ No newline at end of file
diff --git a/testing/test_package_experiments/lib/opt_out_of_nnbd.dart b/testing/test_package_experiments/lib/opt_out_of_nnbd.dart
new file mode 100644
index 0000000..0b9584a
--- /dev/null
+++ b/testing/test_package_experiments/lib/opt_out_of_nnbd.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2020, 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.
+
+// @dart=2.8
+
+library opt_out_of_nnbd;
+
+String notOptedIn = false ? 'hi' : null;
diff --git a/testing/test_package_experiments/pubspec.yaml b/testing/test_package_experiments/pubspec.yaml
index bda7ea2..37a98a5 100644
--- a/testing/test_package_experiments/pubspec.yaml
+++ b/testing/test_package_experiments/pubspec.yaml
@@ -1,4 +1,4 @@
 name: test_package_experiments
 version: 0.0.1
-sdk: '>=2.9.0 <2.10.0'
+sdk: '>=2.9.0-dev.0 <2.10.0'
 description: Experimental flags are tested here.
diff --git a/tool/grind.dart b/tool/grind.dart
index 4fde62e..b1ca818 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -241,7 +241,10 @@
     // Passing parameters to dartfmt for directories to search results in
     // filenames being stripped of the dirname so we have to filter here.
     void addFileToFix(String fileName) {
-      if (path.split(fileName).first == 'testing') return;
+      var pathComponents = path.split(fileName);
+      if (pathComponents.isNotEmpty && pathComponents.first == 'testing') {
+        return;
+      }
       filesToFix.add(fileName);
     }