Skip to content

Commit

Permalink
Add prefer-match-file-name rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Denis Bogatirov committed Sep 20, 2023
1 parent 3e2f9fc commit 0674bc1
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

/// Data class represents declaration token and declaration parent node
class DeclarationTokeInfo {
/// Declaration token
final Token token;

/// Declaration parent node
final AstNode parent;

/// Creates instance of [DeclarationTokeInfo]
const DeclarationTokeInfo(this.token, this.parent);
}
74 changes: 74 additions & 0 deletions lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:path/path.dart' as p;
import 'package:solid_lints/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart';
import 'package:solid_lints/models/rule_config.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';
import 'package:solid_lints/utils/node_utils.dart';

/// A `prefer-match-file-name` rule which warns about
/// mismatch between file name and declared element inside
class PreferMatchFileNameRule extends SolidLintRule {
/// The [LintCode] of this lint rule that represents the error if iterable
/// access can be simplified.
static const String lintName = 'prefer-match-file-name';
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');

PreferMatchFileNameRule._(super.config);

/// Creates a new instance of [PreferMatchFileNameRule]
/// based on the lint configuration.
factory PreferMatchFileNameRule.createRule(CustomLintConfigs configs) {
final config = RuleConfig(
configs: configs,
name: lintName,
problemMessage: (value) =>
'File name does not match with first declared element name.',
);

return PreferMatchFileNameRule._(config);
}

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addCompilationUnit((node) {
final visitor = PreferMatchFileNameVisitor();

node.accept(visitor);

if (visitor.declarations.isEmpty) return;

final info = visitor.declarations.first;
if (!_hasMatchName(resolver.source.fullName, info.token.lexeme)) {
final nodeType = humanReadableNodeType(info.parent).toLowerCase();

reporter.reportErrorForToken(
LintCode(
name: lintName,
problemMessage:
'File name does not match with first $nodeType name.',
),
info.token,
);
}
});
}

bool _hasMatchName(String path, String identifierName) {
final identifierNameFormatted =
identifierName.replaceAll(_onlySymbolsRegex, '').toLowerCase();

final fileNameFormatted = p
.basename(path)
.split('.')
.first
.replaceAll(_onlySymbolsRegex, '')
.toLowerCase();

return identifierNameFormatted == fileNameFormatted;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/lints/prefer_match_file_name/models/declaration_token_info.dart';

/// The AST visitor that will collect all Class, Enum, Extension and Mixin
/// declarations
class PreferMatchFileNameVisitor extends RecursiveAstVisitor<void> {
final _declarations = <DeclarationTokeInfo>[];

/// List of all declarations
Iterable<DeclarationTokeInfo> get declarations =>
_declarations..sort(_compareByPrivateType);

@override
void visitClassDeclaration(ClassDeclaration node) {
super.visitClassDeclaration(node);

_declarations.add(DeclarationTokeInfo(node.name, node));
}

@override
void visitExtensionDeclaration(ExtensionDeclaration node) {
super.visitExtensionDeclaration(node);

final name = node.name;
if (name != null) {
_declarations.add(DeclarationTokeInfo(name, node));
}
}

@override
void visitMixinDeclaration(MixinDeclaration node) {
super.visitMixinDeclaration(node);

_declarations.add(DeclarationTokeInfo(node.name, node));
}

@override
void visitEnumDeclaration(EnumDeclaration node) {
super.visitEnumDeclaration(node);

_declarations.add(DeclarationTokeInfo(node.name, node));
}

int _compareByPrivateType(DeclarationTokeInfo a, DeclarationTokeInfo b) {
final isAPrivate = Identifier.isPrivateName(a.token.lexeme);
final isBPrivate = Identifier.isPrivateName(b.token.lexeme);
if (!isAPrivate && isBPrivate) {
return -1;
} else if (isAPrivate && !isBPrivate) {
return 1;
}

return a.token.offset.compareTo(b.token.offset);
}
}
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'package:solid_lints/lints/number_of_parameters/number_of_parameters_metr
import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart';
import 'package:solid_lints/lints/prefer_first/prefer_first_rule.dart';
import 'package:solid_lints/lints/prefer_last/prefer_last_rule.dart';
import 'package:solid_lints/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';

/// Creates a plugin for our custom linter
Expand Down Expand Up @@ -53,6 +54,7 @@ class _SolidLints extends PluginBase {
PreferConditionalExpressionsRule.createRule(configs),
PreferFirstRule.createRule(configs),
PreferLastRule.createRule(configs),
PreferMatchFileNameRule.createRule(configs),
];

// Return only enabled rules
Expand Down
16 changes: 16 additions & 0 deletions lib/utils/node_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,19 @@ bool isOverride(List<Annotation> metadata) => metadata.any(
(node) =>
node.name.name == 'override' && node.atSign.type == TokenType.AT,
);

/// Returns human readable node type
/// Self explanatory
String humanReadableNodeType(AstNode? node) {
if (node is ClassDeclaration) {
return 'Class';
} else if (node is EnumDeclaration) {
return 'Enum';
} else if (node is ExtensionDeclaration) {
return 'Extension';
} else if (node is MixinDeclaration) {
return 'Mixin';
}

return 'Node';
}
5 changes: 3 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ environment:
dependencies:
analyzer: ^5.12.0
collection: ^1.17.2
custom_lint_builder: ^0.5.0
custom_lint_builder: ^0.5.3
path: ^1.8.3

dev_dependencies:
# These packages are mandatory for some of tests
# These packages are mandatory for some of tests
flutter:
sdk: flutter
test: ^1.24.6
Expand Down

0 comments on commit 0674bc1

Please sign in to comment.