Skip to content

feat: automate deprecated lint rules #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ jobs:
git config user.name VGV Bot
git config user.email [email protected]

- 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/[email protected]
with:
Expand All @@ -54,3 +54,27 @@ jobs:
author: VGV Bot <[email protected]>
assignees: vgvbot
committer: VGV Bot <[email protected]>

- 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/[email protected]
with:
base: main
branch: chore/remove-deprecated-rules
commit-message: "fix: remove deprecated rules"
title: "fix: remove deprecated rules"
body: |
There are now rules deprecated that require an update to the Very Good Analysis.
labels: bot
author: VGV Bot <[email protected]>
assignees: vgvbot
committer: VGV Bot <[email protected]>

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/// This script will:
/// - Copy the most recent yaml to a new one with the new desired version.
/// - Include that file on the main yaml file `lib/analysis_options.yaml.
/// - Bump the version of the Very Good Analysis package in the pubspec.yaml.
///
/// ## Usage
///
Expand Down Expand Up @@ -31,24 +32,41 @@ final _latestAnalysisVersionRegExp = RegExp(
r'analysis_options\.(\d+\.\d+\.\d+)\.yaml',
);

void main(List<String> args) {
final analysisOptionsFile = File('lib/analysis_options.yaml');
void main(List<String> 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(
_latestAnalysisVersionRegExp,
'analysis_options.$newVersion.yaml',
);
analysisOptionsFile.writeAsStringSync(newContent);

final pubspecFile = File(
'${basePath}pubspec.yaml',
);
final pubspecContent = pubspecFile.readAsStringSync();
final newPubspecContent = pubspecContent.replaceFirst(
'version: $latestVersion',
'version: $newVersion',
);

pubspecFile.writeAsStringSync(newPubspecContent);
}
8 changes: 8 additions & 0 deletions tool/bump_version/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

46 changes: 28 additions & 18 deletions tool/linter_rules/bin/analyze.dart
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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
Expand All @@ -37,24 +35,32 @@ Future<void> main(
List<String> 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<dynamic>;
final parsedArgs = argsParser.parse(args);

final dartRules = json
.map((rule) => LinterRule.fromJson(rule as Map<String, dynamic>))
.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();
Expand All @@ -65,4 +71,8 @@ Future<void> main(
deprecationMessage.write('\n - $rule');
}
log(deprecationMessage.toString());

if (deprecatedVgaRules.isNotEmpty && setExitIfChanged) {
exit(2);
}
}
117 changes: 117 additions & 0 deletions tool/linter_rules/bin/remove_deprecated_rules.dart
Original file line number Diff line number Diff line change
@@ -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, bump the version
/// in the pubspec.yaml, and update the exclusion reasons file and the
/// table of excluded rules in the README.md file.
Future<void> 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<String> 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());
}
29 changes: 7 additions & 22 deletions tool/linter_rules/lib/exclusion_reason_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
'<!-- start:excluded_rules_table -->',
'<!-- end:excluded_rules_table -->',
);

/// 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.
Expand Down Expand Up @@ -75,7 +63,7 @@ Future<void> 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(
Expand Down Expand Up @@ -103,16 +91,13 @@ Future<void> 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.''');

Expand Down
1 change: 1 addition & 0 deletions tool/linter_rules/lib/linter_rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading