Skip to content

Commit e3a06fb

Browse files
solid-illiaaihistovIllia Aihistov
andauthored
refactor: migrate prefer_match_file_name (#305)
* refactor: migrate prefer_match_file_name * refactor: improve prefer_match_file_name lint logic with switch expressions and centralized parameter initialization * test: add test cases for prefer_match_file_name rule * refactor: move normalization logic to visitor and add multi-sort utility to improve PreferMatchFileName rule encapsulation * refactor: replace hardcoded exclude_entity key with constant in ExcludedEntitiesListParameter --------- Co-authored-by: Illia Aihistov <illia.aihistov-us@solid.software>
1 parent 562cddd commit e3a06fb

19 files changed

Lines changed: 375 additions & 250 deletions

lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart';
3232
import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart';
3333
import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart';
3434
import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart';
35+
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
3536
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
3637
import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart';
3738
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
@@ -84,6 +85,7 @@ class SolidLintsPlugin extends Plugin {
8485
preferFirstRule,
8586
preferConditionalExpressionsRule,
8687
preferLastRule,
88+
PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader),
8789
// TODO: Add more lint rules and use analysisLoader
8890
// for rules that need parameters
8991
// For example: `CyclomaticComplexityRule(analysisLoader)`

lib/src/common/parameters/excluded_entities_list_parameter.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import 'package:analyzer/dart/ast/ast.dart';
22

33
/// A model representing "exclude_entity" parameters for linting, defining
4-
/// identifiers (classes, mixins, enums, extensions) to be ignored during
5-
/// analysis.
4+
/// identifiers (classes, mixins, enums, extensions, extension_types) to be
5+
/// ignored during analysis.
66
/// Supported entities:
77
/// - mixin
88
/// - extension
9+
/// - extension_type
910
/// - enum
1011
class ExcludedEntitiesListParameter {
1112
/// The parameter model
@@ -19,22 +20,20 @@ class ExcludedEntitiesListParameter {
1920
required this.excludedEntityNames,
2021
});
2122

22-
/// Method for creating from json data
23-
factory ExcludedEntitiesListParameter.fromJson(Map<String, dynamic> json) {
24-
final raw = json['exclude_entity'];
23+
/// Creates an [ExcludedEntitiesListParameter] from JSON.
24+
factory ExcludedEntitiesListParameter.fromJson(Map<String, Object?> json) {
25+
final raw = json[excludeEntityKey];
2526
if (raw is Iterable) {
2627
return ExcludedEntitiesListParameter(
27-
excludedEntityNames: Set<String>.from(raw.whereType<String>()),
28+
excludedEntityNames: raw.whereType<String>().toSet(),
2829
);
2930
} else if (raw is String) {
3031
return ExcludedEntitiesListParameter(
3132
excludedEntityNames: {raw},
3233
);
3334
}
3435

35-
return ExcludedEntitiesListParameter(
36-
excludedEntityNames: {},
37-
);
36+
return ExcludedEntitiesListParameter(excludedEntityNames: {});
3837
}
3938

4039
/// Returns whether the target node should be ignored during analysis.
@@ -49,6 +48,9 @@ class ExcludedEntitiesListParameter {
4948
} else if (node is ExtensionDeclaration &&
5049
excludedEntityNames.contains('extension')) {
5150
return true;
51+
} else if (node is ExtensionTypeDeclaration &&
52+
excludedEntityNames.contains('extension_type')) {
53+
return true;
5254
}
5355

5456
return false;

lib/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ class PreferMatchFileNameParameters {
1111
required this.excludeEntity,
1212
});
1313

14+
/// Empty [PreferMatchFileNameParameters] model.
15+
factory PreferMatchFileNameParameters.empty() {
16+
return PreferMatchFileNameParameters(
17+
excludeEntity: ExcludedEntitiesListParameter(excludedEntityNames: {}),
18+
);
19+
}
20+
1421
/// Method for creating from json data
1522
factory PreferMatchFileNameParameters.fromJson(Map<String, Object?> json) =>
1623
PreferMatchFileNameParameters(
Lines changed: 48 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
1-
import 'package:analyzer/error/listener.dart';
2-
import 'package:custom_lint_builder/custom_lint_builder.dart';
3-
import 'package:path/path.dart' as p;
1+
import 'package:analyzer/analysis_rule/rule_context.dart';
2+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
3+
import 'package:analyzer/error/error.dart';
44
import 'package:solid_lints/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart';
55
import 'package:solid_lints/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart';
6-
import 'package:solid_lints/src/models/rule_config.dart';
76
import 'package:solid_lints/src/models/solid_lint_rule.dart';
8-
import 'package:solid_lints/src/utils/node_utils.dart';
97

108
/// Warns about a mismatch between file name and first declared element inside.
119
///
1210
/// This improves navigation by matching file content and file name.
1311
///
12+
/// ### Example config:
13+
///
14+
/// ```yaml
15+
/// plugins:
16+
/// solid_lints:
17+
/// diagnostics:
18+
/// prefer_match_file_name:
19+
/// exclude_entity:
20+
/// - mixin
21+
/// - extension
22+
/// - extension_type
23+
/// - enum
24+
/// ```
25+
///
1426
/// ## Tests
1527
///
1628
/// State: **Disabled**.
@@ -51,80 +63,46 @@ import 'package:solid_lints/src/utils/node_utils.dart';
5163
///
5264
class PreferMatchFileNameRule
5365
extends SolidLintRule<PreferMatchFileNameParameters> {
54-
/// This lint rule represents the error if iterable
55-
/// access can be simplified.
56-
static const String lintName = 'prefer_match_file_name';
57-
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');
66+
/// Name of the lint.
67+
static const lintName = 'prefer_match_file_name';
5868

59-
PreferMatchFileNameRule._(super.config);
69+
static const _code = LintCode(
70+
lintName,
71+
'File name does not match with first {0} name.',
72+
);
73+
74+
@override
75+
DiagnosticCode get diagnosticCode => _code;
6076

6177
/// Creates a new instance of [PreferMatchFileNameRule]
6278
/// based on the lint configuration.
63-
factory PreferMatchFileNameRule.createRule(CustomLintConfigs configs) {
64-
final config = RuleConfig(
65-
configs: configs,
66-
name: lintName,
67-
paramsParser: PreferMatchFileNameParameters.fromJson,
68-
problemMessage: (value) =>
69-
'File name does not match with first declared element name.',
70-
);
71-
72-
return PreferMatchFileNameRule._(config);
73-
}
79+
PreferMatchFileNameRule({
80+
required super.analysisOptionsLoader,
81+
}) : super.withParameters(
82+
name: lintName,
83+
description:
84+
'Warns about a mismatch between file name and first declared '
85+
'element inside.',
86+
parametersParser: PreferMatchFileNameParameters.fromJson,
87+
);
7488

7589
@override
76-
void run(
77-
CustomLintResolver resolver,
78-
DiagnosticReporter reporter,
79-
CustomLintContext context,
90+
void registerNodeProcessors(
91+
RuleVisitorRegistry registry,
92+
RuleContext context,
8093
) {
81-
context.registry.addCompilationUnit((node) {
82-
final excludedEntities = config.parameters.excludeEntity;
83-
84-
final visitor = PreferMatchFileNameVisitor(
85-
excludedEntities: excludedEntities,
86-
);
87-
88-
node.accept(visitor);
94+
super.registerNodeProcessors(registry, context);
8995

90-
if (visitor.declarations.isEmpty) return;
96+
final parameters =
97+
getParametersForContext(context) ??
98+
PreferMatchFileNameParameters.empty();
9199

92-
final firstDeclaration = visitor.declarations.first;
93-
94-
if (_doNormalizedNamesMatch(
95-
resolver.source.fullName,
96-
firstDeclaration.token.lexeme,
97-
)) {
98-
return;
99-
}
100-
101-
final nodeType =
102-
humanReadableNodeType(firstDeclaration.parent).toLowerCase();
103-
104-
reporter.atToken(
105-
firstDeclaration.token,
106-
LintCode(
107-
name: lintName,
108-
problemMessage: 'File name does not match with first $nodeType name.',
109-
),
110-
);
111-
});
112-
}
113-
114-
bool _doNormalizedNamesMatch(String path, String identifierName) {
115-
final fileName = _normalizePath(path);
116-
final dartIdentifier = _normalizeDartIdentifierName(identifierName);
100+
final visitor = PreferMatchFileNameVisitor(
101+
diagnosticCode: diagnosticCode,
102+
context: context,
103+
excludedEntities: parameters.excludeEntity,
104+
);
117105

118-
return fileName == dartIdentifier;
106+
registry.addCompilationUnit(this, visitor);
119107
}
120-
121-
String _normalizePath(String s) => p
122-
.basename(s)
123-
.split('.')
124-
.first
125-
.replaceAll(_onlySymbolsRegex, '')
126-
.toLowerCase();
127-
128-
String _normalizeDartIdentifierName(String s) =>
129-
s.replaceAll(_onlySymbolsRegex, '').toLowerCase();
130108
}
Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,97 @@
1+
import 'package:analyzer/analysis_rule/rule_context.dart';
12
import 'package:analyzer/dart/ast/ast.dart';
23
import 'package:analyzer/dart/ast/visitor.dart';
4+
import 'package:analyzer/error/error.dart';
5+
import 'package:collection/collection.dart';
6+
import 'package:path/path.dart' as p;
37
import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart';
48
import 'package:solid_lints/src/lints/prefer_match_file_name/models/declaration_token_info.dart';
9+
import 'package:solid_lints/src/utils/iterable_utils.dart';
10+
import 'package:solid_lints/src/utils/node_utils.dart';
511

6-
/// The AST visitor that will collect all Class, Enum, Extension and Mixin
7-
/// declarations
8-
class PreferMatchFileNameVisitor extends RecursiveAstVisitor<void> {
9-
final _declarations = <DeclarationTokenInfo>[];
12+
/// The AST visitor that will collect all Class, Enum, Extension, Mixin and
13+
/// Extension Type declarations
14+
class PreferMatchFileNameVisitor extends SimpleAstVisitor<void> {
15+
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');
16+
17+
/// The diagnostic code to report
18+
final DiagnosticCode diagnosticCode;
19+
20+
/// The rule context
21+
final RuleContext context;
1022

1123
/// Iterable that contains the name of entity (or entities) that should
1224
/// be ignored
1325
final ExcludedEntitiesListParameter excludedEntities;
1426

1527
/// Constructor of [PreferMatchFileNameVisitor] class
1628
PreferMatchFileNameVisitor({
29+
required this.diagnosticCode,
30+
required this.context,
1731
required this.excludedEntities,
1832
});
1933

20-
/// List of all declarations
21-
Iterable<DeclarationTokenInfo> get declarations => _declarations.where(
22-
(declaration) {
23-
if (declaration.parent is Declaration) {
24-
return !excludedEntities
25-
.shouldIgnoreEntity(declaration.parent as Declaration);
26-
}
27-
return true;
28-
},
29-
).toList()
30-
..sort(
31-
(a, b) => _publicDeclarationsFirst(a, b) ?? _byDeclarationOrder(a, b),
32-
);
33-
3434
@override
35-
void visitClassDeclaration(ClassDeclaration node) {
36-
super.visitClassDeclaration(node);
35+
void visitCompilationUnit(CompilationUnit node) {
36+
final declarations = node.declarations
37+
.whereNot(excludedEntities.shouldIgnoreEntity)
38+
.map<DeclarationTokenInfo?>(
39+
(d) {
40+
final token = switch (d) {
41+
ClassDeclaration() => d.namePart.typeName,
42+
ExtensionDeclaration() => d.name,
43+
MixinDeclaration() => d.name,
44+
EnumDeclaration() => d.namePart.typeName,
45+
ExtensionTypeDeclaration() => d.primaryConstructor.typeName,
46+
_ => null,
47+
};
3748

38-
_declarations.add((token: node.name, parent: node));
39-
}
49+
return token == null ? null : (token: token, parent: d);
50+
},
51+
)
52+
.nonNulls
53+
.multiSortedBy(
54+
[
55+
(t) => Identifier.isPrivateName(t.token.lexeme) ? 1 : 0,
56+
(t) => t.token.offset,
57+
],
58+
);
4059

41-
@override
42-
void visitExtensionDeclaration(ExtensionDeclaration node) {
43-
super.visitExtensionDeclaration(node);
60+
if (declarations.isEmpty) return;
4461

45-
final name = node.name;
46-
if (name != null) {
47-
_declarations.add((token: name, parent: node));
62+
final firstDeclaration = declarations.first;
63+
final fullName = context.currentUnit?.file.path;
64+
65+
if (fullName != null &&
66+
_doNormalizedNamesMatch(
67+
fullName,
68+
firstDeclaration.token.lexeme,
69+
)) {
70+
return;
4871
}
49-
}
5072

51-
@override
52-
void visitMixinDeclaration(MixinDeclaration node) {
53-
super.visitMixinDeclaration(node);
73+
final nodeType = humanReadableNodeType(
74+
firstDeclaration.parent,
75+
).toLowerCase();
5476

55-
_declarations.add((token: node.name, parent: node));
77+
final reporter = context.currentUnit?.diagnosticReporter;
78+
reporter?.atToken(
79+
firstDeclaration.token,
80+
diagnosticCode,
81+
arguments: [nodeType],
82+
);
5683
}
5784

58-
@override
59-
void visitEnumDeclaration(EnumDeclaration node) {
60-
super.visitEnumDeclaration(node);
85+
bool _doNormalizedNamesMatch(String path, String identifierName) {
86+
final fileName = _normalizePath(path);
87+
final dartIdentifier = _normalizeDartIdentifierName(identifierName);
6188

62-
_declarations.add((token: node.name, parent: node));
89+
return fileName == dartIdentifier;
6390
}
6491

65-
int? _publicDeclarationsFirst(
66-
DeclarationTokenInfo a,
67-
DeclarationTokenInfo b,
68-
) {
69-
final isAPrivate = Identifier.isPrivateName(a.token.lexeme);
70-
final isBPrivate = Identifier.isPrivateName(b.token.lexeme);
71-
if (!isAPrivate && isBPrivate) {
72-
return -1;
73-
} else if (isAPrivate && !isBPrivate) {
74-
return 1;
75-
}
76-
// no reorder needed;
77-
return null;
78-
}
92+
String _normalizePath(String s) =>
93+
_normalizeDartIdentifierName(p.basename(s).split('.').first);
7994

80-
int _byDeclarationOrder(DeclarationTokenInfo a, DeclarationTokenInfo b) {
81-
return a.token.offset.compareTo(b.token.offset);
82-
}
95+
String _normalizeDartIdentifierName(String s) =>
96+
s.replaceAll(_onlySymbolsRegex, '').toLowerCase();
8397
}

0 commit comments

Comments
 (0)