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