| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as p; |
| import 'package:yaml/yaml.dart' as yaml; |
| import 'package:http/http.dart' as http; |
| |
| /// Source of truth for linter rules. |
| const rulesUrl = |
| 'https://raw.githubusercontent.com/dart-lang/site-www/main/src/_data/linter_rules.json'; |
| |
| /// Local cache of linter rules from [rulesUrl]. |
| /// |
| /// Relative to package root. |
| const rulesCacheFilePath = 'tool/rules.json'; |
| |
| /// Generated rules documentation markdown file. |
| /// |
| /// Relative to package root. |
| const rulesMarkdownFilePath = 'rules.md'; |
| |
| /// Fetches the [rulesUrl] JSON description of all lints, saves a cached |
| /// summary of the relevant fields in [rulesCacheFilePath], and |
| /// updates [rulesMarkdownFilePath] to |
| /// |
| /// Passing any command line argument disables generating documentation, |
| /// and makes this tool just verify that the doc is up-to-date with the |
| /// [rulesCacheFilePath]. (Which it always should be, since the two |
| /// are saved at the same time.) |
| void main(List<String> args) async { |
| final verifyOnly = args.isNotEmpty; |
| |
| // Read lint rules. |
| final rulesJson = await _fetchRulesJson(verifyOnly: verifyOnly); |
| |
| // Read existing generated Markdown documentation. |
| final rulesMarkdownFile = _packageRelativeFile(rulesMarkdownFilePath); |
| final rulesMarkdownContent = rulesMarkdownFile.readAsStringSync(); |
| |
| if (verifyOnly) { |
| print('Validating that ${rulesMarkdownFile.path} is up-to-date ...'); |
| } else { |
| print('Regenerating ${rulesMarkdownFile.path} ...'); |
| } |
| |
| // Generate new documentation. |
| var newRulesMarkdownContent = _updateMarkdown( |
| rulesMarkdownContent, |
| rulesJson, |
| ); |
| |
| // If no documentation change, all is up-to-date. |
| if (newRulesMarkdownContent == rulesMarkdownContent) { |
| print('${rulesMarkdownFile.path} is up-to-date.'); |
| return; |
| } |
| |
| /// Documentation has changed. |
| if (verifyOnly) { |
| print( |
| '${rulesMarkdownFile.path} is not up-to-date (lint tables need to be ' |
| 'regenerated).', |
| ); |
| print(''); |
| print("Run 'dart tool/gen_docs.dart' to re-generate."); |
| exit(1); |
| } else { |
| // Save [rulesMarkdownFilePath]. |
| rulesMarkdownFile.writeAsStringSync(newRulesMarkdownContent); |
| print('Wrote ${rulesMarkdownFile.path}.'); |
| } |
| } |
| |
| /// Fetches or load the JSON lint rules. |
| /// |
| /// If [verifyOnly] is `false`, fetches JSON from [rulesUrl], |
| /// extracts the needed information, and writes a summary to |
| /// [rulesCacheFilePath]. |
| /// |
| /// If [verifyOnly] is `true`, only reads the cached data back from |
| /// [rulesCacheFilePath]. |
| Future<Map<String, Map<String, String>>> _fetchRulesJson({ |
| required bool verifyOnly, |
| }) async { |
| final rulesJsonFile = _packageRelativeFile(rulesCacheFilePath); |
| if (verifyOnly) { |
| final rulesJsonText = rulesJsonFile.readAsStringSync(); |
| return _readJson(rulesJsonText); |
| } |
| final rulesJsonText = (await http.get(Uri.parse(rulesUrl))).body; |
| final rulesJson = _readJson(rulesJsonText); |
| |
| // Re-save [rulesJsonFile] file. |
| var newRulesJson = [...rulesJson.values]; |
| rulesJsonFile.writeAsStringSync( |
| JsonEncoder.withIndent(' ').convert(newRulesJson), |
| ); |
| |
| return rulesJson; |
| } |
| |
| /// Extracts relevant information from a list of JSON objects. |
| /// |
| /// For each JSON object, includes only the relevant (string-typed) properties, |
| /// then creates a map indexed by the `'name'` property of the objects. |
| Map<String, Map<String, String>> _readJson(String rulesJsonText) { |
| /// Relevant keys in the JSON information about lints. |
| const relevantKeys = {'name', 'description', 'fixStatus'}; |
| final rulesJson = jsonDecode(rulesJsonText) as List<dynamic>; |
| return { |
| for (Map<String, Object?> rule in rulesJson) |
| rule['name'] as String: { |
| for (var key in relevantKeys) key: rule[key] as String, |
| }, |
| }; |
| } |
| |
| /// Inserts new Markdown content for both rule sets into [content]. |
| /// |
| /// For both "core" and "recommended" rule sets, |
| /// replaces the table between the two `<!-- core -->` and the two |
| /// `<!-- recommended -->` markers with a new table generated from |
| /// [rulesJson], based on the list of rules in `lib/core.yaml` and |
| /// `lib/recommended.yaml`. |
| String _updateMarkdown( |
| String content, |
| Map<String, Map<String, String>> rulesJson, |
| ) { |
| for (var ruleSetName in ['core', 'recommended']) { |
| var ruleFile = _packageRelativeFile(p.join('lib', '$ruleSetName.yaml')); |
| var ruleSet = _parseRules(ruleFile); |
| |
| final rangeDelimiter = '<!-- $ruleSetName -->\n'; |
| var rangeStart = content.indexOf(rangeDelimiter) + rangeDelimiter.length; |
| var rangeEnd = content.indexOf(rangeDelimiter, rangeStart); |
| if (rangeEnd < 0) { |
| stderr.writeln('Missing "$rangeDelimiter" in $rulesMarkdownFilePath.'); |
| continue; |
| } |
| content = content.replaceRange( |
| rangeStart, |
| rangeEnd, |
| _createRuleTable(ruleSet, rulesJson), |
| ); |
| } |
| return content; |
| } |
| |
| /// Parses analysis options YAML file, and extracts linter rules. |
| List<String> _parseRules(File yamlFile) { |
| var yamlData = yaml.loadYaml(yamlFile.readAsStringSync()) as Map; |
| var linterEntry = yamlData['linter'] as Map; |
| return List<String>.from(linterEntry['rules'] as List); |
| } |
| |
| /// Creates markdown source for a table of lint rules. |
| String _createRuleTable( |
| List<String> rules, |
| Map<String, Map<String, String>> lintMeta, |
| ) { |
| rules.sort(); |
| |
| final lines = [ |
| '| Lint Rules | Description | [Fix][] |', |
| '| :--------- | :---------- | ------- |', |
| for (var rule in rules) _createRuleTableRow(rule, lintMeta), |
| ]; |
| |
| return '${lines.join('\n')}\n'; |
| } |
| |
| /// Creates a line containing the markdown table row for a single lint rule. |
| /// |
| /// Used by [_createRuleTable] for each row in the generated table. |
| /// The row should have the same number of entires as the table format, |
| /// and should be on a single line with no newline at the end. |
| String _createRuleTableRow( |
| String rule, |
| Map<String, Map<String, String>> lintMeta, |
| ) { |
| final ruleMeta = lintMeta[rule]; |
| if (ruleMeta == null) { |
| stderr.writeln("WARNING: Missing rule information for rule: $rule"); |
| } |
| final description = (ruleMeta?['description'] ?? '') |
| .replaceAll('\n', ' ') |
| .replaceAll(RegExp(r'\s+'), ' ') |
| .trim(); |
| final hasFix = ruleMeta?['fixStatus'] == 'hasFix'; |
| final fixDesc = hasFix ? '✅' : ''; |
| |
| return '| [`$rule`](https://dart.dev/lints/$rule) | ' |
| '$description | $fixDesc |'; |
| } |
| |
| /// A path relative to the root of this package. |
| /// |
| /// Works independently of the current working directory. |
| /// Is based on the location of this script, through [Platform.script]. |
| File _packageRelativeFile(String packagePath) => |
| File(p.join(_packageRoot, packagePath)); |
| |
| /// Cached package root used by [_packageRelative]. |
| final String _packageRoot = _relativePackageRoot(); |
| |
| /// A path to the package root from the current directory. |
| /// |
| /// If the current directory is inside the package, the returned path is |
| /// a relative path of a number of `..` segments. |
| /// If the current directory is outside of the package, the returned path |
| /// may be absolute. |
| String _relativePackageRoot() { |
| var rootPath = p.dirname(p.dirname(Platform.script.path)); |
| if (p.isRelative(rootPath)) return rootPath; |
| var baseDir = p.current; |
| if (rootPath == baseDir) return ''; |
| if (baseDir.startsWith(rootPath)) { |
| var backSteps = <String>[]; |
| do { |
| backSteps.add('..'); |
| baseDir = p.dirname(baseDir); |
| } while (baseDir != rootPath); |
| return p.joinAll(backSteps); |
| } |
| return rootPath; |
| } |