diff --git a/.github/workflows/bot_update_exclusions.yaml b/.github/workflows/bot_updater.yaml similarity index 59% rename from .github/workflows/bot_update_exclusions.yaml rename to .github/workflows/bot_updater.yaml index 1b474a0..2c25b7f 100644 --- a/.github/workflows/bot_update_exclusions.yaml +++ b/.github/workflows/bot_updater.yaml @@ -36,16 +36,16 @@ jobs: git config user.name VGV Bot git config user.email vgvbot@users.noreply.github.com - - name: ✍️ Make changes + - name: ✍️ Make changes for exclusion table if: ${{ env.did_change == 'true' }} run: dart lib/exclusion_reason_table.dart - - name: 📝 Create Pull Request + - name: 📝 Create Pull Request for exclusion table if: ${{ env.did_change == 'true' }} uses: peter-evans/create-pull-request@v7.0.8 with: base: main - branch: chore/update-spdx-license + branch: chore/update-exclusion-table commit-message: "docs: update exclusion table" title: "docs: update exclusion table" body: | @@ -54,3 +54,27 @@ jobs: author: VGV Bot assignees: vgvbot committer: VGV Bot + + - name: 🔍 Check for deprecated rules changes + id: deprecated + run: (dart bin/analyze.dart --set-exit-if-changed && echo "deprecated_rules_changed=false" >> $GITHUB_ENV) || echo "deprecated_rules_changed=true" >> $GITHUB_ENV + + - name: ✍️ Remove deprecated rules + if: ${{ env.deprecated_rules_changed == 'true' }} + run: dart bin/remove_deprecated_rules.dart + + - name: 📝 Create Pull Request for deprecated rules + if: ${{ env.deprecated_rules_changed == 'true' }} + uses: peter-evans/create-pull-request@v7.0.8 + with: + base: main + branch: feat/remove-deprecated-rules + commit-message: "feat: remove deprecated rules" + title: "feat: remove deprecated rules" + body: | + There are now rules deprecated that require an update to the Very Good Analysis. + labels: bot + author: VGV Bot + assignees: vgvbot + committer: VGV Bot + diff --git a/.github/workflows/tool_linter_rules.yaml b/.github/workflows/tool_linter_rules.yaml index 4dabcc7..c6100e9 100644 --- a/.github/workflows/tool_linter_rules.yaml +++ b/.github/workflows/tool_linter_rules.yaml @@ -1,29 +1,19 @@ name: linter_rules (tool) -on: pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths: + - 'tool/linter_rules/**' + - '.github/workflows/tool_linter_rules.yaml' + - 'pubspec.yaml' jobs: build: - defaults: - run: - working-directory: tool/linter_rules - - runs-on: ubuntu-latest - - steps: - - name: 📚 Git Checkout - uses: actions/checkout@v5 - - - name: 🎯 Setup Dart - uses: dart-lang/setup-dart@v1 - with: - sdk: 3.8.0 - - - name: 📦 Install Dependencies - run: dart pub get - - - name: ✨ Check Formatting - run: dart format --set-exit-if-changed . - - - name: 🕵️ Analyze - run: dart analyze --fatal-infos --fatal-warnings + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + dart_sdk: 3.8.0 + working_directory: tool/linter_rules diff --git a/tool/bump_version/main.dart b/tool/bump_version/lib/bump_version.dart similarity index 75% rename from tool/bump_version/main.dart rename to tool/bump_version/lib/bump_version.dart index bb035c8..2f019e4 100644 --- a/tool/bump_version/main.dart +++ b/tool/bump_version/lib/bump_version.dart @@ -31,19 +31,23 @@ final _latestAnalysisVersionRegExp = RegExp( r'analysis_options\.(\d+\.\d+\.\d+)\.yaml', ); -void main(List args) { - final analysisOptionsFile = File('lib/analysis_options.yaml'); +void main(List args) => bumpVersion(args[0]); + +/// Bumps the version of the analysis options file and the pubspec.yaml file. +void bumpVersion(String newVersion, {String basePath = ''}) { + final analysisOptionsFile = File('${basePath}lib/analysis_options.yaml'); final content = analysisOptionsFile.readAsStringSync(); final latestVersion = _latestAnalysisVersionRegExp .firstMatch(content) ?.group(1); final latestAnalysisOptionsFile = File( - 'lib/analysis_options.$latestVersion.yaml', + '${basePath}lib/analysis_options.$latestVersion.yaml', ); - final newVersion = args[0]; - final newAnalysisOptionsFile = File('lib/analysis_options.$newVersion.yaml'); + final newAnalysisOptionsFile = File( + '${basePath}lib/analysis_options.$newVersion.yaml', + ); latestAnalysisOptionsFile.copySync(newAnalysisOptionsFile.path); final newContent = content.replaceFirst( diff --git a/tool/bump_version/pubspec.yaml b/tool/bump_version/pubspec.yaml new file mode 100644 index 0000000..9858b50 --- /dev/null +++ b/tool/bump_version/pubspec.yaml @@ -0,0 +1,8 @@ +name: bump_version +description: Bumps the version of the analysis options file and the Very Good Analysis package. +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ^3.8.0 + diff --git a/tool/linter_rules/.gitignore b/tool/linter_rules/.gitignore index 526da15..57f0116 100644 --- a/tool/linter_rules/.gitignore +++ b/tool/linter_rules/.gitignore @@ -4,4 +4,5 @@ .dart_tool/ .packages build/ -pubspec.lock \ No newline at end of file +pubspec.lock +coverage/ \ No newline at end of file diff --git a/tool/linter_rules/README.md b/tool/linter_rules/README.md index 9a756ee..2eb7861 100644 --- a/tool/linter_rules/README.md +++ b/tool/linter_rules/README.md @@ -62,3 +62,35 @@ dart bin/analyze.dart $version ``` Where version is the existing Very Good Analysis version you would like to analyze, for example `9.0.0`. + + +## Check and remove deprecated rules 🔍 + +The script `tool/linter_rules/bin/remove_deprecated_rules.dart` helps maintain Very Good Analysis by automatically handling deprecated linter rules. + +This script will: + +- Fetch the latest rules from the official Dart SDK +- Identify any deprecated rules currently used in Very Good Analysis +- Remove deprecated rules from the analysis options file by creating a new minor version of Very Good Analysis without the deprecated rules +- Assign the new file as the latest version in lib/analysis_options.yaml +- Add removed rules to the exclusion table with reason 'Deprecated' + +### Usage + +To check and remove deprecated rules, run the following command (from `tool/linter_rules`): + +```sh +dart bin/remove_deprecated_rules.dart +``` + + +## Automations + +There is a [GitHub workflow](../../.github/workflows/bot_updater.yaml) that automates the maintenance of the linter rules. This workflow runs on a schedule (every weekday) and can also be triggered manually. + +It performs two main tasks: + +1. **Update Exclusion Table**: It checks if there are any changes to the exclusion reasons for linter rules. If there are, it regenerates the exclusion table in this README and creates a pull request with the title `docs: update exclusion table`. + +2. **Remove Deprecated Rules**: It checks for deprecated linter rules currently used in Very Good Analysis. If any are found, it automatically removes them from the analysis options, creates a new version of the analysis options file, and opens a pull request with the changes, titled `feat: remove deprecated rules`. \ No newline at end of file diff --git a/tool/linter_rules/bin/analyze.dart b/tool/linter_rules/bin/analyze.dart index 82b3621..96e3ff8 100644 --- a/tool/linter_rules/bin/analyze.dart +++ b/tool/linter_rules/bin/analyze.dart @@ -1,13 +1,8 @@ -import 'dart:convert'; +import 'dart:io'; -import 'package:http/http.dart'; +import 'package:args/args.dart'; import 'package:linter_rules/linter_rules.dart'; -/// The [Uri] to fetch all linter rules from. -final Uri _allLinterRulesUri = Uri.parse( - 'https://raw.githubusercontent.com/dart-lang/site-www/refs/heads/main/src/_data/linter_rules.json', -); - /// Compares Very Good Analysis with the all available Dart linter rules. /// /// Should be run from the root of the `linter_rules` package (tool/linter_rules). @@ -27,6 +22,9 @@ final Uri _allLinterRulesUri = Uri.parse( /// dart bin/analyze.dart 5.1.0 /// ``` /// +/// Set `--set-exit-if-changed` to exit with code 2 if there are deprecated +/// rules in the given Very Good Analysis version. +/// /// It will log information about: /// - The number of Dart linter rules fetched. /// - The number of rules being declared in the given Very Good Analysis @@ -37,24 +35,32 @@ Future main( List args, { void Function(String) log = print, }) async { - final version = args.isNotEmpty ? args[0] : latestVgaVersion(); + final argsParser = ArgParser() + ..addOption( + 'version', + help: + 'The version of the VGA to check for deprecated rules. ' + 'If not provided, the latest version will be used.', + ) + ..addFlag( + 'set-exit-if-changed', + help: + '''Set the exit code to 2 if there are changes to the deprecated rules.''', + ); - final response = await get(_allLinterRulesUri); - final json = jsonDecode(response.body) as List; + final parsedArgs = argsParser.parse(args); - final dartRules = json - .map((rule) => LinterRule.fromJson(rule as Map)) - .toList(); - log('Fetched ${dartRules.length} Dart linter rules'); + final version = parsedArgs['version'] as String? ?? latestVgaVersion(); + final setExitIfChanged = parsedArgs['set-exit-if-changed'] as bool; + + final dartRules = await allLinterRules(state: LinterRuleState.deprecated); + log('Fetched ${dartRules.length} deprecated Dart linter rules'); final vgaRules = await allVeryGoodAnalysisRules(version: version); log('Fetched ${vgaRules.length} Very Good Analysis rules'); log(''); - final deprecatedDartRules = dartRules - .where((rule) => rule.state == LinterRuleState.deprecated) - .map((rule) => rule.name) - .toSet(); + final deprecatedDartRules = dartRules.map((rule) => rule.name).toSet(); final deprecatedVgaRules = vgaRules .where(deprecatedDartRules.contains) .toList(); @@ -65,4 +71,8 @@ Future main( deprecationMessage.write('\n - $rule'); } log(deprecationMessage.toString()); + + if (deprecatedVgaRules.isNotEmpty && setExitIfChanged) { + exit(2); + } } diff --git a/tool/linter_rules/bin/remove_deprecated_rules.dart b/tool/linter_rules/bin/remove_deprecated_rules.dart new file mode 100644 index 0000000..ba74acf --- /dev/null +++ b/tool/linter_rules/bin/remove_deprecated_rules.dart @@ -0,0 +1,117 @@ +import 'dart:io'; + +import 'package:bump_version/bump_version.dart'; +import 'package:linter_rules/linter_rules.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +/// Removes deprecated rules from the analysis options file based on the +/// official Dart linter rules. +/// +/// It will create a new version of the analysis options file and update the +/// exclusion reasons file and the table of excluded rules in the README.md +/// file. +Future main({ + void Function(String) log = print, +}) async { + const basePath = '../../'; + final deprecatedRules = await allLinterRules( + state: LinterRuleState.deprecated, + ); + final deprecatedRulesCount = deprecatedRules.length; + log('Fetched $deprecatedRulesCount Dart deprecated linter rules'); + log(''); + + if (deprecatedRulesCount == 0) { + log('No deprecated Dart linter rules found.'); + return; + } + + final latestVersion = latestVgaVersion(); + log('Latest Very Good Analysis version: $latestVersion'); + log(''); + + final latestVgaRules = await allVeryGoodAnalysisRules( + version: latestVersion, + ); + log('Fetched ${latestVgaRules.length} Very Good Analysis linter rules'); + log(''); + + final deprecatedVgaRules = latestVgaRules + .where( + (rule) => deprecatedRules.any((dartRule) => dartRule.name == rule), + ) + .toList(); + final deprecatedVgaRulesCount = deprecatedVgaRules.length; + log( + 'Found $deprecatedVgaRulesCount deprecated Very Good Analysis rules:', + ); + + if (deprecatedVgaRulesCount == 0) { + log('No deprecated Very Good Analysis rules found.'); + return; + } + + for (final rule in deprecatedVgaRules) { + log(' - $rule'); + } + log(''); + + //// Update the exclusion reasons file. + final currentExclusionReasons = await readExclusionReasons(); + final newExclusionReasons = currentExclusionReasons + ..addAll({ + for (final rule in deprecatedVgaRules) rule: 'Deprecated', + }); + await writeExclusionReasons(newExclusionReasons); + log('''Updated the exclusion reasons file.'''); + log(''); + + //// Bump the version of the Very Good Analysis package. + final parts = latestVersion.split('.'); + // Increment the minor version. + final newVersion = '${parts[0]}.${int.parse(parts[1]) + 1}.0'; + bumpVersion( + newVersion, + basePath: basePath, + ); + log('Bumped Very Good Analysis version to $newVersion'); + log(''); + + //// Remove deprecated rules from the analysis options file. + final analysisOptionsFile = File( + '$basePath/lib/analysis_options.$newVersion.yaml', + ); + _removeLinterRules( + analysisOptionsFile.path, + deprecatedVgaRules, + ); + + //// Update the table of excluded rules in the README.md file. + final readme = Readme(); + final currentExclusionReasonsKeys = currentExclusionReasons.keys.toList(); + final markdownTable = readme.generateExcludedRulesTable( + currentExclusionReasonsKeys, + currentExclusionReasons, + ); + await readme.updateTagContent(excludedRulesTableTag, '\n$markdownTable'); + + log('''Updated the README.md file with the excluded rules table.'''); +} + +void _removeLinterRules(String filePath, List ruleNames) { + final yamlEditor = YamlEditor(File(filePath).readAsStringSync()); + + // Get the current rules list + final rules = yamlEditor.parseAt(['linter', 'rules']) as List; + + // Remove rules in reverse order to avoid index shifting + for (final ruleName in ruleNames.reversed) { + final index = rules.indexOf(ruleName); + if (index != -1) { + yamlEditor.remove(['linter', 'rules', index]); + } + } + + // Write back to file + File(filePath).writeAsStringSync(yamlEditor.toString()); +} diff --git a/tool/linter_rules/lib/exclusion_reason_table.dart b/tool/linter_rules/lib/exclusion_reason_table.dart index e978421..b1c3e1c 100644 --- a/tool/linter_rules/lib/exclusion_reason_table.dart +++ b/tool/linter_rules/lib/exclusion_reason_table.dart @@ -8,18 +8,6 @@ import 'package:linter_rules/linter_rules.dart'; /// file. const _noReasonFallback = 'Not specified'; -/// The tag delimiting the start and end of the excluded rules table in the -/// README.md file. -const _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. @@ -75,7 +63,7 @@ Future main( final version = parsedArgs['version'] as String? ?? latestVgaVersion(); final setExitIfChanged = parsedArgs['set-exit-if-changed'] as bool; - final linterRules = (await allLinterRules()).toSet(); + final linterRules = (await allLinterRules()).map((rule) => rule.name).toSet(); log('Found ${linterRules.length} available linter rules'); final veryGoodAnalysisRules = (await allVeryGoodAnalysisRules( @@ -103,16 +91,13 @@ Future main( } await writeExclusionReasons(exclusionReasons); + final readme = Readme(); + final markdownTable = readme.generateExcludedRulesTable( + excludedRules, + exclusionReasons, + ); - final markdownTable = generateMarkdownTable([ - ['Rule', 'Reason'], - ...excludedRules.map((rule) { - final ruleMarkdownLink = '[`$rule`](${_linterRuleLink(rule)})'; - return [ruleMarkdownLink, exclusionReasons[rule]!]; - }), - ]); - - await Readme().updateTagContent(_excludedRulesTableTag, '\n$markdownTable'); + 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 index a1ce8b9..d7bdce1 100644 --- a/tool/linter_rules/lib/linter_rules.dart +++ b/tool/linter_rules/lib/linter_rules.dart @@ -8,3 +8,4 @@ export 'src/linter_rules_reasons.dart'; export 'src/markdown_table_generator.dart'; export 'src/models/models.dart'; export 'src/readme.dart'; +export 'src/shared.dart'; diff --git a/tool/linter_rules/lib/src/all_linter_rules.dart b/tool/linter_rules/lib/src/all_linter_rules.dart index 99c1e8a..9387f62 100644 --- a/tool/linter_rules/lib/src/all_linter_rules.dart +++ b/tool/linter_rules/lib/src/all_linter_rules.dart @@ -1,36 +1,34 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:linter_rules/linter_rules.dart'; +import 'package:meta/meta.dart' show visibleForTesting; -/// The [Uri] to fetch all linter rules from. -final Uri _allLinterRulesUri = Uri.parse( - 'https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/tool/machine/rules.json', -); - -/// Fetches all linter rules names currently available in the Dart Language. +/// Fetches all linter rules currently available in the Dart Language. /// -/// It reads and parses from a JSON file at [_allLinterRulesUri]. +/// 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); +Future> allLinterRules({ + LinterRuleState? state, + void Function(String) log = print, + @visibleForTesting + Future Function(Uri url, {Map? headers}) get = get, +}) async { + final response = await get(allLinterRulesUri); + final json = jsonDecode(response.body) as List; + + final dartRules = json + .map((rule) => LinterRule.fromJson(rule as Map)) + .where((rule) => rule.state != LinterRuleState.removed) + .where((rule) => !rule.sinceDartSdk.contains('wip')); - 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'); - }); + log('Fetched ${dartRules.length} Dart linter rules'); + log(''); - return data.map((data) { - final rule = data as Map; - return rule['name'] as String; - }); + return dartRules + .where((rule) => state == null || rule.state == state) + .toList(); } diff --git a/tool/linter_rules/lib/src/all_vga_rules.dart b/tool/linter_rules/lib/src/all_vga_rules.dart index 6271b5a..353700c 100644 --- a/tool/linter_rules/lib/src/all_vga_rules.dart +++ b/tool/linter_rules/lib/src/all_vga_rules.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; @@ -29,10 +30,11 @@ String _analysisOptionsFileName({required String version}) => /// Throws an [ArgumentError] if the [version] is not found. Future> allVeryGoodAnalysisRules({ required String version, + @visibleForTesting String? filePath, }) async { final analysisOptionsFile = File( path.join( - _allVeryGoodAnalysisOptionsDirectoryPath, + filePath ?? _allVeryGoodAnalysisOptionsDirectoryPath, _analysisOptionsFileName(version: version), ), ); diff --git a/tool/linter_rules/lib/src/latest_vga_version.dart b/tool/linter_rules/lib/src/latest_vga_version.dart index 33ccd33..d959cb5 100644 --- a/tool/linter_rules/lib/src/latest_vga_version.dart +++ b/tool/linter_rules/lib/src/latest_vga_version.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; /// The file path containing the latest Very Good Analysis options version. @@ -15,8 +16,10 @@ final String _latestVeryGoodAnalaysisFilePath = path.joinAll([ /// Returns the latest Very Good Analysis version from the analysis options /// file. -String latestVgaVersion() { - final analysisOptionsFile = File(_latestVeryGoodAnalaysisFilePath); +String latestVgaVersion({@visibleForTesting String? filePath}) { + final analysisOptionsFile = File( + filePath ?? _latestVeryGoodAnalaysisFilePath, + ); if (!analysisOptionsFile.existsSync()) { throw ArgumentError( diff --git a/tool/linter_rules/lib/src/linter_rules_reasons.dart b/tool/linter_rules/lib/src/linter_rules_reasons.dart index ded360e..00ab7bb 100644 --- a/tool/linter_rules/lib/src/linter_rules_reasons.dart +++ b/tool/linter_rules/lib/src/linter_rules_reasons.dart @@ -26,7 +26,11 @@ Future readExclusionReasons() async { /// Writes all the reasons for disabling a rule. Future writeExclusionReasons(LinterRulesReasons reasons) async { + // Sort the reasons by rule name to make the output more readable. + final sortedReasons = Map.fromEntries( + reasons.entries.toList()..sort((a, b) => a.key.compareTo(b.key)), + ); final file = File(_reasonsFilePath); - final json = const JsonEncoder.withIndent(' ').convert(reasons); + final json = const JsonEncoder.withIndent(' ').convert(sortedReasons); await file.writeAsString(json); } diff --git a/tool/linter_rules/lib/src/readme.dart b/tool/linter_rules/lib/src/readme.dart index 0aa8d68..794e921 100644 --- a/tool/linter_rules/lib/src/readme.dart +++ b/tool/linter_rules/lib/src/readme.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:linter_rules/linter_rules.dart'; import 'package:path/path.dart' as path; /// A tag indicates the start and end of a section in the README. @@ -41,4 +42,22 @@ class Readme { await _readmeFile.writeAsString(newReadmeContent); } + + /// Generates a markdown table of the excluded rules. + String generateExcludedRulesTable( + Iterable excludedRules, + Map exclusionReasons, + ) { + // Sort the excluded rules to ensure a consistent order. + final sortedExcludedRules = excludedRules.toList()..sort(); + final markdownTable = generateMarkdownTable([ + ['Rule', 'Reason'], + ...sortedExcludedRules.map((rule) { + final ruleMarkdownLink = '[`$rule`](${linterRuleLink(rule)})'; + return [ruleMarkdownLink, exclusionReasons[rule]!]; + }), + ]); + + return markdownTable; + } } diff --git a/tool/linter_rules/lib/src/shared.dart b/tool/linter_rules/lib/src/shared.dart new file mode 100644 index 0000000..b2c3649 --- /dev/null +++ b/tool/linter_rules/lib/src/shared.dart @@ -0,0 +1,16 @@ +/// The [Uri] to fetch all linter rules from. +final Uri allLinterRulesUri = Uri.parse( + 'https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/tool/machine/rules.json', +); + +/// Returns the link to the documentation for the given linter [rule]. +String linterRuleLink(String rule) { + return 'https://dart.dev/tools/linter-rules/$rule'; +} + +/// The tag delimiting the start and end of the excluded rules table in the +/// README.md file. +const excludedRulesTableTag = ( + '', + '', +); diff --git a/tool/linter_rules/pubspec.yaml b/tool/linter_rules/pubspec.yaml index cc6f7cf..76378dc 100644 --- a/tool/linter_rules/pubspec.yaml +++ b/tool/linter_rules/pubspec.yaml @@ -8,11 +8,14 @@ environment: dependencies: args: ^2.6.0 + bump_version: + path: ../bump_version collection: ^1.19.1 http: ^1.2.1 meta: ^1.16.0 path: ^1.9.0 yaml: ^3.1.2 + yaml_edit: ^2.2.2 dev_dependencies: test: ^1.25.14 diff --git a/tool/linter_rules/test/src/all_linter_rules_test.dart b/tool/linter_rules/test/src/all_linter_rules_test.dart new file mode 100644 index 0000000..7f1e526 --- /dev/null +++ b/tool/linter_rules/test/src/all_linter_rules_test.dart @@ -0,0 +1,89 @@ +import 'package:http/http.dart'; +import 'package:linter_rules/linter_rules.dart'; +import 'package:test/test.dart'; + +// Create a minimal fixture +const minimalFixture = r""" +[ + { + "name": "always_declare_return_types", + "description": "Declare method return types.", + "categories": [ + "style" + ], + "state": "stable", + "incompatible": [], + "sets": [], + "fixStatus": "hasFix", + "details": "**DO** declare method return types.\n\nWhen declaring a method or function *always* specify a return type.\nDeclaring return types for functions helps improve your codebase by allowing the\nanalyzer to more adequately check your code for errors that could occur during\nruntime.\n\n**BAD:**\n```dart\nmain() { }\n\n_bar() => _Foo();\n\nclass _Foo {\n _foo() => 42;\n}\n```\n\n**GOOD:**\n```dart\nvoid main() { }\n\n_Foo _bar() => _Foo();\n\nclass _Foo {\n int _foo() => 42;\n}\n\ntypedef predicate = bool Function(Object o);\n```", + "sinceDartSdk": "2.0" + }, + { + "name": "always_put_control_body_on_new_line", + "description": "Separate the control structure expression from its statement.", + "categories": [ + "errorProne", + "style" + ], + "state": "deprecated", + "incompatible": [], + "sets": [], + "fixStatus": "hasFix", + "details": "From the [style guide for the flutter repo](https://flutter.dev/style-guide/):\n\n**DO** separate the control structure expression from its statement.\n\nDon't put the statement part of an `if`, `for`, `while`, `do` on the same line\nas the expression, even if it is short. Doing so makes it unclear that there\nis relevant code there. This is especially important for early returns.\n\n**BAD:**\n```dart\nif (notReady) return;\n\nif (notReady)\n return;\nelse print('ok')\n\nwhile (condition) i += 1;\n```\n\n**GOOD:**\n```dart\nif (notReady)\n return;\n\nif (notReady)\n return;\nelse\n print('ok')\n\nwhile (condition)\n i += 1;\n```\n\nNote that this rule can conflict with the\n[Dart formatter](https://dart.dev/tools/dart-format), and should not be enabled\nwhen the Dart formatter is used.", + "sinceDartSdk": "2.0" + }, + { + "name": "always_put_required_named_parameters_first", + "description": "Put required named parameters first.", + "categories": [ + "style" + ], + "state": "removed", + "incompatible": [], + "sets": [], + "fixStatus": "hasFix", + "details": "**DO** specify `required` on named parameter before other named parameters.\n\n**BAD:**\n```dart\nm({b, c, required a}) ;\n```\n\n**GOOD:**\n```dart\nm({required a, b, c}) ;\n```\n\n**BAD:**\n```dart\nm({b, c, @required a}) ;\n```\n\n**GOOD:**\n```dart\nm({@required a, b, c}) ;\n```", + "sinceDartSdk": "2.0" + }, + { + "name": "always_require_non_null_named_parameters", + "description": "Specify `@required` on named parameters without defaults.", + "categories": [], + "state": "stable", + "incompatible": [], + "sets": [], + "fixStatus": "noFix", + "details": "NOTE: This rule is removed in Dart 3.3.0; it is no longer functional.\n\n**DO** specify `@required` on named parameters without a default value on which\nan `assert(param != null)` is done.\n\n**BAD:**\n```dart\nm1({a}) {\n assert(a != null);\n}\n```\n\n**GOOD:**\n```dart\nm1({@required a}) {\n assert(a != null);\n}\n\nm2({a: 1}) {\n assert(a != null);\n}\n```\n\nNOTE: Only asserts at the start of the bodies will be taken into account.", + "sinceDartSdk": "wip" + } +] +"""; + +void main() { + group('allLinterRules', () { + test('returns all linter rules non-removed or wip', () async { + final linterRules = await allLinterRules( + get: (url, {headers}) async => Response( + minimalFixture, + 200, + headers: headers ?? {}, + ), + ); + + expect(linterRules.length, 2); + }); + + test('filters rules correctly', () async { + final linterRules = await allLinterRules( + state: LinterRuleState.deprecated, + get: (url, {headers}) async => Response( + minimalFixture, + 200, + headers: headers ?? {}, + ), + ); + + expect(linterRules.length, 1); + }); + }); +} diff --git a/tool/linter_rules/test/src/all_vga_rules_test.dart b/tool/linter_rules/test/src/all_vga_rules_test.dart new file mode 100644 index 0000000..1331f13 --- /dev/null +++ b/tool/linter_rules/test/src/all_vga_rules_test.dart @@ -0,0 +1,32 @@ +import 'package:linter_rules/linter_rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('allVeryGoodAnalysisRules', () { + test('returns all very good analysis rules', () async { + final rules = await allVeryGoodAnalysisRules( + filePath: 'test/test_data', + version: '9.0.0', + ); + + expect(rules, [ + 'always_declare_return_types', + 'always_put_required_named_parameters_first', + 'always_use_package_imports', + 'annotate_overrides', + 'avoid_bool_literals_in_conditional_expressions', + 'avoid_catches_without_on_clauses', + 'avoid_catching_errors', + 'avoid_double_and_int_checks', + 'avoid_dynamic_calls', + ]); + }); + + test('throws $ArgumentError if the version is not found', () async { + expect( + () => allVeryGoodAnalysisRules(version: 'invalid'), + throwsA(isA()), + ); + }); + }); +} diff --git a/tool/linter_rules/test/src/latest_vga_version_test.dart b/tool/linter_rules/test/src/latest_vga_version_test.dart new file mode 100644 index 0000000..9f59d93 --- /dev/null +++ b/tool/linter_rules/test/src/latest_vga_version_test.dart @@ -0,0 +1,32 @@ +import 'package:linter_rules/linter_rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('latestVgaVersion', () { + test('returns the latest very good analysis version', () { + final version = latestVgaVersion(); + + expect(version, equals('9.0.0')); + }); + + test('throws $ArgumentError if the file is not found', () { + expect( + () => latestVgaVersion(filePath: 'invalid'), + throwsA(isA()), + ); + }); + + test( + 'throws $ArgumentError if the version is not found in ' + 'the given file path', + () { + expect( + () => latestVgaVersion( + filePath: 'test/test_data/corrupted_analysis_options.yaml', + ), + throwsA(isA()), + ); + }, + ); + }); +} diff --git a/tool/linter_rules/test/src/linter_rules_reasons_test.dart b/tool/linter_rules/test/src/linter_rules_reasons_test.dart new file mode 100644 index 0000000..bd86a53 --- /dev/null +++ b/tool/linter_rules/test/src/linter_rules_reasons_test.dart @@ -0,0 +1,30 @@ +import 'package:linter_rules/linter_rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('linterRulesReasons', () { + test('returns the linter rules reasons', () async { + final reasons = await readExclusionReasons(); + + expect(reasons, isNotEmpty); + expect(reasons, isA>()); + }); + + test('writes the linter rules reasons', () async { + final currentReasons = await readExclusionReasons(); + addTearDown(() async { + await writeExclusionReasons(currentReasons); + }); + + final reasons = { + '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)', + }; + + await writeExclusionReasons(reasons); + + final writtenReasons = await readExclusionReasons(); + expect(writtenReasons, equals(reasons)); + }); + }); +} diff --git a/tool/linter_rules/test/src/markdown_table_generator_test.dart b/tool/linter_rules/test/src/markdown_table_generator_test.dart new file mode 100644 index 0000000..1adf90f --- /dev/null +++ b/tool/linter_rules/test/src/markdown_table_generator_test.dart @@ -0,0 +1,24 @@ +import 'package:linter_rules/linter_rules.dart'; +import 'package:test/test.dart'; + +void main() { + group('MarkdownTableGenerator', () { + test('generates a markdown table', () { + final markdown = generateMarkdownTable([ + ['Header 1', 'Header 2'], + ['Row 1, Cell 1', 'Row 1, Cell 2'], + ['Row 2, Cell 1', 'Row 2, Cell 2'], + ]); + + expect( + markdown, + equals(''' +| Header 1 | Header 2 | +| --- | --- | +| Row 1, Cell 1 | Row 1, Cell 2 | +| Row 2, Cell 1 | Row 2, Cell 2 | +'''), + ); + }); + }); +} diff --git a/tool/linter_rules/test/src/models/fixtures/all_linter_rules.dart b/tool/linter_rules/test/src/models/fixtures/all_linter_rules.dart index 7838889..49e7b79 100644 --- a/tool/linter_rules/test/src/models/fixtures/all_linter_rules.dart +++ b/tool/linter_rules/test/src/models/fixtures/all_linter_rules.dart @@ -1,8 +1,8 @@ /// The fixture for the `all_linter_rules.json` file. /// -/// The content is a copy from the [all_linter_rules.json file](https://raw.githubusercontent.com/dart-lang/site-www/17ccab9e54d0166753c088651a98a5b6e78c1078/src/_data/linter_rules.json). +/// The content is a copy from the [all_linter_rules.json file](https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/tool/machine/rules.json). /// -/// Yo may find the latest file within the [Dart Language Repository](https://github.com/dart-lang/site-www/blob/17ccab9e54d0166753c088651a98a5b6e78c1078/src/_data/linter_rules.json#L1764). +/// You may find the latest file within the [Dart Language Repository](https://raw.githubusercontent.com/dart-lang/sdk/main/pkg/linter/tool/machine/rules.json). const allLinterRulesFixute = r""" [ { diff --git a/tool/linter_rules/test/src/models/linter_rule_test.dart b/tool/linter_rules/test/src/models/linter_rule_test.dart index b6564d3..27ca1a3 100644 --- a/tool/linter_rules/test/src/models/linter_rule_test.dart +++ b/tool/linter_rules/test/src/models/linter_rule_test.dart @@ -24,6 +24,44 @@ void main() { ); } }); + + test('throws an exception if the fixStatus is invalid', () { + const invalidRule = { + 'name': 'avoid_null_checks_in_equality_operators', + 'description': "Don't check for `null` in custom `==` operators.", + 'categories': ['style'], + 'state': 'stable', + 'fixStatus': 'invalid', + 'incompatible': [], + 'sets': [], + 'details': 'Invalid rule due to wrong state', + 'sinceDartSdk': '2.0', + }; + + expect( + () => LinterRule.fromJson(invalidRule as Map), + throwsA(isA()), + ); + }); + + test('throws an exception if the state is invalid', () { + const invalidRule = { + 'name': 'avoid_null_checks_in_equality_operators', + 'description': "Don't check for `null` in custom `==` operators.", + 'categories': ['style'], + 'state': 'invalid', + 'fixStatus': 'noFix', + 'incompatible': [], + 'sets': [], + 'details': 'Invalid rule due to wrong state', + 'sinceDartSdk': '2.0', + }; + + expect( + () => LinterRule.fromJson(invalidRule as Map), + throwsA(isA()), + ); + }); }); }); } diff --git a/tool/linter_rules/test/src/readme_test.dart b/tool/linter_rules/test/src/readme_test.dart new file mode 100644 index 0000000..8e2a620 --- /dev/null +++ b/tool/linter_rules/test/src/readme_test.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:linter_rules/linter_rules.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +void main() { + group('readme', () { + test('updates the tag content', () async { + final readmePath = path.joinAll(['..', '..', 'README.md']); + final currentReadmeContent = await File(readmePath).readAsString(); + addTearDown(() async { + await File(readmePath).writeAsString(currentReadmeContent); + }); + + final readme = Readme(); + + const content = 'Test content'; + await readme.updateTagContent(excludedRulesTableTag, content); + final readmeContent = await File(readmePath).readAsString(); + + expect( + readmeContent, + contains( + '''Test content''', + ), + ); + }); + + test('throws StateError if the tag is not found', () async { + final readme = Readme(); + await expectLater( + () => readme.updateTagContent(('invalid', 'invalid'), 'Test content'), + throwsA(isA()), + ); + }); + + test('generates a markdown table of the excluded rules', () async { + final excludedRules = ['always_put_control_body_on_new_line']; + final exclusionReasons = { + '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)', + }; + + final markdownTable = Readme().generateExcludedRulesTable( + excludedRules, + exclusionReasons, + ); + + expect( + markdownTable, + equals(''' +| 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) | +'''), + ); + }); + }); +} diff --git a/tool/linter_rules/test/test_data/analysis_options.9.0.0.yaml b/tool/linter_rules/test/test_data/analysis_options.9.0.0.yaml new file mode 100644 index 0000000..69fbf16 --- /dev/null +++ b/tool/linter_rules/test/test_data/analysis_options.9.0.0.yaml @@ -0,0 +1,33 @@ +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + + errors: + close_sinks: ignore + missing_required_param: error + missing_return: error + record_literal_one_positional_no_trailing_comma: error + collection_methods_unrelated_type: warning + unrelated_type_equality_checks: warning + + exclude: + - test/.test_coverage.dart + - lib/generated_plugin_registrant.dart + +formatter: + trailing_commas: preserve + +linter: + rules: + - always_declare_return_types + - always_put_required_named_parameters_first + - always_use_package_imports + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_double_and_int_checks + - avoid_dynamic_calls + diff --git a/tool/linter_rules/test/test_data/corrupted_analysis_options.yaml b/tool/linter_rules/test/test_data/corrupted_analysis_options.yaml new file mode 100644 index 0000000..311906e --- /dev/null +++ b/tool/linter_rules/test/test_data/corrupted_analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.9.0.yaml