Ban unresolved dartdoc directives from HTML output (#62167)
diff --git a/dev/tools/dartdoc_checker.dart b/dev/tools/dartdoc_checker.dart new file mode 100644 index 0000000..9340b40 --- /dev/null +++ b/dev/tools/dartdoc_checker.dart
@@ -0,0 +1,127 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +/// Scans the dartdoc HTML output in the provided `htmlOutputPath` for +/// unresolved dartdoc directives (`{@foo x y}`). +/// +/// Dartdoc usually replaces those directives with other content. However, +/// if the directive is misspelled (or contains other errors) it is placed +/// verbatim into the HTML output. That's not desirable and this check verifies +/// that no directives appear verbatim in the output by checking that the +/// string `{@` does not appear in the HTML output outside of <code> sections. +/// +/// The string `{@` is allowed in <code> sections, because those may contain +/// sample code where the sequence is perfectly legal, e.g. for required named +/// parameters of a method: +/// +/// ``` +/// void foo({@required int bar}); +/// ``` +void checkForUnresolvedDirectives(String htmlOutputPath) { + final Directory dartDocDir = Directory(htmlOutputPath); + if (!dartDocDir.existsSync()) { + throw Exception('Directory with dartdoc output (${dartDocDir.path}) does not exist.'); + } + + // Makes sure that the path we were given contains some of the expected + // libraries and HTML files. + final List<String> canaryLibraries = <String>[ + 'animation', + 'cupertino', + 'material', + 'widgets', + 'rendering', + 'flutter_driver', + ]; + final List<String> canaryFiles = <String>[ + 'Widget-class.html', + 'Material-class.html', + 'Canvas-class.html', + ]; + + print('Scanning for unresolved dartdoc directives...'); + + final List<FileSystemEntity> toScan = dartDocDir.listSync(); + int count = 0; + + while (toScan.isNotEmpty) { + final FileSystemEntity entity = toScan.removeLast(); + if (entity is File) { + if (path.extension(entity.path) != '.html') { + continue; + } + canaryFiles.remove(path.basename(entity.path)); + + // TODO(goderbauer): Remove this exception when https://github.com/dart-lang/dartdoc/issues/2272 is fixed. + if (entity.path.endsWith('-class.html') || entity.path.endsWith('-library.html') ) { + continue; + } + + count += _scanFile(entity); + } else if (entity is Directory) { + canaryLibraries.remove(path.basename(entity.path)); + toScan.addAll(entity.listSync()); + } else { + throw Exception('$entity is neither file nor directory.'); + } + } + + if (canaryLibraries.isNotEmpty) { + throw Exception('Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.'); + } + if (canaryFiles.isNotEmpty) { + throw Exception('Did not find docs for the following files: ${canaryFiles.join(', ')}.'); + } + if (count > 0) { + throw Exception('Found $count unresolved dartdoc directives (see log above).'); + } + print('No unresolved dartdoc directives detected.'); +} + +int _scanFile(File file) { + assert(path.extension(file.path) == 'html'); + Iterable<String> matches = _pattern.allMatches(file.readAsStringSync()) + .map((RegExpMatch m ) => m.group(0)); + + // TODO(goderbauer): Remove this exception when https://github.com/dart-lang/dartdoc/issues/1945 is fixed. + matches = matches + .where((String m) => m != '{@inject-html}') + .where((String m) => m != '{@end-inject-html}'); + + if (matches.isNotEmpty) { + stderr.writeln('Found unresolved dartdoc directives in ${file.path}:'); + for (final String match in matches) { + stderr.writeln(' $match'); + } + } + return matches.length; +} + +// Matches all `{@` that are not within `<code></code>` sections. +// +// This regex may lead to false positives if the docs ever contain nested tags +// inside <code> sections. Since we currently don't do that, doing the matching +// with a regex is a lot faster than using an HTML parser to strip out the +// <code> sections. +final RegExp _pattern = RegExp(r'({@[^}\n]*}?)(?![^<>]*</code)'); + +// Usually, the checker is invoked directly from `dartdoc.dart`. Main method +// is included for convenient local runs without having to regenerate +// the dartdocs every time. +// +// Provide the path to the dartdoc HTML output as an argument when running the +// program. +void main(List<String> args) { + if (args.length != 1) { + throw Exception('Must provide the path to the dartdoc HTML output as argument.'); + } + if (!Directory(args.single).existsSync()) { + throw Exception('The dartdoc HTML output directory ${args.single} does not exist.'); + } + checkForUnresolvedDirectives(args.single); +}