|
5 | 5 | import 'dart:convert';
|
6 | 6 | import 'dart:io';
|
7 | 7 |
|
8 |
| -import 'package:collection/collection.dart'; |
9 | 8 | import 'package:path/path.dart' as p;
|
10 | 9 | import 'package:yaml/yaml.dart' as yaml;
|
11 | 10 | import 'package:http/http.dart' as http;
|
12 | 11 |
|
| 12 | +/// Source of truth for linter rules. |
| 13 | +const rulesUrl = |
| 14 | + 'https://raw.githubusercontent.com/dart-lang/site-www/main/src/_data/linter_rules.json'; |
| 15 | + |
| 16 | +/// Local cache of linter rules from [rulesUrl]. |
| 17 | +/// |
| 18 | +/// Relative to package root. |
| 19 | +const rulesCacheFilePath = 'tool/rules.json'; |
| 20 | + |
| 21 | +/// Generated rules documentation markdown file. |
| 22 | +/// |
| 23 | +/// Relative to package root. |
| 24 | +const rulesMarkdownFilePath = 'rules.md'; |
| 25 | + |
| 26 | +/// Fetches the [rulesUrl] JSON description of all lints, saves a cached |
| 27 | +/// summary of the relevant fields in [rulesCacheFilePath], and |
| 28 | +/// updates [rulesMarkdownFilePath] to |
| 29 | +/// |
| 30 | +/// Passing any command line argument disables generating documentation, |
| 31 | +/// and makes this tool just verify that the doc is up-to-date with the |
| 32 | +/// [rulesCacheFilePath]. (Which it always should be, since the two |
| 33 | +/// are saved at the same time.) |
13 | 34 | void main(List<String> args) async {
|
14 |
| - final justVerify = args.isNotEmpty; |
15 |
| - final lintRules = <String, List<String>>{}; |
16 |
| - |
17 |
| - final rulesJsonFile = File('tool/rules.json'); |
18 |
| - final rulesUrl = |
19 |
| - 'https://raw.githubusercontent.com/dart-lang/site-www/main/src/_data/linter_rules.json'; |
20 |
| - if (!justVerify) { |
21 |
| - rulesJsonFile.writeAsStringSync((await http.get(Uri.parse(rulesUrl))).body); |
| 35 | + final verifyOnly = args.isNotEmpty; |
| 36 | + |
| 37 | + // Read lint rules. |
| 38 | + final rulesJson = await _fetchRulesJson(verifyOnly: verifyOnly); |
| 39 | + |
| 40 | + // Read existing generated Markdown documentation. |
| 41 | + final rulesMarkdownFile = _packageRelativeFile(rulesMarkdownFilePath); |
| 42 | + final rulesMarkdownContent = rulesMarkdownFile.readAsStringSync(); |
| 43 | + |
| 44 | + if (verifyOnly) { |
| 45 | + print('Validating that ${rulesMarkdownFile.path} is up-to-date ...'); |
| 46 | + } else { |
| 47 | + print('Regenerating ${rulesMarkdownFile.path} ...'); |
22 | 48 | }
|
23 |
| - final rulesJson = (jsonDecode(rulesJsonFile.readAsStringSync()) as List) |
24 |
| - .cast<Map<String, dynamic>>(); |
25 | 49 |
|
26 |
| - final rulesMdFile = File('rules.md'); |
27 |
| - final rulesMdContent = rulesMdFile.readAsStringSync(); |
| 50 | + // Generate new documentation. |
| 51 | + var newRulesMarkdownContent = |
| 52 | + _updateMarkdown(rulesMarkdownContent, rulesJson); |
| 53 | + |
| 54 | + // If no documentation change, all is up-to-date. |
| 55 | + if (newRulesMarkdownContent == rulesMarkdownContent) { |
| 56 | + print('${rulesMarkdownFile.path} is up-to-date.'); |
| 57 | + return; |
| 58 | + } |
28 | 59 |
|
29 |
| - if (justVerify) { |
30 |
| - print('Validating that ${rulesMdFile.path} is up-to-date ...'); |
| 60 | + /// Documentation has changed. |
| 61 | + if (verifyOnly) { |
| 62 | + print('${rulesMarkdownFile.path} is not up-to-date (lint tables need to be ' |
| 63 | + 'regenerated).'); |
| 64 | + print(''); |
| 65 | + print("Run 'dart tool/gen_docs.dart' to re-generate."); |
| 66 | + exit(1); |
31 | 67 | } else {
|
32 |
| - print('Regenerating ${rulesMdFile.path} ...'); |
| 68 | + // Save [rulesMarkdownFilePath]. |
| 69 | + rulesMarkdownFile.writeAsStringSync(newRulesMarkdownContent); |
| 70 | + print('Wrote ${rulesMarkdownFile.path}.'); |
33 | 71 | }
|
| 72 | +} |
34 | 73 |
|
35 |
| - for (var file in ['lib/core.yaml', 'lib/recommended.yaml']) { |
36 |
| - var name = p.basenameWithoutExtension(file); |
37 |
| - lintRules[name] = _parseRules(File(file)); |
| 74 | +/// Fetches or load the JSON lint rules. |
| 75 | +/// |
| 76 | +/// If [verifyOnly] is `false`, fetches JSON from [rulesUrl], |
| 77 | +/// extracts the needed information, and writes a summary to |
| 78 | +/// [rulesCacheFilePath]. |
| 79 | +/// |
| 80 | +/// If [verifyOnly] is `true`, only reads the cached data back from |
| 81 | +/// [rulesCacheFilePath]. |
| 82 | +Future<Map<String, Map<String, String>>> _fetchRulesJson( |
| 83 | + {required bool verifyOnly}) async { |
| 84 | + final rulesJsonFile = _packageRelativeFile(rulesCacheFilePath); |
| 85 | + if (verifyOnly) { |
| 86 | + final rulesJsonText = rulesJsonFile.readAsStringSync(); |
| 87 | + return _readJson(rulesJsonText); |
38 | 88 | }
|
| 89 | + final rulesJsonText = (await http.get(Uri.parse(rulesUrl))).body; |
| 90 | + final rulesJson = _readJson(rulesJsonText); |
39 | 91 |
|
40 |
| - var newContent = rulesMdContent; |
| 92 | + // Re-save [rulesJsonFile] file. |
| 93 | + var newRulesJson = [...rulesJson.values]; |
| 94 | + rulesJsonFile |
| 95 | + .writeAsStringSync(JsonEncoder.withIndent(' ').convert(newRulesJson)); |
41 | 96 |
|
42 |
| - for (var ruleSetName in lintRules.keys) { |
43 |
| - final comment = '<!-- $ruleSetName -->\n'; |
| 97 | + return rulesJson; |
| 98 | +} |
44 | 99 |
|
45 |
| - newContent = newContent.replaceRange( |
46 |
| - newContent.indexOf(comment) + comment.length, |
47 |
| - newContent.lastIndexOf(comment), |
48 |
| - _createRuleTable(lintRules[ruleSetName]!, rulesJson), |
49 |
| - ); |
50 |
| - } |
| 100 | +/// Extracts relevant information from a list of JSON objects. |
| 101 | +/// |
| 102 | +/// For each JSON object, includes only the relevant (string-typed) properties, |
| 103 | +/// then creates a map indexed by the `'name'` property of the objects. |
| 104 | +Map<String, Map<String, String>> _readJson(String rulesJsonText) { |
| 105 | + /// Relevant keys in the JSON information about lints. |
| 106 | + const relevantKeys = {'name', 'description', 'fixStatus'}; |
| 107 | + final rulesJson = jsonDecode(rulesJsonText) as List<dynamic>; |
| 108 | + return { |
| 109 | + for (Map<String, Object?> rule in rulesJson) |
| 110 | + rule['name'] as String: { |
| 111 | + for (var key in relevantKeys) key: rule[key] as String |
| 112 | + } |
| 113 | + }; |
| 114 | +} |
51 | 115 |
|
52 |
| - if (justVerify) { |
53 |
| - if (newContent != rulesMdContent) { |
54 |
| - print('${rulesMdFile.path} is not up-to-date (lint tables need to be ' |
55 |
| - 'regenerated).'); |
56 |
| - print(''); |
57 |
| - print("Run 'dart tool/gen_docs.dart' to re-generate."); |
58 |
| - exit(1); |
59 |
| - } else { |
60 |
| - print('${rulesMdFile.path} is up-to-date.'); |
61 |
| - } |
62 |
| - } else { |
63 |
| - // Re-save rules.json. |
64 |
| - const retainKeys = {'name', 'description', 'fixStatus'}; |
65 |
| - for (var rule in rulesJson) { |
66 |
| - rule.removeWhere((key, value) => !retainKeys.contains(key)); |
| 116 | +/// Inserts new Markdown content for both rule sets into [content]. |
| 117 | +/// |
| 118 | +/// For both "core" and "recommended" rule sets, |
| 119 | +/// replaces the table between the two `<!-- core -->` and the two |
| 120 | +/// `<!-- recommended -->` markers with a new table generated from |
| 121 | +/// [rulesJson], based on the list of rules in `lib/core.yaml` and |
| 122 | +/// `lib/recommended.yaml`. |
| 123 | +String _updateMarkdown( |
| 124 | + String content, Map<String, Map<String, String>> rulesJson) { |
| 125 | + for (var ruleSetName in ['core', 'recommended']) { |
| 126 | + var ruleFile = _packageRelativeFile(p.join('lib', '$ruleSetName.yaml')); |
| 127 | + var ruleSet = _parseRules(ruleFile); |
| 128 | + |
| 129 | + final rangeDelimiter = '<!-- $ruleSetName -->\n'; |
| 130 | + var rangeStart = content.indexOf(rangeDelimiter) + rangeDelimiter.length; |
| 131 | + var rangeEnd = content.indexOf(rangeDelimiter, rangeStart); |
| 132 | + if (rangeEnd < 0) { |
| 133 | + stderr.writeln('Missing "$rangeDelimiter" in $rulesMarkdownFilePath.'); |
| 134 | + continue; |
67 | 135 | }
|
68 |
| - rulesJsonFile |
69 |
| - .writeAsStringSync(JsonEncoder.withIndent(' ').convert(rulesJson)); |
70 |
| - |
71 |
| - // Write out the rules md file. |
72 |
| - rulesMdFile.writeAsStringSync(newContent); |
73 |
| - print('Wrote ${rulesMdFile.path}.'); |
| 136 | + content = content.replaceRange( |
| 137 | + rangeStart, rangeEnd, _createRuleTable(ruleSet, rulesJson)); |
74 | 138 | }
|
| 139 | + return content; |
75 | 140 | }
|
76 | 141 |
|
| 142 | +/// Parses analysis options YAML file, and extracts linter rules. |
77 | 143 | List<String> _parseRules(File yamlFile) {
|
78 | 144 | var yamlData = yaml.loadYaml(yamlFile.readAsStringSync()) as Map;
|
79 |
| - return (yamlData['linter']['rules'] as List).toList().cast<String>(); |
| 145 | + var linterEntry = yamlData['linter'] as Map; |
| 146 | + return List<String>.from(linterEntry['rules'] as List); |
80 | 147 | }
|
81 | 148 |
|
| 149 | +/// Creates markdown source for a table of lint rules. |
82 | 150 | String _createRuleTable(
|
83 |
| - List<String> rules, List<Map<String, dynamic>> lintMeta) { |
| 151 | + List<String> rules, Map<String, Map<String, String>> lintMeta) { |
84 | 152 | rules.sort();
|
85 | 153 |
|
86 | 154 | final lines = [
|
87 | 155 | '| Lint Rules | Description | [Fix][] |',
|
88 | 156 | '| :--------- | :---------- | ------- |',
|
89 |
| - ...rules.map((rule) { |
90 |
| - final ruleMeta = |
91 |
| - lintMeta.firstWhereOrNull((meta) => meta['name'] == rule); |
92 |
| - |
93 |
| - final description = ruleMeta?['description'] as String? ?? ''; |
94 |
| - final hasFix = ruleMeta?['fixStatus'] == 'hasFix'; |
95 |
| - final fixDesc = hasFix ? '✅' : ''; |
96 |
| - |
97 |
| - return '| [`$rule`](https://dart.dev/lints/$rule) | $description | $fixDesc |'; |
98 |
| - }), |
| 157 | + for (var rule in rules) _createRuleTableRow(rule, lintMeta), |
99 | 158 | ];
|
100 | 159 |
|
101 | 160 | return '${lines.join('\n')}\n';
|
102 | 161 | }
|
| 162 | + |
| 163 | +/// Creates a line containing the markdown table row for a single lint rule. |
| 164 | +/// |
| 165 | +/// Used by [_createRuleTable] for each row in the generated table. |
| 166 | +/// The row should have the same number of entires as the table format, |
| 167 | +/// and should be on a single line with no newline at the end. |
| 168 | +String _createRuleTableRow( |
| 169 | + String rule, Map<String, Map<String, String>> lintMeta) { |
| 170 | + final ruleMeta = lintMeta[rule]; |
| 171 | + if (ruleMeta == null) { |
| 172 | + stderr.writeln("WARNING: Missing rule information for rule: $rule"); |
| 173 | + } |
| 174 | + final description = ruleMeta?['description'] ?? ''; |
| 175 | + final hasFix = ruleMeta?['fixStatus'] == 'hasFix'; |
| 176 | + final fixDesc = hasFix ? '✅' : ''; |
| 177 | + |
| 178 | + return '| [`$rule`](https://dart.dev/lints/$rule) | ' |
| 179 | + '$description | $fixDesc |'; |
| 180 | +} |
| 181 | + |
| 182 | +/// A path relative to the root of this package. |
| 183 | +/// |
| 184 | +/// Works independently of the current working directory. |
| 185 | +/// Is based on the location of this script, through [Platform.script]. |
| 186 | +File _packageRelativeFile(String packagePath) => |
| 187 | + File(p.join(_packageRoot, packagePath)); |
| 188 | + |
| 189 | +/// Cached package root used by [_packageRelative]. |
| 190 | +final String _packageRoot = _relativePackageRoot(); |
| 191 | + |
| 192 | +/// A path to the package root from the current directory. |
| 193 | +/// |
| 194 | +/// If the current directory is inside the package, the returned path is |
| 195 | +/// a relative path of a number of `..` segments. |
| 196 | +/// If the current directory is outside of the package, the returned path |
| 197 | +/// may be absolute. |
| 198 | +String _relativePackageRoot() { |
| 199 | + var rootPath = p.dirname(p.dirname(Platform.script.path)); |
| 200 | + if (p.isRelative(rootPath)) return rootPath; |
| 201 | + var baseDir = p.current; |
| 202 | + if (rootPath == baseDir) return ''; |
| 203 | + if (baseDir.startsWith(rootPath)) { |
| 204 | + var backSteps = <String>[]; |
| 205 | + do { |
| 206 | + backSteps.add('..'); |
| 207 | + baseDir = p.dirname(baseDir); |
| 208 | + } while (baseDir != rootPath); |
| 209 | + return p.joinAll(backSteps); |
| 210 | + } |
| 211 | + return rootPath; |
| 212 | +} |
0 commit comments