Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit b12807d

Browse files
authored
feat: add validation for rule names in analysis_options (#1059)
1 parent 9d28572 commit b12807d

20 files changed

+234
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* feat: **Breaking change** cleanup public API.
1515
* chore: restrict `analyzer` version to `>=5.1.0 <5.3.0`.
1616
* feat: add `print-config` option to all commands.
17+
* feat: add validation for rule names in `analysis_options.yaml` both for the `analyze` command and the plugin.
1718
* feat: support `includes` in the rules config.
1819
* fix: ignore `@override` methods for [`avoid-redundant-async`](https://dartcodemetrics.dev/docs/rules/common/avoid-redundant-async).
1920

example/example.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Future<void> main() async {
2424
},
2525
antiPatterns: {'long-method': {}},
2626
shouldPrintConfig: false,
27+
analysisOptionsPath: '',
2728
);
2829

2930
const analyzer = LintAnalyzer();

lib/src/analyzer_plugin/analyzer_plugin.dart

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:analyzer_plugin/plugin/plugin.dart';
88
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
99

1010
import '../analyzers/lint_analyzer/lint_analysis_config.dart';
11+
import '../analyzers/lint_analyzer/lint_analysis_options_validator.dart';
1112
import '../analyzers/lint_analyzer/lint_analyzer.dart';
1213
import '../analyzers/lint_analyzer/metrics/metrics_list/number_of_parameters_metric.dart';
1314
import '../analyzers/lint_analyzer/metrics/metrics_list/source_lines_of_code/source_lines_of_code_metric.dart';
@@ -28,7 +29,7 @@ class AnalyzerPlugin extends ServerPlugin {
2829
'https://github.com/dart-code-checker/dart-code-metrics/issues';
2930

3031
@override
31-
List<String> get fileGlobsToAnalyze => const ['*.dart'];
32+
List<String> get fileGlobsToAnalyze => const ['*.dart', '*.yaml'];
3233

3334
@override
3435
String get name => 'Dart Code Metrics $packageVersion';
@@ -62,15 +63,21 @@ class AnalyzerPlugin extends ServerPlugin {
6263
return;
6364
}
6465

66+
final rootPath = analysisContext.contextRoot.root.path;
67+
if (path.endsWith('analysis_options.yaml')) {
68+
final config = _configs[rootPath];
69+
if (config != null) {
70+
_validateAnalysisOptions(config, rootPath);
71+
}
72+
}
73+
6574
try {
6675
final resolvedUnit =
6776
await analysisContext.currentSession.getResolvedUnit(path);
6877

6978
if (resolvedUnit is ResolvedUnitResult) {
70-
final analysisErrors = _getErrorsForResolvedUnit(
71-
resolvedUnit,
72-
analysisContext.contextRoot.root.path,
73-
);
79+
final analysisErrors =
80+
_getErrorsForResolvedUnit(resolvedUnit, rootPath);
7481

7582
channel.sendNotification(
7683
plugin.AnalysisErrorsParams(
@@ -181,6 +188,31 @@ class AnalyzerPlugin extends ServerPlugin {
181188
);
182189

183190
_configs[rootPath] = lintConfig;
191+
192+
_validateAnalysisOptions(lintConfig, rootPath);
184193
}
185194
}
195+
196+
void _validateAnalysisOptions(LintAnalysisConfig config, String rootPath) {
197+
if (config.analysisOptionsPath == null) {
198+
return;
199+
}
200+
201+
final result = <plugin.AnalysisErrorFixes>[];
202+
203+
final report =
204+
LintAnalysisOptionsValidator.validateOptions(config, rootPath);
205+
if (report != null) {
206+
result.addAll(report.issues.map(
207+
(issue) => codeIssueToAnalysisErrorFixes(issue, null),
208+
));
209+
}
210+
211+
channel.sendNotification(
212+
plugin.AnalysisErrorsParams(
213+
config.analysisOptionsPath!,
214+
result.map((analysisError) => analysisError.error).toList(),
215+
).toNotification(),
216+
);
217+
}
186218
}

lib/src/analyzers/lint_analyzer/lint_analysis_config.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class LintAnalysisConfig {
1717
final Iterable<Glob> metricsExcludes;
1818
final Map<String, Object> metricsConfig;
1919
final String rootFolder;
20+
final String? analysisOptionsPath;
2021

2122
const LintAnalysisConfig(
2223
this.globalExcludes,
@@ -29,6 +30,7 @@ class LintAnalysisConfig {
2930
this.metricsExcludes,
3031
this.metricsConfig,
3132
this.rootFolder,
33+
this.analysisOptionsPath,
3234
);
3335

3436
Map<String, Object?> toJson() => {
@@ -42,5 +44,6 @@ class LintAnalysisConfig {
4244
'file-metrics': fileMetrics.map((metric) => metric.id).toList(),
4345
'metrics-excludes':
4446
metricsExcludes.map((glob) => glob.pattern).toList(),
47+
'analysis-options-path': analysisOptionsPath,
4548
};
4649
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import 'dart:io';
2+
3+
import 'package:collection/collection.dart';
4+
import 'package:path/path.dart';
5+
import 'package:source_span/source_span.dart';
6+
import 'package:yaml/yaml.dart';
7+
8+
import 'lint_analysis_config.dart';
9+
import 'models/issue.dart';
10+
import 'models/lint_file_report.dart';
11+
import 'models/severity.dart';
12+
13+
class LintAnalysisOptionsValidator {
14+
static LintFileReport? validateOptions(
15+
LintAnalysisConfig config,
16+
String rootFolder,
17+
) {
18+
final path = config.analysisOptionsPath;
19+
final file = path != null && File(path).existsSync() ? File(path) : null;
20+
if (file == null) {
21+
return null;
22+
}
23+
24+
final fileContent = file.readAsStringSync();
25+
final node = loadYamlNode(fileContent);
26+
final rulesList = _getRulesList(node);
27+
if (rulesList == null) {
28+
return null;
29+
}
30+
31+
final parsedRuleIds = config.codeRules.map((rule) => rule.id).toList();
32+
final issues = <Issue>[];
33+
34+
for (final rule in rulesList) {
35+
if (!parsedRuleIds.contains(rule.ruleName)) {
36+
issues.add(
37+
Issue(
38+
ruleId: 'unknown-config',
39+
severity: Severity.warning,
40+
message:
41+
"'${rule.ruleName}' is not recognized as a valid rule name.",
42+
documentation: Uri.parse('https://dartcodemetrics.dev/docs/rules'),
43+
location: _copySpanWithOffset(rule.span),
44+
),
45+
);
46+
}
47+
}
48+
49+
if (issues.isNotEmpty) {
50+
final filePath = file.path;
51+
final relativePath = relative(filePath, from: rootFolder);
52+
53+
return LintFileReport.onlyIssues(
54+
path: file.path,
55+
relativePath: relativePath,
56+
issues: issues,
57+
);
58+
}
59+
60+
return null;
61+
}
62+
63+
static List<_RuleWithSpan>? _getRulesList(YamlNode node) {
64+
if (node is YamlMap) {
65+
final rules =
66+
(node['dart_code_metrics'] as YamlMap?)?['rules'] as YamlNode?;
67+
if (rules is YamlList) {
68+
return rules.nodes
69+
// ignore: avoid_types_on_closure_parameters
70+
.map((Object? rule) {
71+
if (rule is YamlMap) {
72+
final key = rule.nodes.keys.first as Object?;
73+
if (key is YamlScalar && key.value is String) {
74+
return _RuleWithSpan(key.value as String, key.span);
75+
}
76+
}
77+
78+
if (rule is YamlScalar && rule.value is String) {
79+
return _RuleWithSpan(rule.value as String, rule.span);
80+
}
81+
82+
return null;
83+
})
84+
.whereNotNull()
85+
.toList();
86+
}
87+
}
88+
89+
return null;
90+
}
91+
92+
static SourceSpan _copySpanWithOffset(SourceSpan span) => SourceSpan(
93+
SourceLocation(
94+
span.start.offset,
95+
sourceUrl: span.start.sourceUrl,
96+
line: span.start.line + 1,
97+
column: span.start.column + 1,
98+
),
99+
SourceLocation(
100+
span.end.offset,
101+
sourceUrl: span.end.sourceUrl,
102+
line: span.end.line + 1,
103+
column: span.end.column + 1,
104+
),
105+
span.text,
106+
);
107+
}
108+
109+
class _RuleWithSpan {
110+
final String ruleName;
111+
final SourceSpan span;
112+
113+
const _RuleWithSpan(this.ruleName, this.span);
114+
}

lib/src/analyzers/lint_analyzer/lint_analyzer.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../../utils/exclude_utils.dart';
1414
import '../../utils/node_utils.dart';
1515
import '../../utils/suppression.dart';
1616
import 'lint_analysis_config.dart';
17+
import 'lint_analysis_options_validator.dart';
1718
import 'lint_config.dart';
1819
import 'metrics/metrics_list/cyclomatic_complexity/cyclomatic_complexity_metric.dart';
1920
import 'metrics/metrics_list/source_lines_of_code/source_lines_of_code_metric.dart';
@@ -85,6 +86,14 @@ class LintAnalyzer {
8586
final lintAnalysisConfig =
8687
_getAnalysisConfig(context, rootFolder, config);
8788

89+
final report = LintAnalysisOptionsValidator.validateOptions(
90+
lintAnalysisConfig,
91+
rootFolder,
92+
);
93+
if (report != null) {
94+
analyzerResult.add(report);
95+
}
96+
8897
if (config.shouldPrintConfig) {
8998
_logger?.printConfig(lintAnalysisConfig.toJson());
9099
}
@@ -244,7 +253,6 @@ class LintAnalyzer {
244253
return LintFileReport.onlyIssues(
245254
path: filePath,
246255
relativePath: relativePath,
247-
file: _getEmptyFileReport(internalResult),
248256
issues: issues,
249257
);
250258
}
@@ -371,14 +379,6 @@ class LintAnalyzer {
371379
);
372380
}
373381

374-
Report _getEmptyFileReport(InternalResolvedUnitResult internalResult) =>
375-
Report(
376-
location:
377-
nodeLocation(node: internalResult.unit, source: internalResult),
378-
metrics: const [],
379-
declaration: internalResult.unit,
380-
);
381-
382382
Map<ScopedFunctionDeclaration, Report> _checkFunctionMetrics(
383383
ScopeVisitor visitor,
384384
InternalResolvedUnitResult source,

lib/src/analyzers/lint_analyzer/lint_config.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class LintConfig {
1212
final Iterable<String> excludeForRulesPatterns;
1313
final Map<String, Map<String, Object>> antiPatterns;
1414
final bool shouldPrintConfig;
15+
final String? analysisOptionsPath;
1516

1617
const LintConfig({
1718
required this.excludePatterns,
@@ -21,6 +22,7 @@ class LintConfig {
2122
required this.excludeForRulesPatterns,
2223
required this.antiPatterns,
2324
required this.shouldPrintConfig,
25+
required this.analysisOptionsPath,
2426
});
2527

2628
/// Creates the config from analysis [options].
@@ -35,6 +37,7 @@ class LintConfig {
3537
antiPatterns:
3638
options.readMapOfMap(['anti-patterns'], packageRelated: true),
3739
shouldPrintConfig: false,
40+
analysisOptionsPath: options.fullPath,
3841
);
3942

4043
/// Creates the config from cli [arguments].
@@ -51,6 +54,7 @@ class LintConfig {
5154
rules: const {},
5255
excludeForRulesPatterns: const [],
5356
antiPatterns: const {},
57+
analysisOptionsPath: null,
5458
);
5559

5660
/// Merges two configs into a single one
@@ -74,5 +78,7 @@ class LintConfig {
7478
antiPatterns:
7579
mergeMaps(defaults: antiPatterns, overrides: overrides.antiPatterns)
7680
.cast<String, Map<String, Object>>(),
81+
analysisOptionsPath:
82+
analysisOptionsPath ?? overrides.analysisOptionsPath,
7783
);
7884
}

lib/src/analyzers/lint_analyzer/models/lint_file_report.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class LintFileReport implements FileReport {
1313
final String relativePath;
1414

1515
/// The metrics report about the target file.
16-
final Report file;
16+
final Report? file;
1717

1818
/// The all classes reports in the target file.
1919
final Map<String, Report> classes;
@@ -40,9 +40,9 @@ class LintFileReport implements FileReport {
4040
const LintFileReport.onlyIssues({
4141
required this.path,
4242
required this.relativePath,
43-
required this.file,
4443
required this.issues,
4544
}) : classes = const {},
4645
functions = const {},
47-
antiPatternCases = const {};
46+
antiPatternCases = const {},
47+
file = null;
4848
}

lib/src/analyzers/lint_analyzer/reporters/reporters_list/checkstyle/lint_checkstyle_reporter.dart

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,20 @@ class LintCheckstyleReporter
6565
}
6666

6767
void _reportMetrics(XmlBuilder builder, LintFileReport record) {
68-
for (final metric in record.file.metrics) {
69-
if (_isMetricNeedToReport(metric.level)) {
70-
builder.element(
71-
'error',
72-
attributes: {
73-
'line': '0',
74-
'severity': _metricSeverityMapping[metric.level] ?? 'ignore',
75-
'message': metric.comment,
76-
'source': metric.metricsId,
77-
},
78-
);
68+
final metrics = record.file?.metrics;
69+
if (metrics != null) {
70+
for (final metric in metrics) {
71+
if (_isMetricNeedToReport(metric.level)) {
72+
builder.element(
73+
'error',
74+
attributes: {
75+
'line': '0',
76+
'severity': _metricSeverityMapping[metric.level] ?? 'ignore',
77+
'message': metric.comment,
78+
'source': metric.metricsId,
79+
},
80+
);
81+
}
7982
}
8083
}
8184

@@ -107,7 +110,7 @@ class LintCheckstyleReporter
107110
bool _needToReport(LintFileReport report) =>
108111
report.issues.isNotEmpty ||
109112
report.antiPatternCases.isNotEmpty ||
110-
_isMetricNeedToReport(report.file.metricsLevel);
113+
(report.file != null && _isMetricNeedToReport(report.file!.metricsLevel));
111114

112115
bool _isMetricNeedToReport(MetricValueLevel level) =>
113116
level > MetricValueLevel.none;

0 commit comments

Comments
 (0)