|
| 1 | +// Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +import 'dart:io'; |
| 6 | + |
| 7 | +import 'package:path/path.dart' as path; |
| 8 | + |
| 9 | +/// Scans the dartdoc HTML output in the provided `htmlOutputPath` for |
| 10 | +/// unresolved dartdoc directives (`{@foo x y}`). |
| 11 | +/// |
| 12 | +/// Dartdoc usually replaces those directives with other content. However, |
| 13 | +/// if the directive is misspelled (or contains other errors) it is placed |
| 14 | +/// verbatim into the HTML output. That's not desirable and this check verifies |
| 15 | +/// that no directives appear verbatim in the output by checking that the |
| 16 | +/// string `{@` does not appear in the HTML output outside of <code> sections. |
| 17 | +/// |
| 18 | +/// The string `{@` is allowed in <code> sections, because those may contain |
| 19 | +/// sample code where the sequence is perfectly legal, e.g. for required named |
| 20 | +/// parameters of a method: |
| 21 | +/// |
| 22 | +/// ``` |
| 23 | +/// void foo({@required int bar}); |
| 24 | +/// ``` |
| 25 | +void checkForUnresolvedDirectives(String htmlOutputPath) { |
| 26 | + final Directory dartDocDir = Directory(htmlOutputPath); |
| 27 | + if (!dartDocDir.existsSync()) { |
| 28 | + throw Exception('Directory with dartdoc output (${dartDocDir.path}) does not exist.'); |
| 29 | + } |
| 30 | + |
| 31 | + // Makes sure that the path we were given contains some of the expected |
| 32 | + // libraries and HTML files. |
| 33 | + final List<String> canaryLibraries = <String>[ |
| 34 | + 'animation', |
| 35 | + 'cupertino', |
| 36 | + 'material', |
| 37 | + 'widgets', |
| 38 | + 'rendering', |
| 39 | + 'flutter_driver', |
| 40 | + ]; |
| 41 | + final List<String> canaryFiles = <String>[ |
| 42 | + 'Widget-class.html', |
| 43 | + 'Material-class.html', |
| 44 | + 'Canvas-class.html', |
| 45 | + ]; |
| 46 | + |
| 47 | + print('Scanning for unresolved dartdoc directives...'); |
| 48 | + |
| 49 | + final List<FileSystemEntity> toScan = dartDocDir.listSync(); |
| 50 | + int count = 0; |
| 51 | + |
| 52 | + while (toScan.isNotEmpty) { |
| 53 | + final FileSystemEntity entity = toScan.removeLast(); |
| 54 | + if (entity is File) { |
| 55 | + if (path.extension(entity.path) != '.html') { |
| 56 | + continue; |
| 57 | + } |
| 58 | + canaryFiles.remove(path.basename(entity.path)); |
| 59 | + |
| 60 | + // TODO(goderbauer): Remove this exception when https://github.com/dart-lang/dartdoc/issues/2272 is fixed. |
| 61 | + if (entity.path.endsWith('-class.html') || entity.path.endsWith('-library.html') ) { |
| 62 | + continue; |
| 63 | + } |
| 64 | + |
| 65 | + count += _scanFile(entity); |
| 66 | + } else if (entity is Directory) { |
| 67 | + canaryLibraries.remove(path.basename(entity.path)); |
| 68 | + toScan.addAll(entity.listSync()); |
| 69 | + } else { |
| 70 | + throw Exception('$entity is neither file nor directory.'); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + if (canaryLibraries.isNotEmpty) { |
| 75 | + throw Exception('Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.'); |
| 76 | + } |
| 77 | + if (canaryFiles.isNotEmpty) { |
| 78 | + throw Exception('Did not find docs for the following files: ${canaryFiles.join(', ')}.'); |
| 79 | + } |
| 80 | + if (count > 0) { |
| 81 | + throw Exception('Found $count unresolved dartdoc directives (see log above).'); |
| 82 | + } |
| 83 | + print('No unresolved dartdoc directives detected.'); |
| 84 | +} |
| 85 | + |
| 86 | +int _scanFile(File file) { |
| 87 | + assert(path.extension(file.path) == 'html'); |
| 88 | + Iterable<String> matches = _pattern.allMatches(file.readAsStringSync()) |
| 89 | + .map((RegExpMatch m ) => m.group(0)); |
| 90 | + |
| 91 | + // TODO(goderbauer): Remove this exception when https://github.com/dart-lang/dartdoc/issues/1945 is fixed. |
| 92 | + matches = matches |
| 93 | + .where((String m) => m != '{@inject-html}') |
| 94 | + .where((String m) => m != '{@end-inject-html}'); |
| 95 | + |
| 96 | + if (matches.isNotEmpty) { |
| 97 | + stderr.writeln('Found unresolved dartdoc directives in ${file.path}:'); |
| 98 | + for (final String match in matches) { |
| 99 | + stderr.writeln(' $match'); |
| 100 | + } |
| 101 | + } |
| 102 | + return matches.length; |
| 103 | +} |
| 104 | + |
| 105 | +// Matches all `{@` that are not within `<code></code>` sections. |
| 106 | +// |
| 107 | +// This regex may lead to false positives if the docs ever contain nested tags |
| 108 | +// inside <code> sections. Since we currently don't do that, doing the matching |
| 109 | +// with a regex is a lot faster than using an HTML parser to strip out the |
| 110 | +// <code> sections. |
| 111 | +final RegExp _pattern = RegExp(r'({@[^}\n]*}?)(?![^<>]*</code)'); |
| 112 | + |
| 113 | +// Usually, the checker is invoked directly from `dartdoc.dart`. Main method |
| 114 | +// is included for convenient local runs without having to regenerate |
| 115 | +// the dartdocs every time. |
| 116 | +// |
| 117 | +// Provide the path to the dartdoc HTML output as an argument when running the |
| 118 | +// program. |
| 119 | +void main(List<String> args) { |
| 120 | + if (args.length != 1) { |
| 121 | + throw Exception('Must provide the path to the dartdoc HTML output as argument.'); |
| 122 | + } |
| 123 | + if (!Directory(args.single).existsSync()) { |
| 124 | + throw Exception('The dartdoc HTML output directory ${args.single} does not exist.'); |
| 125 | + } |
| 126 | + checkForUnresolvedDirectives(args.single); |
| 127 | +} |
0 commit comments