Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart';
import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart';
import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart';
import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart';
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
Expand Down Expand Up @@ -65,6 +66,7 @@ class SolidLintsPlugin extends Plugin {
NoMagicNumberRule(analysisOptionsLoader: analysisLoader),
preferFirstRule,
preferLastRule,
PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader),
// TODO: Add more lint rules and use analysisLoader
// for rules that need parameters
// For example: `CyclomaticComplexityRule(analysisLoader)`
Expand Down
18 changes: 10 additions & 8 deletions lib/src/common/parameters/excluded_entities_list_parameter.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'package:analyzer/dart/ast/ast.dart';

/// A model representing "exclude_entity" parameters for linting, defining
/// identifiers (classes, mixins, enums, extensions) to be ignored during
/// identifiers (classes, mixins, enums, extensions, extension_types) to be ignored during
/// analysis.
/// Supported entities:
/// - mixin
/// - extension
/// - extension_type
/// - enum
class ExcludedEntitiesListParameter {
/// The parameter model
Expand All @@ -20,17 +21,15 @@ class ExcludedEntitiesListParameter {
});

/// Method for creating from json data
factory ExcludedEntitiesListParameter.fromJson(Map<String, dynamic> json) {
final raw = json['exclude_entity'];
if (raw is List) {
factory ExcludedEntitiesListParameter.fromJson(Map<String, Object?> json) {
final excludedEntities = json['exclude_entity'];
if (excludedEntities is Iterable) {
return ExcludedEntitiesListParameter(
excludedEntityNames: Set<String>.from(raw),
excludedEntityNames: excludedEntities.cast<String>().toSet(),
);
}

return ExcludedEntitiesListParameter(
excludedEntityNames: {},
);
return ExcludedEntitiesListParameter(excludedEntityNames: {});
}

/// Returns whether the target node should be ignored during analysis.
Expand All @@ -45,6 +44,9 @@ class ExcludedEntitiesListParameter {
} else if (node is ExtensionDeclaration &&
excludedEntityNames.contains('extension')) {
return true;
} else if (node is ExtensionTypeDeclaration &&
excludedEntityNames.contains('extension_type')) {
return true;
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ class PreferMatchFileNameParameters {
required this.excludeEntity,
});

/// Empty [PreferMatchFileNameParameters] model.
factory PreferMatchFileNameParameters.empty() {
return PreferMatchFileNameParameters(
excludeEntity: ExcludedEntitiesListParameter(excludedEntityNames: {}),
);
}

/// Method for creating from json data
factory PreferMatchFileNameParameters.fromJson(Map<String, Object?> json) =>
PreferMatchFileNameParameters(
Expand Down
110 changes: 52 additions & 58 deletions lib/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/error/error.dart';
import 'package:path/path.dart' as p;
import 'package:solid_lints/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';
import 'package:solid_lints/src/utils/node_utils.dart';

/// Warns about a mismatch between file name and first declared element inside.
///
/// This improves navigation by matching file content and file name.
///
/// ### Example config:
///
/// ```yaml
/// plugins:
/// solid_lints:
/// diagnostics:
/// prefer_match_file_name:
/// exclude_entity:
/// - mixin
/// - extension
/// - extension_type
/// - enum
/// ```
///
/// ## Tests
///
/// State: **Disabled**.
Expand Down Expand Up @@ -51,79 +64,60 @@ import 'package:solid_lints/src/utils/node_utils.dart';
///
class PreferMatchFileNameRule
extends SolidLintRule<PreferMatchFileNameParameters> {
/// This lint rule represents the error if iterable
/// access can be simplified.
static const String lintName = 'prefer_match_file_name';
/// Name of the lint.
static const lintName = 'prefer_match_file_name';
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');

PreferMatchFileNameRule._(super.config);
static const _code = LintCode(
lintName,
'File name does not match with first {0} name.',
);

@override
DiagnosticCode get diagnosticCode => _code;

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

return PreferMatchFileNameRule._(config);
}
PreferMatchFileNameRule({
required super.analysisOptionsLoader,
}) : super.withParameters(
name: lintName,
description:
'Warns about a mismatch between file name and first declared '
'element inside.',
parametersParser: PreferMatchFileNameParameters.fromJson,
);

@override
void run(
CustomLintResolver resolver,
DiagnosticReporter reporter,
CustomLintContext context,
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
context.registry.addCompilationUnit((node) {
final excludedEntities = config.parameters.excludeEntity;

final visitor = PreferMatchFileNameVisitor(
excludedEntities: excludedEntities,
);

node.accept(visitor);
super.registerNodeProcessors(registry, context);

if (visitor.declarations.isEmpty) return;
final parameters =
getParametersForContext(context) ??
PreferMatchFileNameParameters.empty();

final firstDeclaration = visitor.declarations.first;

if (_doNormalizedNamesMatch(
resolver.source.fullName,
firstDeclaration.token.lexeme,
)) {
return;
}

final nodeType =
humanReadableNodeType(firstDeclaration.parent).toLowerCase();
final visitor = PreferMatchFileNameVisitor(
this,
context,
parameters.excludeEntity,
);

reporter.atToken(
firstDeclaration.token,
LintCode(
name: lintName,
problemMessage: 'File name does not match with first $nodeType name.',
),
);
});
registry.addCompilationUnit(this, visitor);
}

bool _doNormalizedNamesMatch(String path, String identifierName) {
/// Checks if the normalized file path matches the normalized identifier name.
bool doNormalizedNamesMatch(String path, String identifierName) {
final fileName = _normalizePath(path);
final dartIdentifier = _normalizeDartIdentifierName(identifierName);

return fileName == dartIdentifier;
}

String _normalizePath(String s) => p
.basename(s)
.split('.')
.first
.replaceAll(_onlySymbolsRegex, '')
.toLowerCase();
String _normalizePath(String s) =>
_normalizeDartIdentifierName(p.basename(s).split('.').first);

String _normalizeDartIdentifierName(String s) =>
s.replaceAll(_onlySymbolsRegex, '').toLowerCase();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,82 @@
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/models/declaration_token_info.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'package:solid_lints/src/utils/node_utils.dart';

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

/// The rule context
final RuleContext context;

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

/// Constructor of [PreferMatchFileNameVisitor] class
PreferMatchFileNameVisitor({
required this.excludedEntities,
});

/// List of all declarations
Iterable<DeclarationTokenInfo> get declarations => _declarations.where(
(declaration) {
if (declaration.parent is Declaration) {
return !excludedEntities
.shouldIgnoreEntity(declaration.parent as Declaration);
}
return true;
},
).toList()
..sort(
(a, b) => _publicDeclarationsFirst(a, b) ?? _byDeclarationOrder(a, b),
);
PreferMatchFileNameVisitor(
this.rule,
this.context,
this.excludedEntities,
);

@override
void visitClassDeclaration(ClassDeclaration node) {
super.visitClassDeclaration(node);
void visitCompilationUnit(CompilationUnit node) {
final declarations = <DeclarationTokenInfo>[];

_declarations.add((token: node.name, parent: node));
}
for (final declaration in node.declarations) {
if (excludedEntities.shouldIgnoreEntity(declaration)) {
continue;
}

@override
void visitExtensionDeclaration(ExtensionDeclaration node) {
super.visitExtensionDeclaration(node);
final token = switch (declaration) {
final ClassDeclaration classDecl => classDecl.namePart.typeName,
final ExtensionDeclaration extDecl => extDecl.name,
final MixinDeclaration mixinDecl => mixinDecl.name,
final EnumDeclaration enumDecl => enumDecl.namePart.typeName,
final ExtensionTypeDeclaration extTypeDecl =>
extTypeDecl.primaryConstructor.typeName,
_ => null,
};

final name = node.name;
if (name != null) {
_declarations.add((token: name, parent: node));
if (token != null) {
declarations.add((token: token, parent: declaration));
}
}
}

@override
void visitMixinDeclaration(MixinDeclaration node) {
super.visitMixinDeclaration(node);
if (declarations.isEmpty) return;

_declarations.add((token: node.name, parent: node));
}
declarations.sort(
(a, b) => _publicDeclarationsFirst(a, b) ?? _byDeclarationOrder(a, b),
);

@override
void visitEnumDeclaration(EnumDeclaration node) {
super.visitEnumDeclaration(node);
final firstDeclaration = declarations.first;
final fullName = context.currentUnit?.file.path;

if (fullName != null &&
rule.doNormalizedNamesMatch(
fullName,
firstDeclaration.token.lexeme,
)) {
return;
}

_declarations.add((token: node.name, parent: node));
final nodeType = humanReadableNodeType(
firstDeclaration.parent,
).toLowerCase();

final reporter = context.currentUnit?.diagnosticReporter;
reporter?.atToken(
firstDeclaration.token,
rule.diagnosticCode,
arguments: [nodeType],
);
}

int? _publicDeclarationsFirst(
Expand All @@ -68,13 +85,12 @@ class PreferMatchFileNameVisitor extends RecursiveAstVisitor<void> {
) {
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;
}
// no reorder needed;
return null;

return switch ((isAPrivate, isBPrivate)) {
(false, true) => -1,
(true, false) => 1,
_ => null,
};
}

int _byDeclarationOrder(DeclarationTokenInfo a, DeclarationTokenInfo b) {
Expand Down
5 changes: 0 additions & 5 deletions lint_test/prefer_match_file_name_enum_test.dart

This file was deleted.

5 changes: 0 additions & 5 deletions lint_test/prefer_match_file_name_extension_test.dart

This file was deleted.

This file was deleted.

Loading
Loading