diff --git a/README.md b/README.md index a10ee05c..0873cc1f 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,19 @@ custom_lint: some_parameter: "some value" ``` +#### Overriding lint error severities + +You can also override the severity of lint rules in the `analysis_options.yaml` file. +This allows you to change INFO level lints to WARNING or ERROR, or vice versa: + +```yaml +custom_lint: + errors: + my_lint_rule: error +``` + +The available severity levels are: `error`, `warning`, `info`, and `ignore`. + ### Obtaining the list of lints in the CI Unfortunately, running `dart analyze` does not pick up our newly defined lints. diff --git a/packages/custom_lint/example/analysis_options.yaml b/packages/custom_lint/example/analysis_options.yaml index afa5cfd1..ab6f2710 100644 --- a/packages/custom_lint/example/analysis_options.yaml +++ b/packages/custom_lint/example/analysis_options.yaml @@ -8,4 +8,4 @@ linter: rules: public_member_api_docs: false avoid_print: false - unreachable_from_main: false + unreachable_from_main: false \ No newline at end of file diff --git a/packages/custom_lint/test/analysis_options_test.dart b/packages/custom_lint/test/analysis_options_test.dart new file mode 100644 index 00000000..6dcf2868 --- /dev/null +++ b/packages/custom_lint/test/analysis_options_test.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/error/error.dart'; +import 'package:test/test.dart'; + +import 'cli_process_test.dart'; +import 'create_project.dart'; +import 'peer_project_meta.dart'; + +void main() { + group('Errors severities override', () { + Future runProcess(String workingDirectoryPath) async => + Process.run( + 'dart', + [customLintBinPath], + workingDirectory: workingDirectoryPath, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); + + Directory createLintUsageWith({ + required Uri pluginUri, + required String analysisOptions, + }) => + createLintUsage( + name: 'test_app', + source: {'lib/main.dart': 'void fn() {}'}, + plugins: {'test_lint': pluginUri}, + analysisOptions: analysisOptions, + ); + + Directory createTestPlugin({ + ErrorSeverity errorSeverity = ErrorSeverity.INFO, + }) => + createPlugin( + name: 'test_lint', + main: createPluginSource([ + TestLintRule( + code: 'test_lint', + message: 'Test lint message', + errorSeverity: errorSeverity, + ), + ]), + ); + test('correctly applies error severity from analysis_options.yaml', + () async { + final plugin = createTestPlugin(); + + final app = createLintUsageWith( + pluginUri: plugin.uri, + analysisOptions: ''' +custom_lint: + errors: + test_lint: error +''', + ); + + final process = await runProcess(app.path); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Test lint message • test_lint • ERROR + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + + test('correctly applies warning severity from analysis_options.yaml', + () async { + final plugin = createTestPlugin(); + + final app = createLintUsageWith( + pluginUri: plugin.uri, + analysisOptions: ''' +custom_lint: + errors: + test_lint: warning +''', + ); + + final process = await runProcess(app.path); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Test lint message • test_lint • WARNING + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + + test('correctly applies info severity from analysis_options.yaml', + () async { + final plugin = createTestPlugin(); + + final app = createLintUsageWith( + pluginUri: plugin.uri, + analysisOptions: ''' +custom_lint: + errors: + test_lint: info +''', + ); + + final process = await runProcess(app.path); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + + lib/main.dart:1:6 • Test lint message • test_lint • INFO + +1 issue found. +'''); + expect(process.exitCode, 1); + }); + + test('correctly applies ignore severity from analysis_options.yaml', + () async { + final plugin = createTestPlugin(); + + final app = createLintUsageWith( + pluginUri: plugin.uri, + analysisOptions: ''' +custom_lint: + errors: + test_lint: ignore +''', + ); + + final process = await runProcess(app.path); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + }); +} diff --git a/packages/custom_lint/test/create_project.dart b/packages/custom_lint/test/create_project.dart index f1478097..49c48af0 100644 --- a/packages/custom_lint/test/create_project.dart +++ b/packages/custom_lint/test/create_project.dart @@ -219,6 +219,7 @@ Directory createLintUsage({ Directory? parent, Map plugins = const {}, Map source = const {}, + String? analysisOptions, Map extraPackageConfig = const {}, bool installAsDevDependency = true, required String name, @@ -239,6 +240,7 @@ analyzer: plugins: - custom_lint +${analysisOptions ?? ''} ''', pubspec: ''' name: $name diff --git a/packages/custom_lint_builder/lib/src/client.dart b/packages/custom_lint_builder/lib/src/client.dart index 74de8a86..39e8508f 100644 --- a/packages/custom_lint_builder/lib/src/client.dart +++ b/packages/custom_lint_builder/lib/src/client.dart @@ -991,11 +991,27 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { CustomLintEvent.analyzerPluginNotification( analyzer_plugin.AnalysisErrorsParams( path, - CustomAnalyzerConverter().convertAnalysisErrors( - allAnalysisErrors, - lineInfo: resolver.lineInfo, - options: analysisContext.getAnalysisOptionsForFile(file), - ), + CustomAnalyzerConverter() + .convertAnalysisErrors( + allAnalysisErrors, + lineInfo: resolver.lineInfo, + options: analysisContext.getAnalysisOptionsForFile(file), + ) + // Filter out lints with severity: ignore + .whereNot( + (error) => + configs.configs.errors[error.code] == ErrorSeverity.NONE, + ) + // Override severities from analysis_options.yaml + .map((error) { + var severity = error.severity; + if (configs.configs.errors[error.code] + case final ErrorSeverity errorSeverity) { + severity = CustomAnalyzerConverter() + .convertErrorSeverity(errorSeverity); + } + return error.copyWith(severity: severity); + }).toList(), ).toNotification(), ), ); @@ -1240,3 +1256,29 @@ extension on ChangeReporterBuilder { ); } } + +extension on analyzer_plugin.AnalysisError { + analyzer_plugin.AnalysisError copyWith({ + analyzer_plugin.AnalysisErrorSeverity? severity, + String? correction, + analyzer_plugin.Location? location, + String? message, + analyzer_plugin.AnalysisErrorType? type, + String? code, + String? url, + List? contextMessages, + bool? hasFix, + }) { + return analyzer_plugin.AnalysisError( + severity ?? this.severity, + type ?? this.type, + location ?? this.location, + message ?? this.message, + code ?? this.code, + correction: correction ?? this.correction, + url: url ?? this.url, + contextMessages: contextMessages ?? this.contextMessages, + hasFix: hasFix ?? this.hasFix, + ); + } +} diff --git a/packages/custom_lint_core/lib/src/configs.dart b/packages/custom_lint_core/lib/src/configs.dart index 31609c76..af2216e8 100644 --- a/packages/custom_lint_core/lib/src/configs.dart +++ b/packages/custom_lint_core/lib/src/configs.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/error/error.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; @@ -17,6 +18,7 @@ class CustomLintConfigs { required this.verbose, required this.debug, required this.rules, + required this.errors, }); /// Decode a [CustomLintConfigs] from a file. @@ -108,11 +110,30 @@ class CustomLintConfigs { } } + final errors = {...includedOptions.errors}; + + if (customLint['errors'] case final YamlMap errorsYaml) { + for (final entry in errorsYaml.entries) { + if (entry.key case final String key) { + errors[key] = switch (entry.value) { + 'info' => ErrorSeverity.INFO, + 'warning' => ErrorSeverity.WARNING, + 'error' => ErrorSeverity.ERROR, + 'ignore' => ErrorSeverity.NONE, + _ => throw UnsupportedError( + 'Unsupported severity ${entry.value} for key: ${entry.key}', + ), + }; + } + } + } + return CustomLintConfigs( enableAllLintRules: enableAllLintRules, verbose: verbose, debug: debug, rules: UnmodifiableMapView(rules), + errors: UnmodifiableMapView(errors), ); } @@ -123,6 +144,7 @@ class CustomLintConfigs { verbose: false, debug: false, rules: {}, + errors: {}, ); /// A field representing whether to enable/disable lint rules that are not @@ -147,13 +169,18 @@ class CustomLintConfigs { /// Whether enable hot-reload and log the VM-service URI. final bool debug; + /// A map of lint rules to their severity. This is used to override the severity + /// of a lint rule for a specific lint. + final Map errors; + @override bool operator ==(Object other) => other is CustomLintConfigs && other.enableAllLintRules == enableAllLintRules && other.verbose == verbose && other.debug == debug && - const MapEquality().equals(other.rules, rules); + const MapEquality().equals(other.rules, rules) && + const MapEquality().equals(other.errors, errors); @override int get hashCode => Object.hash( @@ -161,6 +188,7 @@ class CustomLintConfigs { verbose, debug, const MapEquality().hash(rules), + const MapEquality().hash(errors), ); } diff --git a/packages/custom_lint_core/test/configs_test.dart b/packages/custom_lint_core/test/configs_test.dart index b6a2ae63..8d73bf28 100644 --- a/packages/custom_lint_core/test/configs_test.dart +++ b/packages/custom_lint_core/test/configs_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io' as io; +import 'package:analyzer/error/error.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:custom_lint_core/custom_lint_core.dart'; @@ -115,6 +116,7 @@ custom_lint: test('Empty config', () { expect(CustomLintConfigs.empty.enableAllLintRules, null); expect(CustomLintConfigs.empty.rules, isEmpty); + expect(CustomLintConfigs.empty.errors, isEmpty); }); group('parse', () { @@ -188,6 +190,25 @@ custom_lint: ); }); + test('has an immutable map of errors', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + a: error +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect( + configs.errors, + {'a': ErrorSeverity.ERROR}, + ); + + expect( + () => configs.errors['a'] = ErrorSeverity.INFO, + throwsUnsupportedError, + ); + }); + test( 'if custom_lint is present and defines some properties, merges with "include"', () { @@ -338,6 +359,67 @@ foo: }); }); + test('Parses error severities from configs', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: error + rule_name_2: warning + rule_name_3: info + rule_name_4: ignore +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.errors, { + 'rule_name_1': ErrorSeverity.ERROR, + 'rule_name_2': ErrorSeverity.WARNING, + 'rule_name_3': ErrorSeverity.INFO, + 'rule_name_4': ErrorSeverity.NONE, + }); + }); + + test('Handles unknown error severity values', () { + final analysisOptions = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: invalid_severity +'''); + expect( + () => CustomLintConfigs.parse(analysisOptions, packageConfig), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Unsupported severity invalid_severity for key: rule_name_1', + ), + ), + ); + }); + + test('Merges error severities from included config file', () { + final includedFile = createAnalysisOptions(''' +custom_lint: + errors: + rule_name_1: error + rule_name_2: warning +'''); + + final analysisOptions = createAnalysisOptions(''' +include: ${includedFile.path} +custom_lint: + errors: + rule_name_2: info + rule_name_3: error +'''); + final configs = CustomLintConfigs.parse(analysisOptions, packageConfig); + + expect(configs.errors, { + 'rule_name_1': ErrorSeverity.ERROR, + 'rule_name_2': ErrorSeverity.INFO, + 'rule_name_3': ErrorSeverity.ERROR, + }); + }); + group('package config', () { test('single package', () async { final dir = createDir(); @@ -353,7 +435,6 @@ foo: test('workspace', () async { final dir = createDir(); - final projectPath = await createTempProject( tempDirPath: dir.path, projectName: testPackageName,