Performance improvements for documentation generation (#1837)

* Split out documentation comment computation and make it cached

* Precache local documentation only if it is possible a macro template is defined there

* Directory.current is expensive, also get rid of excessive split/joins for strings

* Minor tweaks

* dartfmt

* Stable doesn't like Future<void>

* Fix import for stable
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index d205153..e523843 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -32,6 +32,12 @@
 const double _kDoubleVal = 0.0;
 const bool _kBoolVal = true;
 
+/// Args are computed relative to the current directory at the time the
+/// program starts.
+final Directory directoryCurrent = Directory.current;
+final String directoryCurrentPath =
+    pathLib.canonicalize(Directory.current.path);
+
 String resolveTildePath(String originalPath) {
   if (originalPath == null || !originalPath.startsWith('~/')) {
     return originalPath;
@@ -585,8 +591,8 @@
   /// corresponding files or directories.
   T valueAt(Directory dir);
 
-  /// Calls [valueAt] with the current working directory.
-  T valueAtCurrent() => valueAt(Directory.current);
+  /// Calls [valueAt] with the working directory at the start of the program.
+  T valueAtCurrent() => valueAt(directoryCurrent);
 
   /// Calls [valueAt] on the directory this element is defined in.
   T valueAtElement(Element element) => valueAt(new Directory(
@@ -945,7 +951,7 @@
     return _valueAtFromFiles(dir) ?? defaultsTo;
   }
 
-  Map<String, T> __valueAtFromFiles = new Map();
+  final Map<String, T> __valueAtFromFiles = new Map();
   // The value of this option from files will not change unless files are
   // modified during execution (not allowed in Dartdoc).
   T _valueAtFromFiles(Directory dir) {
@@ -1050,8 +1056,8 @@
   _YamlFileData _yamlAtDirectory(Directory dir) {
     List<String> canonicalPaths = [pathLib.canonicalize(dir.path)];
     if (!_yamlAtCanonicalPathCache.containsKey(canonicalPaths.first)) {
-      _YamlFileData yamlData = new _YamlFileData(
-          new Map(), pathLib.canonicalize(Directory.current.path));
+      _YamlFileData yamlData =
+          new _YamlFileData(new Map(), directoryCurrentPath);
       if (dir.existsSync()) {
         File dartdocOptionsFile;
 
@@ -1126,7 +1132,7 @@
   }
 
   /// Generates an _OptionValueWithContext using the value of the argument from
-  /// the [argParser] and the working directory from [Directory.current].
+  /// the [argParser] and the working directory from [directoryCurrent].
   ///
   /// Throws [UnsupportedError] if [T] is not a supported type.
   _OptionValueWithContext _valueAtFromArgsWithContext() {
@@ -1157,7 +1163,7 @@
     } else {
       throw UnsupportedError('Type ${T} is not supported');
     }
-    return new _OptionValueWithContext(retval, Directory.current.path);
+    return new _OptionValueWithContext(retval, directoryCurrentPath);
   }
 
   /// The name of this option as a command line argument.
@@ -1234,8 +1240,8 @@
   /// the inputDir flag to determine the context.
   DartdocOptionContext(this.optionSet, FileSystemEntity entity) {
     if (entity == null) {
-      String inputDir = optionSet['inputDir'].valueAt(Directory.current) ??
-          Directory.current.path;
+      String inputDir = optionSet['inputDir'].valueAt(directoryCurrent) ??
+          directoryCurrentPath;
       context = new Directory(inputDir);
     } else {
       context = new Directory(pathLib
@@ -1404,7 +1410,7 @@
     new DartdocOptionArgOnly<bool>('injectHtml', false,
         help: 'Allow the use of the {@inject-html} directive to inject raw '
             'HTML into dartdoc output.'),
-    new DartdocOptionArgOnly<String>('input', Directory.current.path,
+    new DartdocOptionArgOnly<String>('input', directoryCurrentPath,
         isDir: true, help: 'Path to source directory', mustExist: true),
     new DartdocOptionSyntheticOnly<String>('inputDir',
         (DartdocSyntheticOption<String> option, Directory dir) {
diff --git a/lib/src/markdown_processor.dart b/lib/src/markdown_processor.dart
index d8a8cac..eb3eda1 100644
--- a/lib/src/markdown_processor.dart
+++ b/lib/src/markdown_processor.dart
@@ -198,8 +198,9 @@
   // Try expensive not-scoped lookup.
   if (refModelElement == null && element is ModelElement) {
     Class preferredClass = _getPreferredClass(element);
-    refModelElement =
-        new _MarkdownCommentReference(codeRef, element, commentRefs, preferredClass).computeReferredElement();
+    refModelElement = new _MarkdownCommentReference(
+            codeRef, element, commentRefs, preferredClass)
+        .computeReferredElement();
   }
 
   // Did not find it anywhere.
@@ -269,32 +270,40 @@
   return null;
 }
 
-
 /// Represents a single comment reference.
 class _MarkdownCommentReference {
   /// The code reference text.
   final String codeRef;
+
   /// The element containing the code reference.
   final Warnable element;
+
   /// A list of [CommentReference]s from the analyzer.
   final List<CommentReference> commentRefs;
+
   /// Disambiguate inheritance with this class.
   final Class preferredClass;
+
   /// Current results.  Input/output of all _find and _reduce methods.
   Set<ModelElement> results;
+
   /// codeRef with any leading constructor string, stripped.
   String codeRefChomped;
+
   /// Library associated with this element.
   Library library;
+
   /// PackageGraph associated with this element.
   PackageGraph packageGraph;
 
-  _MarkdownCommentReference(this.codeRef, this.element, this.commentRefs, this.preferredClass) {
+  _MarkdownCommentReference(
+      this.codeRef, this.element, this.commentRefs, this.preferredClass) {
     assert(element != null);
     assert(element.packageGraph.allLibrariesAdded);
 
     codeRefChomped = codeRef.replaceFirst(isConstructor, '');
-    library = element is ModelElement ? (element as ModelElement).library : null;
+    library =
+        element is ModelElement ? (element as ModelElement).library : null;
     packageGraph = library.packageGraph;
   }
 
@@ -334,7 +343,8 @@
       // This could conceivably be a reference to an enum member.  They don't show up in allModelElements.
       _findEnumReferences,
       // Use the analyzer to resolve a comment reference.
-      _findAnalyzerReferences]) {
+      _findAnalyzerReferences
+    ]) {
       findMethod();
       // Remove any "null" objects after each step of trying to add to results.
       // TODO(jcollins-g): Eliminate all situations where nulls can be added
@@ -382,13 +392,20 @@
       if (!results.every((r) => r is Parameter)) {
         element.warn(PackageWarning.ambiguousDocReference,
             message:
-            "[$codeRef] => ${results.map((r) => "'${r.fullyQualifiedName}'").join(", ")}");
+                "[$codeRef] => ${results.map((r) => "'${r.fullyQualifiedName}'").join(", ")}");
       }
       result = results.first;
     }
     return result;
   }
 
+  List<String> _codeRefParts;
+  List<String> get codeRefParts => _codeRefParts ??= codeRef.split('.');
+
+  List<String> _codeRefChompedParts;
+  List<String> get codeRefChompedParts =>
+      _codeRefChompedParts ??= codeRefChomped.split('.');
+
   /// Returns true if this is a constructor we should consider due to its
   /// name and the code reference, or if this isn't a constructor.  False
   /// otherwise.
@@ -397,7 +414,6 @@
     if (modelElement is! Constructor) return true;
     if (codeRef.contains(isConstructor)) return true;
     Constructor aConstructor = modelElement;
-    List<String> codeRefParts = codeRef.split('.');
     if (codeRefParts.length > 1) {
       // Pick the last two parts, in case a specific library was part of the
       // codeRef.
@@ -434,18 +450,20 @@
 
   void _reducePreferLibrariesInLocalImportExportGraph() {
     if (results.any(
-            (r) => library.packageImportedExportedLibraries.contains(r.library))) {
+        (r) => library.packageImportedExportedLibraries.contains(r.library))) {
       results.removeWhere(
-              (r) => !library.packageImportedExportedLibraries.contains(r.library));
+          (r) => !library.packageImportedExportedLibraries.contains(r.library));
     }
   }
 
   void _reducePreferResultsAccessibleInSameLibrary() {
     // TODO(jcollins-g): we could have saved ourselves some work by using the analyzer
     //                   to search the namespace, somehow.  Do that instead.
-    if (element is ModelElement && results.any((r) => r.element.isAccessibleIn((element as ModelElement).library.element))) {
-      results.removeWhere(
-              (r) => !r.element.isAccessibleIn((element as ModelElement).library.element));
+    if (element is ModelElement &&
+        results.any((r) => r.element
+            .isAccessibleIn((element as ModelElement).library.element))) {
+      results.removeWhere((r) =>
+          !r.element.isAccessibleIn((element as ModelElement).library.element));
     }
   }
 
@@ -464,14 +482,14 @@
   void _findTypeParameters() {
     if (element is TypeParameters) {
       results.addAll((element as TypeParameters).typeParameters.where((p) =>
-      p.name == codeRefChomped || codeRefChomped.startsWith("${p.name}.")));
+          p.name == codeRefChomped || codeRefChomped.startsWith("${p.name}.")));
     }
   }
 
   void _findParameters() {
     if (element is ModelElement) {
       results.addAll((element as ModelElement).allParameters.where((p) =>
-      p.name == codeRefChomped || codeRefChomped.startsWith("${p.name}.")));
+          p.name == codeRefChomped || codeRefChomped.startsWith("${p.name}.")));
     }
   }
 
@@ -479,7 +497,8 @@
     if (codeRef.contains(leadingIgnoreStuff)) {
       String newCodeRef = codeRef.replaceFirst(leadingIgnoreStuff, '');
       results.add(new _MarkdownCommentReference(
-          newCodeRef, element, commentRefs, preferredClass).computeReferredElement());
+              newCodeRef, element, commentRefs, preferredClass)
+          .computeReferredElement());
     }
   }
 
@@ -487,7 +506,8 @@
     if (codeRef.contains(trailingIgnoreStuff)) {
       String newCodeRef = codeRef.replaceFirst(trailingIgnoreStuff, '');
       results.add(new _MarkdownCommentReference(
-          newCodeRef, element, commentRefs, preferredClass).computeReferredElement());
+              newCodeRef, element, commentRefs, preferredClass)
+          .computeReferredElement());
     }
   }
 
@@ -495,14 +515,14 @@
     if (codeRef.startsWith(operatorPrefix)) {
       String newCodeRef = codeRef.replaceFirst(operatorPrefix, '');
       results.add(new _MarkdownCommentReference(
-          newCodeRef, element, commentRefs, preferredClass).computeReferredElement());
+              newCodeRef, element, commentRefs, preferredClass)
+          .computeReferredElement());
     }
   }
 
   void _findEnumReferences() {
     // TODO(jcollins-g): Put enum members in allModelElements with useful hrefs without blowing up other assumptions about what that means.
     // TODO(jcollins-g): This doesn't provide good warnings if an enum and class have the same name in different libraries in the same package.  Fix that.
-    List<String> codeRefChompedParts = codeRefChomped.split('.');
     if (codeRefChompedParts.length >= 2) {
       String maybeEnumName = codeRefChompedParts
           .sublist(0, codeRefChompedParts.length - 1)
@@ -510,7 +530,7 @@
       String maybeEnumMember = codeRefChompedParts.last;
       if (packageGraph.findRefElementCache.containsKey(maybeEnumName)) {
         for (final modelElement
-        in packageGraph.findRefElementCache[maybeEnumName]) {
+            in packageGraph.findRefElementCache[maybeEnumName]) {
           if (modelElement is Enum) {
             if (modelElement.constants.any((e) => e.name == maybeEnumMember)) {
               results.add(modelElement);
@@ -525,7 +545,7 @@
   void _findGlobalWithinRefElementCache() {
     if (packageGraph.findRefElementCache.containsKey(codeRefChomped)) {
       for (final modelElement
-      in packageGraph.findRefElementCache[codeRefChomped]) {
+          in packageGraph.findRefElementCache[codeRefChomped]) {
         if (codeRefChomped == modelElement.fullyQualifiedNameWithoutLibrary ||
             (modelElement is Library &&
                 codeRefChomped == modelElement.fullyQualifiedName)) {
@@ -552,10 +572,10 @@
     // We now need the ref element cache to keep from repeatedly searching [Package.allModelElements].
     // But if not, look for a fully qualified match.  (That only makes sense
     // if the codeRef might be qualified, and contains periods.)
-    if (
-    codeRefChomped.contains('.') &&
+    if (codeRefChomped.contains('.') &&
         packageGraph.findRefElementCache.containsKey(codeRefChomped)) {
-      for (final ModelElement modelElement in packageGraph.findRefElementCache[codeRefChomped]) {
+      for (final ModelElement modelElement
+          in packageGraph.findRefElementCache[codeRefChomped]) {
         if (!_ConsiderIfConstructor(modelElement)) continue;
         // For fully qualified matches, the original preferredClass passed
         // might make no sense.  Instead, use the enclosing class from the
@@ -576,10 +596,12 @@
     List<Class> tryClasses = [preferredClass];
     Class realClass = tryClasses.first;
     if (element is Inheritable) {
-      Inheritable overriddenElement = (element as Inheritable).overriddenElement;
+      Inheritable overriddenElement =
+          (element as Inheritable).overriddenElement;
       while (overriddenElement != null) {
         tryClasses.add(
-            ((element as Inheritable).overriddenElement as EnclosedElement).enclosingElement);
+            ((element as Inheritable).overriddenElement as EnclosedElement)
+                .enclosingElement);
         overriddenElement = overriddenElement.overriddenElement;
       }
     }
@@ -594,7 +616,7 @@
 
     if (results.isEmpty && realClass != null) {
       for (Class superClass
-      in realClass.publicSuperChain.map((et) => et.element as Class)) {
+          in realClass.publicSuperChain.map((et) => et.element as Class)) {
         if (!tryClasses.contains(superClass)) {
           _getResultsForClass(superClass);
         }
@@ -613,7 +635,8 @@
       if (refModelElement is Accessor) {
         refModelElement = (refModelElement as Accessor).enclosingCombo;
       }
-      refModelElement = refModelElement.canonicalModelElement ?? refModelElement;
+      refModelElement =
+          refModelElement.canonicalModelElement ?? refModelElement;
       results.add(refModelElement);
     }
   }
@@ -626,13 +649,14 @@
     if ((tryClass.modelType.typeArguments.map((e) => e.name))
         .contains(codeRefChomped)) {
       results.add((tryClass.modelType.typeArguments.firstWhere(
-              (e) => e.name == codeRefChomped && e is DefinedElementType)
-      as DefinedElementType)
+                  (e) => e.name == codeRefChomped && e is DefinedElementType)
+              as DefinedElementType)
           .element);
     } else {
       // People like to use 'this' in docrefs too.
       if (codeRef == 'this') {
-        results.add(packageGraph.findCanonicalModelElementFor(tryClass.element));
+        results
+            .add(packageGraph.findCanonicalModelElementFor(tryClass.element));
       } else {
         // TODO(jcollins-g): get rid of reimplementation of identifier resolution
         //                   or integrate into ModelElement in a simpler way.
@@ -643,7 +667,6 @@
         // TODO(jcollins-g): This makes our caller ~O(n^2) vs length of superChain.
         //                   Fortunately superChains are short, but optimize this if it matters.
         superChain.addAll(tryClass.superChain.map((t) => t.element as Class));
-        List<String> codeRefParts = codeRefChomped.split('.');
         for (final c in superChain) {
           // TODO(jcollins-g): add a hash-map-enabled lookup function to Class?
           for (final modelElement in c.allModelElements) {
@@ -667,7 +690,8 @@
             // TODO(jcollins-g): Fix partial qualifications in _findRefElementInLibrary so it can tell
             // when it is referenced from a non-documented element?
             // TODO(jcollins-g): We could probably check this early.
-            if (codeRefParts.first == c.name && codeRefParts.last == namePart) {
+            if (codeRefChompedParts.first == c.name &&
+                codeRefChompedParts.last == namePart) {
               results.add(packageGraph.findCanonicalModelElementFor(
                   modelElement.element,
                   preferredClass: tryClass));
@@ -675,9 +699,10 @@
             }
             if (modelElement is Constructor) {
               // Constructor names don't include the class, so we might miss them in the above search.
-              if (codeRefParts.length > 1) {
-                String codeRefClass = codeRefParts[codeRefParts.length - 2];
-                String codeRefConstructor = codeRefParts.last;
+              if (codeRefChompedParts.length > 1) {
+                String codeRefClass =
+                    codeRefChompedParts[codeRefChompedParts.length - 2];
+                String codeRefConstructor = codeRefChompedParts.last;
                 if (codeRefClass == c.name &&
                     codeRefConstructor ==
                         modelElement.fullyQualifiedName.split('.').last) {
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 29bef4c..b8b7b63 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -99,6 +99,43 @@
 final RegExp locationSplitter = new RegExp(r'(package:|[\\/;.])');
 final RegExp substituteNameVersion = new RegExp(r'%([bnv])%');
 
+/// This doc may need to be processed in case it has a template or html
+/// fragment.
+final needsPrecacheRegExp = new RegExp(r'{@(template|tool|inject-html)');
+
+final templateRegExp = new RegExp(
+    r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?',
+    multiLine: true);
+final htmlRegExp = new RegExp(
+    r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?',
+    multiLine: true);
+final htmlInjectRegExp =
+    new RegExp(r'<dartdoc-html>([a-f0-9]+)</dartdoc-html>');
+
+// Matches all tool directives (even some invalid ones). This is so
+// we can give good error messages if the directive is malformed, instead of
+// just silently emitting it as-is.
+final basicToolRegExp = new RegExp(
+    r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?',
+    multiLine: true);
+
+/// Regexp to take care of splitting arguments, and handling the quotes
+/// around arguments, if any.
+///
+/// Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
+/// Match group 2 contains the quote character used (which is discarded).
+/// Match group 3 is a quoted arg, if any, without the quotes.
+/// Match group 4 is the unquoted arg, if any.
+final RegExp argMatcher = new RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
+    r'(?:' // Start a new non-capture group for the two possibilities.
+    r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
+    r'([^ ]+))'); // without quotes.
+
+final categoryRegexp = new RegExp(
+    r'[ ]*{@(api|category|subCategory|image|samples) (.+?)}[ ]*\n?',
+    multiLine: true);
+final macroRegExp = new RegExp(r'{@macro\s+([^}]+)}');
+
 /// Mixin for subclasses of ModelElement representing Elements that can be
 /// inherited from one class to another.
 ///
@@ -414,7 +451,7 @@
   }
 
   @override
-  String get computeDocumentationComment {
+  String _computeDocumentationComment() {
     if (isSynthetic) {
       String docComment =
           (element as PropertyAccessorElement).variable.documentationComment;
@@ -427,14 +464,13 @@
                   docComment.contains('@nodoc'))) ||
           (isSetter &&
               enclosingCombo.hasGetter &&
-              enclosingCombo.getter.computeDocumentationComment !=
-                  docComment)) {
+              enclosingCombo.getter.documentationComment != docComment)) {
         return stripComments(docComment);
       } else {
         return '';
       }
     }
-    return stripComments(super.computeDocumentationComment);
+    return stripComments(super._computeDocumentationComment());
   }
 
   @override
@@ -1334,9 +1370,7 @@
     Set<String> _categorySet = new Set();
     Set<String> _subCategorySet = new Set();
     _hasCategorization = false;
-    final categoryRegexp = new RegExp(
-        r'[ ]*{@(api|category|subCategory|image|samples) (.+?)}[ ]*\n?',
-        multiLine: true);
+
     rawDocs = rawDocs.replaceAllMapped(categoryRegexp, (match) {
       _hasCategorization = true;
       switch (match[1]) {
@@ -1768,7 +1802,7 @@
   }
 
   @override
-  String get computeDocumentationComment {
+  String _computeDocumentationComment() {
     String docs = getterSetterDocumentationComment;
     if (docs.isEmpty) return _field.documentationComment;
     return docs;
@@ -1942,8 +1976,7 @@
       // doesn't yield the real elements for GetterSetterCombos.
       if (!config.dropTextFrom
           .contains(getter.documentationFrom.first.element.library.name)) {
-        String docs =
-            getter.documentationFrom.first.computeDocumentationComment;
+        String docs = getter.documentationFrom.first.documentationComment;
         if (docs != null) buffer.write(docs);
       }
     }
@@ -1952,8 +1985,7 @@
       assert(setter.documentationFrom.length == 1);
       if (!config.dropTextFrom
           .contains(setter.documentationFrom.first.element.library.name)) {
-        String docs =
-            setter.documentationFrom.first.computeDocumentationComment;
+        String docs = setter.documentationFrom.first.documentationComment;
         if (docs != null) {
           if (buffer.isNotEmpty) buffer.write('\n\n');
           buffer.write(docs);
@@ -2534,14 +2566,17 @@
   }
 
   Map<String, Set<ModelElement>> _modelElementsNameMap;
+
   /// Map of [fullyQualifiedNameWithoutLibrary] to all matching [ModelElement]s
   /// in this library.  Used for code reference lookups.
   Map<String, Set<ModelElement>> get modelElementsNameMap {
     if (_modelElementsNameMap == null) {
       _modelElementsNameMap = new Map<String, Set<ModelElement>>();
       allModelElements.forEach((ModelElement modelElement) {
-        _modelElementsNameMap.putIfAbsent(modelElement.fullyQualifiedNameWithoutLibrary, () => new Set());
-        _modelElementsNameMap[modelElement.fullyQualifiedNameWithoutLibrary].add(modelElement);
+        _modelElementsNameMap.putIfAbsent(
+            modelElement.fullyQualifiedNameWithoutLibrary, () => new Set());
+        _modelElementsNameMap[modelElement.fullyQualifiedNameWithoutLibrary]
+            .add(modelElement);
       });
     }
     return _modelElementsNameMap;
@@ -3112,7 +3147,7 @@
           !(enclosingElement as Class).isPublic) {
         _isPublic = false;
       } else {
-        String docComment = computeDocumentationComment;
+        String docComment = documentationComment;
         if (docComment == null) {
           _isPublic = hasPublicName(element);
         } else {
@@ -3226,7 +3261,7 @@
   List<ModelElement> get computeDocumentationFrom {
     List<ModelElement> docFrom;
 
-    if (computeDocumentationComment == null &&
+    if (documentationComment == null &&
         canOverride() &&
         this is Inheritable &&
         (this as Inheritable).overriddenElement != null) {
@@ -3252,7 +3287,7 @@
     if (config.dropTextFrom.contains(element.library.name)) {
       _rawDocs = '';
     } else {
-      _rawDocs = computeDocumentationComment ?? '';
+      _rawDocs = documentationComment ?? '';
       _rawDocs = stripComments(_rawDocs) ?? '';
       // Must evaluate tools first, in case they insert any other directives.
       _rawDocs = _evaluateTools(_rawDocs);
@@ -3679,7 +3714,24 @@
         extendedDebug: extendedDebug);
   }
 
-  String get computeDocumentationComment => element.documentationComment;
+  String _computeDocumentationComment() => element.documentationComment;
+
+  bool _documentationCommentComputed = false;
+  String _documentationComment;
+  String get documentationComment {
+    if (_documentationCommentComputed == false) {
+      _documentationComment = _computeDocumentationComment();
+      _documentationCommentComputed = true;
+    }
+    return _documentationComment;
+  }
+
+  /// Call this method to precache docs for this object if it might possibly
+  /// have a macro template or a tool definition.
+  void precacheLocalDocsIfNeeded() {
+    if (documentationComment != null &&
+        needsPrecacheRegExp.hasMatch(documentationComment)) documentationLocal;
+  }
 
   Documentation get _documentation {
     if (__documentation != null) return __documentation;
@@ -3963,13 +4015,6 @@
   /// ## Content to send to tool.
   /// 2018-09-18T21:15+00:00
   String _evaluateTools(String rawDocs) {
-    // Matches all tool directives (even some invalid ones). This is so
-    // we can give good error messages if the directive is malformed, instead of
-    // just silently emitting it as-is.
-    final basicToolRegExp = new RegExp(
-        r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?',
-        multiLine: true);
-
     var runner = new ToolRunner(config.tools, (String message) {
       warn(PackageWarning.toolError, message: message);
     });
@@ -4208,8 +4253,8 @@
   /// but just injected verbatim.
   String _injectHtmlFragments(String rawDocs) {
     if (!config.injectHtml) return rawDocs;
-    final macroRegExp = new RegExp(r'<dartdoc-html>([a-f0-9]+)</dartdoc-html>');
-    return rawDocs.replaceAllMapped(macroRegExp, (match) {
+
+    return rawDocs.replaceAllMapped(htmlInjectRegExp, (match) {
       String fragment = packageGraph.getHtmlFragment(match[1]);
       if (fragment == null) {
         warn(PackageWarning.unknownHtmlFragment, message: match[1]);
@@ -4245,7 +4290,6 @@
   ///     More comments
   ///
   String _injectMacros(String rawDocs) {
-    final macroRegExp = new RegExp(r'{@macro\s+([^}]+)}');
     return rawDocs.replaceAllMapped(macroRegExp, (match) {
       String macro = packageGraph.getMacro(match[1]);
       if (macro == null) {
@@ -4265,9 +4309,6 @@
   ///     &#123;@endtemplate&#125;
   ///
   String _stripMacroTemplatesAndAddToIndex(String rawDocs) {
-    final templateRegExp = new RegExp(
-        r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?',
-        multiLine: true);
     return rawDocs.replaceAllMapped(templateRegExp, (match) {
       packageGraph._addMacro(match[1].trim(), match[2].trim());
       return "{@macro ${match[1].trim()}}";
@@ -4287,10 +4328,7 @@
   ///
   String _stripHtmlAndAddToIndex(String rawDocs) {
     if (!config.injectHtml) return rawDocs;
-    final templateRegExp = new RegExp(
-        r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?',
-        multiLine: true);
-    return rawDocs.replaceAllMapped(templateRegExp, (match) {
+    return rawDocs.replaceAllMapped(htmlRegExp, (match) {
       String fragment = match[1];
       String digest = sha1.convert(fragment.codeUnits).toString();
       packageGraph._addHtmlFragment(digest, fragment);
@@ -4313,19 +4351,7 @@
   /// value.
   Iterable<String> _splitUpQuotedArgs(String argsAsString,
       {bool convertToArgs = false}) {
-    // Regexp to take care of splitting arguments, and handling the quotes
-    // around arguments, if any.
-    //
-    // Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
-    // Match group 2 contains the quote character used (which is discarded).
-    // Match group 3 is a quoted arg, if any, without the quotes.
-    // Match group 4 is the unquoted arg, if any.
-    final RegExp argMatcher = new RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
-        r'(?:' // Start a new non-capture group for the two possibilities.
-        r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
-        r'([^ ]+))'); // without quotes.
     final Iterable<Match> matches = argMatcher.allMatches(argsAsString);
-
     // Remove quotes around args, and if convertToArgs is true, then for any
     // args that look like assignments (start with valid option names followed
     // by an equals sign), add a "--" in front so that they parse as options.
@@ -4634,7 +4660,7 @@
     specialClasses = new SpecialClasses();
     // Go through docs of every ModelElement in package to pre-build the macros
     // index.
-    allModelElements.forEach((m) => m.documentationLocal);
+    allModelElements.forEach((m) => m.precacheLocalDocsIfNeeded());
     _localDocumentationBuilt = true;
 
     // Scan all model elements to insure that interceptor and other special
@@ -6297,7 +6323,7 @@
   Set<String> get features => super.features..addAll(comboFeatures);
 
   @override
-  String get computeDocumentationComment {
+  String _computeDocumentationComment() {
     String docs = getterSetterDocumentationComment;
     if (docs.isEmpty) return _variable.documentationComment;
     return docs;
diff --git a/lib/src/special_elements.dart b/lib/src/special_elements.dart
index d599bc0..b2617a0 100644
--- a/lib/src/special_elements.dart
+++ b/lib/src/special_elements.dart
@@ -68,8 +68,8 @@
 final Map<String, _SpecialClassDefinition> _specialClassDefinitions = {
   'Object': new _SpecialClassDefinition(
       SpecialClass.object, 'Object', 'dart.core', 'dart:core'),
-  'Interceptor': new _SpecialClassDefinition(SpecialClass.interceptor, 'Interceptor',
-      '_interceptors', 'dart:_interceptors',
+  'Interceptor': new _SpecialClassDefinition(SpecialClass.interceptor,
+      'Interceptor', '_interceptors', 'dart:_interceptors',
       required: false),
   'pragma': new _SpecialClassDefinition(
       SpecialClass.pragma, 'pragma', 'dart.core', 'dart:core',
diff --git a/lib/src/tool_runner.dart b/lib/src/tool_runner.dart
index 9653e16..aa79d4f 100644
--- a/lib/src/tool_runner.dart
+++ b/lib/src/tool_runner.dart
@@ -4,6 +4,7 @@
 
 library dartdoc.tool_runner;
 
+import 'dart:async';
 import 'dart:io';
 
 import 'package:path/path.dart' as pathLib;
@@ -53,8 +54,14 @@
   ///
   /// This will remove any temporary files created by the tool runner.
   void dispose() {
-    if (_temporaryDirectory != null && temporaryDirectory.existsSync())
-      temporaryDirectory.deleteSync(recursive: true);
+    if (_temporaryDirectory != null) disposeAsync(_temporaryDirectory);
+  }
+
+  /// Avoid blocking on I/O for cleanups.
+  static Future<void> disposeAsync(Directory temporaryDirectory) async {
+    temporaryDirectory.exists().then((bool exists) {
+      if (exists) return temporaryDirectory.delete(recursive: true);
+    });
   }
 
   void _runSetup(
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 3c53226..d732e50 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -5,8 +5,7 @@
 
 final RegExp leadingWhiteSpace = new RegExp(r'^([ \t]*)[^ ]');
 
-String stripCommonWhitespace(String str) {
-  StringBuffer buf = new StringBuffer();
+Iterable<String> stripCommonWhitespace(String str) sync* {
   List<String> lines = str.split('\n');
   int minimumSeen;
 
@@ -21,18 +20,13 @@
     }
   }
   minimumSeen ??= 0;
-  int lineno = 1;
   for (String line in lines) {
     if (line.length >= minimumSeen) {
-      buf.write('${line.substring(minimumSeen)}\n');
+      yield '${line.substring(minimumSeen)}';
     } else {
-      if (lineno < lines.length) {
-        buf.write('\n');
-      }
+      yield '';
     }
-    ++lineno;
   }
-  return buf.toString();
 }
 
 String stripComments(String str) {
@@ -41,8 +35,7 @@
   StringBuffer buf = new StringBuffer();
 
   if (str.startsWith('///')) {
-    str = stripCommonWhitespace(str);
-    for (String line in str.split('\n')) {
+    for (String line in stripCommonWhitespace(str)) {
       if (line.startsWith('/// ')) {
         buf.write('${line.substring(4)}\n');
       } else if (line.startsWith('///')) {
@@ -59,8 +52,7 @@
     if (str.endsWith('*/')) {
       str = str.substring(0, str.length - 2);
     }
-    str = stripCommonWhitespace(str);
-    for (String line in str.split('\n')) {
+    for (String line in stripCommonWhitespace(str)) {
       if (cStyle && line.startsWith('* ')) {
         buf.write('${line.substring(2)}\n');
       } else if (cStyle && line.startsWith('*')) {
diff --git a/test/compare_output_test.dart b/test/compare_output_test.dart
index d3572fd..82f93b3 100644
--- a/test/compare_output_test.dart
+++ b/test/compare_output_test.dart
@@ -67,7 +67,9 @@
               'Top level package requires Flutter but FLUTTER_ROOT environment variable not set|test_package_flutter_plugin requires the Flutter SDK, version solving failed')));
       expect(result.stderr, isNot(contains('asynchronous gap')));
       expect(result.exitCode, isNot(0));
-    }, skip: true /* TODO(gspencer): Re-enable as soon as Flutter's config is sane again. */ );
+    },
+        skip:
+            true /* TODO(gspencer): Re-enable as soon as Flutter's config is sane again. */);
 
     test("Validate --version works", () async {
       var args = <String>[dartdocBin, '--version'];
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index 1832de9..910254e 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -17,9 +17,10 @@
 
 import 'src/utils.dart';
 
-class DartdocLoggingOptionContext extends DartdocGeneratorOptionContext with LoggingContext {
+class DartdocLoggingOptionContext extends DartdocGeneratorOptionContext
+    with LoggingContext {
   DartdocLoggingOptionContext(DartdocOptionSet optionSet, Directory dir)
-    : super(optionSet, dir);
+      : super(optionSet, dir);
 }
 
 void main() {
@@ -29,13 +30,17 @@
     setUpAll(() async {
       tempDir = Directory.systemTemp.createTempSync('dartdoc.test.');
       outputParam = ['--output', tempDir.path];
-      DartdocOptionSet optionSet = await DartdocOptionSet.fromOptionGenerators('dartdoc', [createLoggingOptions]);
+      DartdocOptionSet optionSet = await DartdocOptionSet.fromOptionGenerators(
+          'dartdoc', [createLoggingOptions]);
       optionSet.parseArguments([]);
-      startLogging(new DartdocLoggingOptionContext(optionSet, Directory.current));
+      startLogging(
+          new DartdocLoggingOptionContext(optionSet, Directory.current));
     });
 
     tearDown(() async {
-      tempDir.listSync().forEach((FileSystemEntity f) {f.deleteSync(recursive: true);});
+      tempDir.listSync().forEach((FileSystemEntity f) {
+        f.deleteSync(recursive: true);
+      });
     });
 
     Future<Dartdoc> buildDartdoc(
@@ -68,7 +73,8 @@
       });
 
       test('examplePathPrefix', () async {
-        Class UseAnExampleHere = p.allCanonicalModelElements.whereType<Class>()
+        Class UseAnExampleHere = p.allCanonicalModelElements
+            .whereType<Class>()
             .firstWhere((ModelElement c) => c.name == 'UseAnExampleHere');
         expect(
             UseAnExampleHere.documentationAsHtml,
@@ -77,7 +83,8 @@
       });
 
       test('includeExternal and showUndocumentedCategories', () async {
-        Class Something = p.allCanonicalModelElements.whereType<Class>()
+        Class Something = p.allCanonicalModelElements
+            .whereType<Class>()
             .firstWhere((ModelElement c) => c.name == 'Something');
         expect(Something.isPublic, isTrue);
         expect(Something.displayedCategories, isNotEmpty);
@@ -91,9 +98,9 @@
       Iterable<String> unresolvedToolErrors = p
           .packageWarningCounter.countedWarnings.values
           .expand<String>((Set<Tuple2<PackageWarning, String>> s) => s
-          .where((Tuple2<PackageWarning, String> t) =>
-      t.item1 == PackageWarning.toolError)
-          .map<String>((Tuple2<PackageWarning, String> t) => t.item2));
+              .where((Tuple2<PackageWarning, String> t) =>
+                  t.item1 == PackageWarning.toolError)
+              .map<String>((Tuple2<PackageWarning, String> t) => t.item2));
 
       expect(p.packageWarningCounter.errorCount, equals(1));
       expect(unresolvedToolErrors.length, equals(1));
diff --git a/test/model_test.dart b/test/model_test.dart
index 38d0cc8..3745e51 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -148,9 +148,13 @@
     });
     test('can invoke a tool multiple times in one comment block', () {
       RegExp envLine = RegExp(r'^Env: \{', multiLine: true);
-      expect(envLine.allMatches(invokeToolMultipleSections.documentation).length, equals(2));
+      expect(
+          envLine.allMatches(invokeToolMultipleSections.documentation).length,
+          equals(2));
       RegExp argLine = RegExp(r'^Args: \[', multiLine: true);
-      expect(argLine.allMatches(invokeToolMultipleSections.documentation).length, equals(2));
+      expect(
+          argLine.allMatches(invokeToolMultipleSections.documentation).length,
+          equals(2));
       expect(invokeToolMultipleSections.documentation,
           contains('Invokes more than one tool in the same comment block.'));
       expect(invokeToolMultipleSections.documentation,
@@ -204,11 +208,15 @@
     });
     test("can inject HTML from tool", () {
       RegExp envLine = RegExp(r'^Env: \{', multiLine: true);
-      expect(envLine.allMatches(injectHtmlFromTool.documentation).length, equals(2));
+      expect(envLine.allMatches(injectHtmlFromTool.documentation).length,
+          equals(2));
       RegExp argLine = RegExp(r'^Args: \[', multiLine: true);
-      expect(argLine.allMatches(injectHtmlFromTool.documentation).length, equals(2));
-      expect(injectHtmlFromTool.documentation,
-          contains('Invokes more than one tool in the same comment block, and injects HTML.'));
+      expect(argLine.allMatches(injectHtmlFromTool.documentation).length,
+          equals(2));
+      expect(
+          injectHtmlFromTool.documentation,
+          contains(
+              'Invokes more than one tool in the same comment block, and injects HTML.'));
       expect(injectHtmlFromTool.documentationAsHtml,
           contains('<div class="title">Title</div>'));
       expect(injectHtmlFromTool.documentationAsHtml,
@@ -746,12 +754,15 @@
           .firstWhere((m) => m.name == 'withPrivateMacro');
       withUndefinedMacro = dog.allInstanceMethods
           .firstWhere((m) => m.name == 'withUndefinedMacro');
-      MacrosFromAccessors = fakeLibrary.enums.firstWhere((e) => e.name == 'MacrosFromAccessors');
-      macroReferencedHere = MacrosFromAccessors.publicConstants.firstWhere((e) => e.name == 'macroReferencedHere');
+      MacrosFromAccessors =
+          fakeLibrary.enums.firstWhere((e) => e.name == 'MacrosFromAccessors');
+      macroReferencedHere = MacrosFromAccessors.publicConstants
+          .firstWhere((e) => e.name == 'macroReferencedHere');
     });
 
     test("renders a macro defined within a enum", () {
-      expect(macroReferencedHere.documentationAsHtml, contains('This is a macro defined in an Enum accessor.'));
+      expect(macroReferencedHere.documentationAsHtml,
+          contains('This is a macro defined in an Enum accessor.'));
     });
 
     test("renders a macro within the same comment where it's defined", () {
@@ -790,21 +801,29 @@
 
     setUpAll(() {
       documentationErrors = errorLibrary.classes
-          .firstWhere((c) => c.name == 'DocumentationErrors');
+          .firstWhere((c) => c.name == 'DocumentationErrors')
+            ..documentation;
       withInvalidNamedAnimation = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withInvalidNamedAnimation');
+          .firstWhere((m) => m.name == 'withInvalidNamedAnimation')
+            ..documentation;
       withAnimationNonUnique = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationNonUnique');
+          .firstWhere((m) => m.name == 'withAnimationNonUnique')
+            ..documentation;
       withAnimationNonUniqueDeprecated = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationNonUniqueDeprecated');
+          .firstWhere((m) => m.name == 'withAnimationNonUniqueDeprecated')
+            ..documentation;
       withAnimationWrongParams = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationWrongParams');
+          .firstWhere((m) => m.name == 'withAnimationWrongParams')
+            ..documentation;
       withAnimationBadWidth = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationBadWidth');
+          .firstWhere((m) => m.name == 'withAnimationBadWidth')
+            ..documentation;
       withAnimationBadHeight = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationBadHeight');
+          .firstWhere((m) => m.name == 'withAnimationBadHeight')
+            ..documentation;
       withAnimationUnknownArg = documentationErrors.allInstanceMethods
-          .firstWhere((m) => m.name == 'withAnimationUnknownArg');
+          .firstWhere((m) => m.name == 'withAnimationUnknownArg')
+            ..documentation;
     });
 
     test("warns with invalidly-named animation within the method documentation",
@@ -1412,7 +1431,8 @@
     });
 
     test(('Verify mixin member is available in findRefElementCache'), () {
-      expect(packageGraph.findRefElementCache['GenericMixin.mixinMember'], isNotEmpty);
+      expect(packageGraph.findRefElementCache['GenericMixin.mixinMember'],
+          isNotEmpty);
     });
 
     test(('Verify inheritance/mixin structure and type inference'), () {
@@ -2482,8 +2502,8 @@
     });
 
     test('Docs from inherited implicit accessors are preserved', () {
-      expect(explicitGetterImplicitSetter.setter.computeDocumentationComment,
-          isNot(''));
+      expect(
+          explicitGetterImplicitSetter.setter.documentationComment, isNot(''));
     });
 
     test('@nodoc on simple property works', () {
@@ -2506,7 +2526,7 @@
         () {
       expect(documentedPartialFieldInSubclassOnly.isPublic, isTrue);
       expect(documentedPartialFieldInSubclassOnly.readOnly, isTrue);
-      expect(documentedPartialFieldInSubclassOnly.computeDocumentationComment,
+      expect(documentedPartialFieldInSubclassOnly.documentationComment,
           contains('This getter is documented'));
       expect(
           documentedPartialFieldInSubclassOnly.annotations
@@ -2517,7 +2537,7 @@
     test('@nodoc overridden in subclass for getter works', () {
       expect(explicitNonDocumentedInBaseClassGetter.isPublic, isTrue);
       expect(explicitNonDocumentedInBaseClassGetter.hasPublicGetter, isTrue);
-      expect(explicitNonDocumentedInBaseClassGetter.computeDocumentationComment,
+      expect(explicitNonDocumentedInBaseClassGetter.documentationComment,
           contains('I should be documented'));
       expect(explicitNonDocumentedInBaseClassGetter.readOnly, isTrue);
     });
@@ -2804,14 +2824,14 @@
     test('@nodoc on setter only works', () {
       expect(nodocSetter.isPublic, isTrue);
       expect(nodocSetter.readOnly, isTrue);
-      expect(nodocSetter.computeDocumentationComment,
+      expect(nodocSetter.documentationComment,
           equals('Getter docs should be shown.'));
     });
 
     test('@nodoc on getter only works', () {
       expect(nodocGetter.isPublic, isTrue);
       expect(nodocGetter.writeOnly, isTrue);
-      expect(nodocGetter.computeDocumentationComment,
+      expect(nodocGetter.documentationComment,
           equals('Setter docs should be shown.'));
     });
 
diff --git a/test/src/utils.dart b/test/src/utils.dart
index 68fd79b..2002660 100644
--- a/test/src/utils.dart
+++ b/test/src/utils.dart
@@ -81,7 +81,8 @@
       additionalArguments: additionalArguments);
 
   testPackageGraphErrors = await bootBasicPackage(
-      'testing/test_package_doc_errors', ['css', 'code_in_comments', 'excluded'],
+      'testing/test_package_doc_errors',
+      ['css', 'code_in_comments', 'excluded'],
       additionalArguments: additionalArguments);
   testPackageGraphSdk = await bootSdkPackage();
 }
diff --git a/test/utils_test.dart b/test/utils_test.dart
index 59539c2..9ef899c 100644
--- a/test/utils_test.dart
+++ b/test/utils_test.dart
@@ -207,7 +207,7 @@
           '2 spaces, one tab (same as 3 space)\n'
           ' \t4 spaces, one tab (preserve the tab)\n'
           '3 space indent again\n';
-      expect(stripCommonWhitespace(input), equals(output));
+      expect(stripCommonWhitespace(input).join('\n'), equals(output));
     });
   });
 }
diff --git a/testing/test_package/bin/setup.dart b/testing/test_package/bin/setup.dart
index 67a6ca1..f67b442 100644
--- a/testing/test_package/bin/setup.dart
+++ b/testing/test_package/bin/setup.dart
@@ -10,7 +10,7 @@
 void main(List<String> args) {
   assert(args.isNotEmpty);
   // Just touch the file given on the command line.
-  File setupFile = new File(args[0])..createSync(recursive:true);
+  File setupFile = new File(args[0])..createSync(recursive: true);
   setupFile.writeAsStringSync('setup');
   exit(0);
 }