From 0674bc1b99a7ddff4b77b830c40a47530cc15fce Mon Sep 17 00:00:00 2001 From: Denis Bogatirov Date: Wed, 20 Sep 2023 15:48:01 +0300 Subject: [PATCH] Add prefer-match-file-name rule --- .../models/declaration_token_info.dart | 14 ++++ .../prefer_match_file_name_rule.dart | 74 +++++++++++++++++++ .../prefer_match_file_name_visitor.dart | 56 ++++++++++++++ lib/solid_lints.dart | 2 + lib/utils/node_utils.dart | 16 ++++ pubspec.yaml | 5 +- 6 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 lib/lints/prefer_match_file_name/models/declaration_token_info.dart create mode 100644 lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart create mode 100644 lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart diff --git a/lib/lints/prefer_match_file_name/models/declaration_token_info.dart b/lib/lints/prefer_match_file_name/models/declaration_token_info.dart new file mode 100644 index 00000000..54a0e72a --- /dev/null +++ b/lib/lints/prefer_match_file_name/models/declaration_token_info.dart @@ -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); +} diff --git a/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart b/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart new file mode 100644 index 00000000..ba4e0b2a --- /dev/null +++ b/lib/lints/prefer_match_file_name/prefer_match_file_name_rule.dart @@ -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; + } +} diff --git a/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart b/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart new file mode 100644 index 00000000..ecc22d33 --- /dev/null +++ b/lib/lints/prefer_match_file_name/prefer_match_file_name_visitor.dart @@ -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 { + final _declarations = []; + + /// List of all declarations + Iterable 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); + } +} diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart index d633409c..2d450638 100644 --- a/lib/solid_lints.dart +++ b/lib/solid_lints.dart @@ -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 @@ -53,6 +54,7 @@ class _SolidLints extends PluginBase { PreferConditionalExpressionsRule.createRule(configs), PreferFirstRule.createRule(configs), PreferLastRule.createRule(configs), + PreferMatchFileNameRule.createRule(configs), ]; // Return only enabled rules diff --git a/lib/utils/node_utils.dart b/lib/utils/node_utils.dart index 72589f94..3cea0c01 100644 --- a/lib/utils/node_utils.dart +++ b/lib/utils/node_utils.dart @@ -6,3 +6,19 @@ bool isOverride(List 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'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 649539c7..3af61062 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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