Skip to content

feat: add support for configuration error severities in analysis_options.yaml #326

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 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ae42d24
feat: add readme for errors severities
Artur-Wisniewski Mar 27, 2025
53f2a59
feat: add config severities
Artur-Wisniewski Mar 27, 2025
3c11288
feat: add config severities tests
Artur-Wisniewski Mar 27, 2025
1595a84
fix: error connected to parsing none error severity that is not prese…
Artur-Wisniewski Mar 28, 2025
2fba63c
feat: add analysis options tests related to errors severities
Artur-Wisniewski Mar 28, 2025
3f3a6a1
fix: custom analyzer converter none severity case
Artur-Wisniewski Mar 28, 2025
4f11ecc
refactor: change server errors from var to final in custom analyzer c…
Artur-Wisniewski Mar 28, 2025
135070a
refactor: customer analyzer converter
Artur-Wisniewski Mar 28, 2025
374e367
feat: add errors severities to analysis options inside custom_lint ex…
Artur-Wisniewski Mar 28, 2025
c398b9d
refactor: remove test that is already covered
Artur-Wisniewski Mar 30, 2025
04b7f96
Apply suggestions from code review
rrousselGit Jun 1, 2025
774d137
Apply suggestions from code review
rrousselGit Jun 1, 2025
314d5db
fix: remove unnecessary custom_lint error severities from example con…
Artur-Wisniewski Jul 20, 2025
981698a
refactor: CustomLintConfigs getting error severities
Artur-Wisniewski Jul 20, 2025
bd5ecbf
refactor: change error severity from none to ignore
Artur-Wisniewski Jul 20, 2025
5f9d12e
refactor: revert code in custom_analyzer_converter and move logic to …
Artur-Wisniewski Jul 20, 2025
cc61df7
Merge branch 'main' into feat/errors-severities-config
Artur-Wisniewski Jul 20, 2025
1908992
refactor: pr for code review
Artur-Wisniewski Jul 20, 2025
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/custom_lint/example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ linter:
rules:
public_member_api_docs: false
avoid_print: false
unreachable_from_main: false
unreachable_from_main: false
148 changes: 148 additions & 0 deletions packages/custom_lint/test/analysis_options_test.dart
Original file line number Diff line number Diff line change
@@ -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<ProcessResult> 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);
});
});
}
2 changes: 2 additions & 0 deletions packages/custom_lint/test/create_project.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ Directory createLintUsage({
Directory? parent,
Map<String, Uri> plugins = const {},
Map<String, String> source = const {},
String? analysisOptions,
Map<String, Uri> extraPackageConfig = const {},
bool installAsDevDependency = true,
required String name,
Expand All @@ -239,6 +240,7 @@ analyzer:
plugins:
- custom_lint

${analysisOptions ?? ''}
''',
pubspec: '''
name: $name
Expand Down
52 changes: 47 additions & 5 deletions packages/custom_lint_builder/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
),
);
Expand Down Expand Up @@ -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<analyzer_plugin.DiagnosticMessage>? 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,
);
}
}
30 changes: 29 additions & 1 deletion packages/custom_lint_core/lib/src/configs.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,6 +18,7 @@ class CustomLintConfigs {
required this.verbose,
required this.debug,
required this.rules,
required this.errors,
});

/// Decode a [CustomLintConfigs] from a file.
Expand Down Expand Up @@ -108,11 +110,30 @@ class CustomLintConfigs {
}
}

final errors = <String, ErrorSeverity>{...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),
);
}

Expand All @@ -123,6 +144,7 @@ class CustomLintConfigs {
verbose: false,
debug: false,
rules: {},
errors: {},
);

/// A field representing whether to enable/disable lint rules that are not
Expand All @@ -147,20 +169,26 @@ 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<String, ErrorSeverity> errors;

@override
bool operator ==(Object other) =>
other is CustomLintConfigs &&
other.enableAllLintRules == enableAllLintRules &&
other.verbose == verbose &&
other.debug == debug &&
const MapEquality<String, LintOptions>().equals(other.rules, rules);
const MapEquality<String, LintOptions>().equals(other.rules, rules) &&
const MapEquality<String, ErrorSeverity>().equals(other.errors, errors);

@override
int get hashCode => Object.hash(
enableAllLintRules,
verbose,
debug,
const MapEquality<String, LintOptions>().hash(rules),
const MapEquality<String, ErrorSeverity>().hash(errors),
);
}

Expand Down
Loading