From a2cb420596ed7f49942119f4e71d27d71efda210 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 15 Jul 2026 18:32:38 +0300 Subject: [PATCH 01/13] feat: add avoid_similar_names lint rule --- lib/main.dart | 2 + .../avoid_similar_names_rule.dart | 66 +++++++ .../models/scope_variable.dart | 49 ++++++ .../utils/name_tokenizer.dart | 55 ++++++ .../visitors/avoid_similar_names_visitor.dart | 145 ++++++++++++++++ .../visitors/local_variables_visitor.dart | 33 ++++ .../avoid_similar_names_rule_test.dart | 162 ++++++++++++++++++ 7 files changed, 512 insertions(+) create mode 100644 lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart create mode 100644 lib/src/lints/avoid_similar_names/models/scope_variable.dart create mode 100644 lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart create mode 100644 lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart create mode 100644 lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart create mode 100644 test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index a6f55fad..3e1f5401 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; @@ -57,6 +58,7 @@ class SolidLintsPlugin extends Plugin { AvoidLateKeywordRule(analysisOptionsLoader: analysisLoader), AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader), AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), + AvoidSimilarNamesRule(), AvoidUnnecessaryReturnVariableRule(), AvoidUnnecessarySetStateRule(), AvoidUnnecessaryTypeAssertionsRule(), diff --git a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart new file mode 100644 index 00000000..1faccc7f --- /dev/null +++ b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart @@ -0,0 +1,66 @@ +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:solid_lints/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; + +/// Avoid similar names +/// +/// Warns about variables or parameters that have confusingly similar names +/// within the same function scope (e.g., using numeric suffixes or +/// single-letter modifiers like `someClass1` and `someClass2`). +/// +/// This encourages using descriptive, distinct names to improve code +/// readability and prevent logical errors caused by mixing up variables. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// void test(SomeClass someClass1, SomeClass someClass2) { // LINT +/// final tempA = 'a'; // LINT +/// final tempB = 'b'; // LINT +/// } +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// void test(SomeClass first, SomeClass second) { +/// final that = 'a'; +/// final other = 'b'; +/// } +/// ``` +class AvoidSimilarNamesRule extends SolidLintRule { + /// The name of this lint rule. + static const String lintName = 'avoid_similar_names'; + + static const LintCode _code = LintCode( + lintName, + 'Avoid using similar names.', + correctionMessage: 'Use more descriptive names.', + ); + + /// Creates an instance of [AvoidSimilarNamesRule]. + AvoidSimilarNamesRule() + : super( + name: lintName, + description: 'Warns about variables or parameters with similar names.', + ); + + @override + LintCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = AvoidSimilarNamesVisitor(this); + + registry.addMethodDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addFunctionDeclaration(this, visitor); + } +} diff --git a/lib/src/lints/avoid_similar_names/models/scope_variable.dart b/lib/src/lints/avoid_similar_names/models/scope_variable.dart new file mode 100644 index 00000000..11991556 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/models/scope_variable.dart @@ -0,0 +1,49 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; + +/// Represents a variable or parameter collected from a scope. +class ScopeVariable { + /// The resolved type of the variable, if available. + final DartType? type; + + /// The AST node representing this variable declaration. + final AstNode node; + + /// The token representing the name. + final Token nameToken; + + /// The individual word/digit tokens of the name. + final List tokens; + + /// The minimum length for a variable name to be considered descriptive enough + /// to be analyzed for similarity. + static const minDescriptiveNameLength = 3; + + /// Creates a new [ScopeVariable] if the [nameToken] is descriptive enough + /// to be analyzed for similarity. Returns `null` if the cleaned name + /// is too short. + static ScopeVariable? createOrNull({ + required Token nameToken, + required DartType? type, + required AstNode node, + }) { + final cleaned = NameTokenizer.cleanName(nameToken.lexeme); + if (cleaned.length < minDescriptiveNameLength) return null; + + return ScopeVariable._( + nameToken: nameToken, + type: type, + node: node, + tokens: NameTokenizer.tokenize(cleaned), + ); + } + + ScopeVariable._({ + required this.nameToken, + required this.type, + required this.node, + required this.tokens, + }); +} diff --git a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart new file mode 100644 index 00000000..02491124 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart @@ -0,0 +1,55 @@ +/// Utility class for tokenizing identifiers and +/// comparing name similarity. +abstract final class NameTokenizer { + /// Regex pattern to match camelCase/snake_case tokens: + /// - `[A-Z]?[a-z]+` : Words (e.g., Class, user) + /// - `[A-Z]+` : Acronyms (e.g., URL, ID) + /// - `\d+` : Numeric sequences (e.g., 1, 10) + static final _tokenPattern = RegExp(r'[A-Z]?[a-z]+|[A-Z]+|\d+'); + + static const _allowedTokens = {'x', 'y', 'z', 'w', 'i', 'j', 'k'}; + + /// Splits a camelCase or snake_case identifier + /// into lowercase tokens. + /// + /// E.g., `someClass1` returns `['some', 'class', '1']`. + static List tokenize(String name) => [ + for (final match in _tokenPattern.allMatches(name)) + match.group(0)!.toLowerCase(), + ]; + + /// Strips leading underscores from a name. + /// + /// E.g., `_someName` returns `someName`. + static String cleanName(String name) => name.replaceFirst(RegExp('^_+'), ''); + + /// Returns `true` if the string consists only + /// of digit characters. + static bool isDigit(String s) => int.tryParse(s) != null; + + /// Returns `true` if the token is a common + /// loop variable or coordinate name. + static bool isAllowedToken(String s) => _allowedTokens.contains(s); + + /// Returns `true` if the token is considered non-descriptive + /// (either a digit or a disallowed single letter). + static bool isNonDescriptiveToken(String s) => + isDigit(s) || (s.length == 1 && !isAllowedToken(s)); + + /// Returns `true` if [longer] is a superset of + /// [shorter] with exactly one extra non-descriptive token. + static bool isSubsetWithNonDescriptiveToken( + List longer, + List shorter, + ) { + var i = 0; + while (i < shorter.length && longer[i] == shorter[i]) { + i++; + } + if (!isNonDescriptiveToken(longer[i])) return false; + while (i < shorter.length && longer[i + 1] == shorter[i]) { + i++; + } + return i == shorter.length; + } +} diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart new file mode 100644 index 00000000..f5382805 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -0,0 +1,145 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart'; + +/// A visitor that checks for variables with +/// confusingly similar names. +class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { + final AvoidSimilarNamesRule _rule; + final _reportedNodes = {}; + + /// Creates a new instance of + /// [AvoidSimilarNamesVisitor]. + AvoidSimilarNamesVisitor(this._rule); + + @override + void visitMethodDeclaration( + MethodDeclaration node, + ) { + super.visitMethodDeclaration(node); + _checkScope(node.parameters, node.body); + } + + @override + void visitConstructorDeclaration( + ConstructorDeclaration node, + ) { + super.visitConstructorDeclaration(node); + _checkScope(node.parameters, node.body); + } + + @override + void visitFunctionDeclaration( + FunctionDeclaration node, + ) { + super.visitFunctionDeclaration(node); + _checkScope( + node.functionExpression.parameters, + node.functionExpression.body, + ); + } + + void _checkScope( + FormalParameterList? parameters, + FunctionBody body, + ) { + final variables = []; + + variables.addAll(_extractParameters(parameters)); + + final collector = LocalVariablesVisitor(); + body.accept(collector); + variables.addAll(collector.variables); + + _compareVariables(variables); + } + + Iterable _extractParameters( + FormalParameterList? parameters, + ) => [ + for (final parameter in parameters?.parameters ?? const []) + if (parameter.name case final nameToken?) + if (ScopeVariable.createOrNull( + nameToken: nameToken, + type: parameter.declaredFragment?.element.type, + node: parameter, + ) + case final variable?) + variable, + ]; + + void _compareVariables( + List variables, + ) { + for (var i = 0; i < variables.length; i++) { + for (var j = i + 1; j < variables.length; j++) { + _comparePair(variables[i], variables[j]); + } + } + } + + void _comparePair( + ScopeVariable a, + ScopeVariable b, + ) { + if (a.type != null && b.type != null && a.type != b.type) { + return; + } + + switch ((a.tokens.length - b.tokens.length).abs()) { + case 0: + _checkSameLengthTokens(a, b); + case 1: + _checkDifferentLengthTokens(a, b); + } + } + + void _checkSameLengthTokens( + ScopeVariable a, + ScopeVariable b, + ) { + var diffCount = 0; + var diffIndex = -1; + for (var k = 0; k < a.tokens.length; k++) { + if (a.tokens[k] != b.tokens[k]) { + diffCount++; + diffIndex = k; + } + } + + if (diffCount != 1) return; + + if (NameTokenizer.isNonDescriptiveToken(a.tokens[diffIndex]) && + NameTokenizer.isNonDescriptiveToken(b.tokens[diffIndex])) { + _report(a, b); + } + } + + void _checkDifferentLengthTokens( + ScopeVariable a, + ScopeVariable b, + ) { + final (longer, shorter) = a.tokens.length > b.tokens.length + ? (a.tokens, b.tokens) + : (b.tokens, a.tokens); + + if (NameTokenizer.isSubsetWithNonDescriptiveToken( + longer, + shorter, + )) { + _report(a, b); + } + } + + void _report(ScopeVariable a, ScopeVariable b) { + if (_reportedNodes.add(a.node)) { + _rule.reportAtToken(a.nameToken); + } + if (_reportedNodes.add(b.node)) { + _rule.reportAtToken(b.nameToken); + } + } +} diff --git a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart new file mode 100644 index 00000000..4644a995 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart @@ -0,0 +1,33 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; + +/// Collects local variable declarations within a function body, +/// stopping at nested function boundaries. +class LocalVariablesVisitor extends RecursiveAstVisitor { + /// The collected variables. + final List variables = []; + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (ScopeVariable.createOrNull( + nameToken: node.name, + type: node.declaredFragment?.element.type, + node: node, + ) + case final variable?) { + variables.add(variable); + } + super.visitVariableDeclaration(node); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Stop traversing nested function scopes. + } + + @override + void visitFunctionExpression(FunctionExpression node) { + // Stop traversing nested closures. + } +} diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart new file mode 100644 index 00000000..33e8551e --- /dev/null +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -0,0 +1,162 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidSimilarNamesRuleTest); + }); +} + +@reflectiveTest +class AvoidSimilarNamesRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + @override + void setUp() { + rule = AvoidSimilarNamesRule(); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + } + + Future test_reports_on_similar_names_in_function() async { + await assertAutoDiagnostics(''' +void test() { + int ${expectLint('someClass1')} = 1; + int ${expectLint('someClass2')} = 2; +} +'''); + } + + Future test_does_not_report_on_different_types() async { + await assertNoDiagnostics(''' +void test() { + String user1 = 'Alice'; + int user2 = 123; +} +'''); + } + + Future test_reports_on_borderline_short_names() async { + await assertAutoDiagnostics(''' +void test() { + int ${expectLint('id1')} = 1; + int ${expectLint('id2')} = 2; +} +'''); + } + + Future test_does_not_report_on_different_descriptive_tokens() async { + await assertNoDiagnostics(''' +void test() { + int minHeight = 10; + int maxHeight = 20; +} +'''); + } + + Future test_does_not_report_on_short_names() async { + await assertNoDiagnostics(''' +void test() { + int x1 = 1; + int x2 = 2; + int dx = 5; + int dy = 10; +} +'''); + } + + Future test_does_not_report_on_allowed_coordinates() async { + await assertNoDiagnostics(''' +void test() { + double pointX = 1.0; + double pointY = 2.0; +} +'''); + } + + Future test_reports_on_digits_in_parameters() async { + await assertAutoDiagnostics(''' +bool isEqual( + int ${expectLint('someClass1')}, + int ${expectLint('someClass2')}, +) { + return someClass1 == someClass2; +} +'''); + } + + Future test_reports_on_similar_names_in_method() async { + await assertAutoDiagnostics(''' +class A { + void test() { + String ${expectLint('tempA')} = "a"; + String ${expectLint('tempB')} = "b"; + } +} +'''); + } + + Future test_reports_on_subset_with_extra_digit() async { + await assertAutoDiagnostics(''' +void test() { + String ${expectLint('data')} = "a"; + String ${expectLint('data1')} = "b"; +} +'''); + } + + Future test_reports_on_mixed_non_descriptive_suffixes() async { + await assertAutoDiagnostics(''' +void test() { + String ${expectLint('user1')} = "a"; + String ${expectLint('userA')} = "b"; +} +'''); + } + + Future test_reports_on_subset_with_extra_letter() async { + await assertAutoDiagnostics(''' +void test() { + String ${expectLint('user')} = "a"; + String ${expectLint('userA')} = "b"; +} +'''); + } + + Future test_does_not_report_on_subset_with_descriptive_token() async { + await assertNoDiagnostics(''' +void test() { + String user = "a"; + String userProfile = "b"; + String data = "c"; + String dataFetch = "d"; +} +'''); + } + + Future test_reports_on_three_similar_names() async { + await assertAutoDiagnostics(''' +void test() { + int ${expectLint('id1')} = 1; + int ${expectLint('id2')} = 2; + int ${expectLint('id3')} = 3; +} +'''); + } + + Future test_does_not_report_on_anonymous_lambda() async { + await assertNoDiagnostics(''' +void test() { + void process(int Function(int, int) callback) {} + process((day1, day2) => day1 + day2); +} +'''); + } +} From a5c00769ed90ac52c87de456814982e95a30e6d2 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 10:30:07 +0300 Subject: [PATCH 02/13] refactor: optimize variable collection and improve tokenization logic in avoid_similar_names lint --- .../avoid_similar_names_rule.dart | 4 ++-- .../utils/name_tokenizer.dart | 24 ++++++++++++++----- .../visitors/avoid_similar_names_visitor.dart | 14 ++++++----- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart index 1faccc7f..83d4f36d 100644 --- a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart +++ b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart @@ -34,9 +34,9 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ``` class AvoidSimilarNamesRule extends SolidLintRule { /// The name of this lint rule. - static const String lintName = 'avoid_similar_names'; + static const lintName = 'avoid_similar_names'; - static const LintCode _code = LintCode( + static const _code = LintCode( lintName, 'Avoid using similar names.', correctionMessage: 'Use more descriptive names.', diff --git a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart index 02491124..b5000450 100644 --- a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart +++ b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart @@ -1,12 +1,23 @@ /// Utility class for tokenizing identifiers and /// comparing name similarity. abstract final class NameTokenizer { - /// Regex pattern to match camelCase/snake_case tokens: - /// - `[A-Z]?[a-z]+` : Words (e.g., Class, user) - /// - `[A-Z]+` : Acronyms (e.g., URL, ID) - /// - `\d+` : Numeric sequences (e.g., 1, 10) + /// Regex Fragment | Meaning + /// ================================================================= + /// [A-Z]?[a-z]+ | Match words (e.g., Class, user): + /// [A-Z]? | ... optional leading uppercase, + /// [a-z]+ | ... followed by lowercase letters. + /// | | OR + /// [A-Z]+ | Match acronyms (uppercase letters). + /// | | OR + /// \d+ | Match digits (e.g., 1, 10). static final _tokenPattern = RegExp(r'[A-Z]?[a-z]+|[A-Z]+|\d+'); + /// Regex Fragment | Meaning + /// ================================================================= + /// ^ | Match start of string. + /// _+ | Match one or more leading underscores. + static final _leadingUnderscoresPattern = RegExp('^_+'); + static const _allowedTokens = {'x', 'y', 'z', 'w', 'i', 'j', 'k'}; /// Splits a camelCase or snake_case identifier @@ -15,13 +26,14 @@ abstract final class NameTokenizer { /// E.g., `someClass1` returns `['some', 'class', '1']`. static List tokenize(String name) => [ for (final match in _tokenPattern.allMatches(name)) - match.group(0)!.toLowerCase(), + if (match.group(0) case final group?) group.toLowerCase(), ]; /// Strips leading underscores from a name. /// /// E.g., `_someName` returns `someName`. - static String cleanName(String name) => name.replaceFirst(RegExp('^_+'), ''); + static String cleanName(String name) => + name.replaceFirst(_leadingUnderscoresPattern, ''); /// Returns `true` if the string consists only /// of digit characters. diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index f5382805..832c09c6 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -10,6 +10,7 @@ import 'package:solid_lints/src/lints/avoid_similar_names/visitors/local_variabl class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { final AvoidSimilarNamesRule _rule; final _reportedNodes = {}; + final _collector = LocalVariablesVisitor(); /// Creates a new instance of /// [AvoidSimilarNamesVisitor]. @@ -46,13 +47,13 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { FormalParameterList? parameters, FunctionBody body, ) { - final variables = []; + _collector.variables.clear(); + body.accept(_collector); - variables.addAll(_extractParameters(parameters)); - - final collector = LocalVariablesVisitor(); - body.accept(collector); - variables.addAll(collector.variables); + final variables = [ + ..._extractParameters(parameters), + ..._collector.variables, + ]; _compareVariables(variables); } @@ -106,6 +107,7 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { for (var k = 0; k < a.tokens.length; k++) { if (a.tokens[k] != b.tokens[k]) { diffCount++; + if (diffCount > 1) return; diffIndex = k; } } From 19674b7fb514e0de8cfebe6df0ee2dde724bdbba Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 10:43:09 +0300 Subject: [PATCH 03/13] fix: improve acronym tokenization in avoid_similar_names lint to support camelCase suffixes --- .../lints/avoid_similar_names/utils/name_tokenizer.dart | 6 ++++-- .../avoid_similar_names_rule_test.dart | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart index b5000450..f82608eb 100644 --- a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart +++ b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart @@ -7,10 +7,12 @@ abstract final class NameTokenizer { /// [A-Z]? | ... optional leading uppercase, /// [a-z]+ | ... followed by lowercase letters. /// | | OR - /// [A-Z]+ | Match acronyms (uppercase letters). + /// [A-Z]+(?=[A-Z][a-z]|\d|\b)| Match acronyms (uppercase letters). /// | | OR /// \d+ | Match digits (e.g., 1, 10). - static final _tokenPattern = RegExp(r'[A-Z]?[a-z]+|[A-Z]+|\d+'); + static final _tokenPattern = RegExp( + r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\b)|\d+', + ); /// Regex Fragment | Meaning /// ================================================================= diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index 33e8551e..e2257838 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -157,6 +157,15 @@ void test() { void process(int Function(int, int) callback) {} process((day1, day2) => day1 + day2); } +'''); + } + + Future test_reports_on_acronym_and_camel_case_suffix() async { + await assertAutoDiagnostics(''' +void test() { + int ${expectLint('APIRequest')} = 1; + int ${expectLint('apiRequest1')} = 2; +} '''); } } From c9e7bb378f5fc234d7fff93c1ee69d7c8da3abd5 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 11:03:19 +0300 Subject: [PATCH 04/13] refactor: support linting of declared identifiers and extract variable collection logic --- .../visitors/local_variables_visitor.dart | 28 +++++++++++++------ .../avoid_similar_names_rule_test.dart | 11 ++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart index 4644a995..56f3d973 100644 --- a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart @@ -1,5 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; /// Collects local variable declarations within a function body, @@ -10,17 +12,16 @@ class LocalVariablesVisitor extends RecursiveAstVisitor { @override void visitVariableDeclaration(VariableDeclaration node) { - if (ScopeVariable.createOrNull( - nameToken: node.name, - type: node.declaredFragment?.element.type, - node: node, - ) - case final variable?) { - variables.add(variable); - } + _collect(node.name, node.declaredFragment?.element.type, node); super.visitVariableDeclaration(node); } + @override + void visitDeclaredIdentifier(DeclaredIdentifier node) { + _collect(node.name, node.declaredFragment?.element.type, node); + super.visitDeclaredIdentifier(node); + } + @override void visitFunctionDeclaration(FunctionDeclaration node) { // Stop traversing nested function scopes. @@ -30,4 +31,15 @@ class LocalVariablesVisitor extends RecursiveAstVisitor { void visitFunctionExpression(FunctionExpression node) { // Stop traversing nested closures. } + + void _collect(Token nameToken, DartType? type, AstNode node) { + final variable = ScopeVariable.createOrNull( + nameToken: nameToken, + type: type, + node: node, + ); + if (variable != null) { + variables.add(variable); + } + } } diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index e2257838..b4f3b015 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -166,6 +166,17 @@ void test() { int ${expectLint('APIRequest')} = 1; int ${expectLint('apiRequest1')} = 2; } +'''); + } + + Future test_reports_on_similar_names_in_for_in_loops() async { + await assertAutoDiagnostics(''' +void test() { + final users = [1, 2]; + for (final ${expectLint('user1')} in users) { + int ${expectLint('user2')} = user1; + } +} '''); } } From 6c569e81eb27cec4b12077578463fef6993e42d1 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 11:21:14 +0300 Subject: [PATCH 05/13] feat: update avoid_similar_names lint to ignore nullability differences when comparing variable types --- .../visitors/avoid_similar_names_visitor.dart | 3 ++- lib/src/utils/types_utils.dart | 7 +++++++ .../avoid_similar_names_rule_test.dart | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index 832c09c6..ea5ace9e 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -4,6 +4,7 @@ import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_ru import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; /// A visitor that checks for variables with /// confusingly similar names. @@ -86,7 +87,7 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { ScopeVariable a, ScopeVariable b, ) { - if (a.type != null && b.type != null && a.type != b.type) { + if (a.type?.isDifferentIgnoringNullability(b.type) ?? false) { return; } diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 64558270..6d60e748 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -75,6 +75,13 @@ extension Subtypes on DartType { return false; } + + /// Compares this type with [other] ignoring nullability where applicable. + bool isDifferentIgnoringNullability(DartType? other) { + if (other == null) return false; + + return (element ?? this) != (other.element ?? other); + } } extension TypeString on String { diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index b4f3b015..db720568 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -43,6 +43,16 @@ void test() { '''); } + Future + test_reports_on_similar_names_with_different_nullability() async { + await assertAutoDiagnostics(''' +void test() { + int ${expectLint('user1')} = 1; + int? ${expectLint('user2')} = 2; +} +'''); + } + Future test_reports_on_borderline_short_names() async { await assertAutoDiagnostics(''' void test() { From 8a1407d332ed834e0f1d1ff773bb9dcbabdb4093 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 11:40:10 +0300 Subject: [PATCH 06/13] refactor: switch to SimpleAstVisitor and remove redundant super calls in AvoidSimilarNamesVisitor --- .../visitors/avoid_similar_names_visitor.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index ea5ace9e..ff88c028 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -8,7 +8,7 @@ import 'package:solid_lints/src/utils/types_utils.dart'; /// A visitor that checks for variables with /// confusingly similar names. -class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { +class AvoidSimilarNamesVisitor extends SimpleAstVisitor { final AvoidSimilarNamesRule _rule; final _reportedNodes = {}; final _collector = LocalVariablesVisitor(); @@ -21,7 +21,6 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { void visitMethodDeclaration( MethodDeclaration node, ) { - super.visitMethodDeclaration(node); _checkScope(node.parameters, node.body); } @@ -29,7 +28,6 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { void visitConstructorDeclaration( ConstructorDeclaration node, ) { - super.visitConstructorDeclaration(node); _checkScope(node.parameters, node.body); } @@ -37,7 +35,6 @@ class AvoidSimilarNamesVisitor extends RecursiveAstVisitor { void visitFunctionDeclaration( FunctionDeclaration node, ) { - super.visitFunctionDeclaration(node); _checkScope( node.functionExpression.parameters, node.functionExpression.body, From 19e9eee09f2886a2deaa4e28a19de9e07bd77091 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 11:48:03 +0300 Subject: [PATCH 07/13] feat: add support for detecting similar names in pattern variable declarations --- .../visitors/local_variables_visitor.dart | 6 ++++++ .../avoid_similar_names_rule_test.dart | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart index 56f3d973..9fe50538 100644 --- a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart @@ -22,6 +22,12 @@ class LocalVariablesVisitor extends RecursiveAstVisitor { super.visitDeclaredIdentifier(node); } + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + _collect(node.name, node.declaredFragment?.element.type, node); + super.visitDeclaredVariablePattern(node); + } + @override void visitFunctionDeclaration(FunctionDeclaration node) { // Stop traversing nested function scopes. diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index db720568..36e22e1d 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -187,6 +187,26 @@ void test() { int ${expectLint('user2')} = user1; } } +'''); + } + + Future + test_reports_on_similar_names_in_pattern_variable_declarations() async { + await assertAutoDiagnostics(''' +void test() { + final (${expectLint('user1')}, ${expectLint('user2')}) = (1, 2); +} +'''); + } + + Future + test_reports_on_similar_names_in_pattern_matching_if_case() async { + await assertAutoDiagnostics(''' +void test(Object obj) { + if (obj case [int ${expectLint('user1')}, int ${expectLint('user2')}]) { + // ... + } +} '''); } } From 03fe8ef767dc144ad023df3d09ffffc3cbe001e1 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 11:53:41 +0300 Subject: [PATCH 08/13] fix: clear reported nodes in avoid similar names visitor to prevent stale lint reports --- .../visitors/avoid_similar_names_visitor.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index ff88c028..9a4726ed 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -45,6 +45,7 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { FormalParameterList? parameters, FunctionBody body, ) { + _reportedNodes.clear(); _collector.variables.clear(); body.accept(_collector); @@ -54,6 +55,7 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { ]; _compareVariables(variables); + _reportedNodes.clear(); } Iterable _extractParameters( From 01d9bfd6c2e8160801ef3b48712557a857909700 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 16 Jul 2026 12:20:47 +0300 Subject: [PATCH 09/13] feat: improve name similarity detection by refining NameTokenizer and adding related tests --- .../utils/name_tokenizer.dart | 1 + .../avoid_similar_names_rule_test.dart | 9 ++++++ .../utils/name_tokenizer_test.dart | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart diff --git a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart index f82608eb..58925281 100644 --- a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart +++ b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart @@ -56,6 +56,7 @@ abstract final class NameTokenizer { List longer, List shorter, ) { + if (longer.length != shorter.length + 1) return false; var i = 0; while (i < shorter.length && longer[i] == shorter[i]) { i++; diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index 36e22e1d..8b2cd274 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -122,6 +122,15 @@ void test() { '''); } + Future test_reports_on_subset_with_multi_digit_number() async { + await assertAutoDiagnostics(''' +void test() { + String ${expectLint('data')} = "a"; + String ${expectLint('data10')} = "b"; +} +'''); + } + Future test_reports_on_mixed_non_descriptive_suffixes() async { await assertAutoDiagnostics(''' void test() { diff --git a/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart b/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart new file mode 100644 index 00000000..55244946 --- /dev/null +++ b/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart @@ -0,0 +1,31 @@ +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; +import 'package:test/test.dart'; + +void main() { + group('NameTokenizer', () { + test('isSubsetWithNonDescriptiveToken guard clause', () { + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken( + ['a', 'descriptive'], + ['a', 'descriptive'], + ), + isFalse, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken( + ['a', 'descriptive'], + ['a'], + ), + isFalse, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken(['a', '1'], ['a']), + isTrue, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken(['a'], ['a', 'b']), + isFalse, + ); + }); + }); +} From a2a026510d995fb76886403e4a2d62d09fc66b6b Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 20 Jul 2026 17:05:42 +0300 Subject: [PATCH 10/13] refactor: simplify avoid_similar_names visitor logic --- .../avoid_similar_names_rule.dart | 7 +- .../visitors/avoid_similar_names_visitor.dart | 64 +++++++------------ lib/src/utils/iterable_utils.dart | 17 +++++ lib/src/utils/types_utils.dart | 7 +- 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart index 83d4f36d..58140dab 100644 --- a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart +++ b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart @@ -59,8 +59,9 @@ class AvoidSimilarNamesRule extends SolidLintRule { ) { final visitor = AvoidSimilarNamesVisitor(this); - registry.addMethodDeclaration(this, visitor); - registry.addConstructorDeclaration(this, visitor); - registry.addFunctionDeclaration(this, visitor); + registry + ..addMethodDeclaration(this, visitor) + ..addConstructorDeclaration(this, visitor) + ..addFunctionDeclaration(this, visitor); } } diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index 9a4726ed..dd31aa37 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -1,9 +1,11 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:collection/collection.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; import 'package:solid_lints/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; /// A visitor that checks for variables with @@ -82,51 +84,36 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { } } - void _comparePair( - ScopeVariable a, - ScopeVariable b, - ) { - if (a.type?.isDifferentIgnoringNullability(b.type) ?? false) { - return; - } - - switch ((a.tokens.length - b.tokens.length).abs()) { - case 0: - _checkSameLengthTokens(a, b); - case 1: - _checkDifferentLengthTokens(a, b); - } - } + void _comparePair(ScopeVariable a, ScopeVariable b) => + a.type?.isDifferentIgnoringNullability(b.type) ?? false + ? null + : switch ((a.tokens.length - b.tokens.length).abs()) { + 0 => _checkSameLengthTokens(a, b), + 1 => _checkSingleTokenLengthDifference(a, b), + _ => null, + }; void _checkSameLengthTokens( ScopeVariable a, ScopeVariable b, ) { - var diffCount = 0; - var diffIndex = -1; - for (var k = 0; k < a.tokens.length; k++) { - if (a.tokens[k] != b.tokens[k]) { - diffCount++; - if (diffCount > 1) return; - diffIndex = k; - } - } - - if (diffCount != 1) return; - - if (NameTokenizer.isNonDescriptiveToken(a.tokens[diffIndex]) && - NameTokenizer.isNonDescriptiveToken(b.tokens[diffIndex])) { + final diff = a.tokens + .zipWithIndexed(b.tokens) + .where((e) => e.$2 != e.$3) + .singleOrNull; + + if (diff != null && + NameTokenizer.isNonDescriptiveToken(a.tokens[diff.$1]) && + NameTokenizer.isNonDescriptiveToken(b.tokens[diff.$1])) { _report(a, b); } } - void _checkDifferentLengthTokens( + void _checkSingleTokenLengthDifference( ScopeVariable a, ScopeVariable b, ) { - final (longer, shorter) = a.tokens.length > b.tokens.length - ? (a.tokens, b.tokens) - : (b.tokens, a.tokens); + final [shorter, longer] = [a.tokens, b.tokens].sortedBy((e) => e.length); if (NameTokenizer.isSubsetWithNonDescriptiveToken( longer, @@ -136,12 +123,7 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { } } - void _report(ScopeVariable a, ScopeVariable b) { - if (_reportedNodes.add(a.node)) { - _rule.reportAtToken(a.nameToken); - } - if (_reportedNodes.add(b.node)) { - _rule.reportAtToken(b.nameToken); - } - } + void _report(ScopeVariable a, ScopeVariable b) => [a, b] + .where((e) => _reportedNodes.add(e.node)) + .forEach((e) => _rule.reportAtToken(e.nameToken)); } diff --git a/lib/src/utils/iterable_utils.dart b/lib/src/utils/iterable_utils.dart index f7037447..3dcf538b 100644 --- a/lib/src/utils/iterable_utils.dart +++ b/lib/src/utils/iterable_utils.dart @@ -25,3 +25,20 @@ extension IterablePairwise on Iterable { } } } + +/// Extension on [Iterable] to zip elements with another iterable. +extension IterableZip on Iterable { + /// Zips this iterable with [other]. + Iterable<(T, U)> zipWith(Iterable other) sync* { + for (var i = 0; i < length && i < other.length; i++) { + yield (elementAt(i), other.elementAt(i)); + } + } + + /// Zips this iterable with [other] and includes the index. + Iterable<(int, T, U)> zipWithIndexed(Iterable other) sync* { + for (var i = 0; i < length && i < other.length; i++) { + yield (i, elementAt(i), other.elementAt(i)); + } + } +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 6d60e748..d3ebac5e 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -77,11 +77,8 @@ extension Subtypes on DartType { } /// Compares this type with [other] ignoring nullability where applicable. - bool isDifferentIgnoringNullability(DartType? other) { - if (other == null) return false; - - return (element ?? this) != (other.element ?? other); - } + bool isDifferentIgnoringNullability(DartType? other) => + other != null && (element ?? this) != (other.element ?? other); } extension TypeString on String { From 6d4504e772c9e65aa9c6480ed0f08f9c91f2723e Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 10:59:13 +0300 Subject: [PATCH 11/13] refactor: decouple lint reporting logic from token comparison methods in avoid_similar_names visitor --- .../visitors/avoid_similar_names_visitor.dart | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index dd31aa37..d403452b 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -84,43 +84,37 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { } } - void _comparePair(ScopeVariable a, ScopeVariable b) => - a.type?.isDifferentIgnoringNullability(b.type) ?? false - ? null - : switch ((a.tokens.length - b.tokens.length).abs()) { - 0 => _checkSameLengthTokens(a, b), - 1 => _checkSingleTokenLengthDifference(a, b), - _ => null, - }; - - void _checkSameLengthTokens( + void _comparePair(ScopeVariable a, ScopeVariable b) { + final lengthDiff = (a.tokens.length - b.tokens.length).abs(); + final shouldReport = + a.type?.isDifferentIgnoringNullability(b.type) != true && + ((lengthDiff == 0 && _checkSameLengthTokens(a, b)) || + (lengthDiff == 1 && _checkSingleTokenLengthDifference(a, b))); + + if (shouldReport) { + _report(a, b); + } + } + + bool _checkSameLengthTokens( ScopeVariable a, ScopeVariable b, ) { final diff = a.tokens .zipWithIndexed(b.tokens) .where((e) => e.$2 != e.$3) - .singleOrNull; + .singleOrNull + ?.$1; - if (diff != null && - NameTokenizer.isNonDescriptiveToken(a.tokens[diff.$1]) && - NameTokenizer.isNonDescriptiveToken(b.tokens[diff.$1])) { - _report(a, b); - } + return diff != null && + NameTokenizer.isNonDescriptiveToken(a.tokens[diff]) && + NameTokenizer.isNonDescriptiveToken(b.tokens[diff]); } - void _checkSingleTokenLengthDifference( - ScopeVariable a, - ScopeVariable b, - ) { + bool _checkSingleTokenLengthDifference(ScopeVariable a, ScopeVariable b) { final [shorter, longer] = [a.tokens, b.tokens].sortedBy((e) => e.length); - if (NameTokenizer.isSubsetWithNonDescriptiveToken( - longer, - shorter, - )) { - _report(a, b); - } + return NameTokenizer.isSubsetWithNonDescriptiveToken(longer, shorter); } void _report(ScopeVariable a, ScopeVariable b) => [a, b] From b53508d4bd468e04eb5366a036f05f9ca4158d8d Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 11:10:39 +0300 Subject: [PATCH 12/13] refactor: simplify avoid_similar_names visitor methods --- .../visitors/avoid_similar_names_visitor.dart | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart index d403452b..e753d87f 100644 --- a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -20,28 +20,18 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { AvoidSimilarNamesVisitor(this._rule); @override - void visitMethodDeclaration( - MethodDeclaration node, - ) { - _checkScope(node.parameters, node.body); - } + void visitMethodDeclaration(MethodDeclaration node) => + _checkScope(node.parameters, node.body); @override - void visitConstructorDeclaration( - ConstructorDeclaration node, - ) { - _checkScope(node.parameters, node.body); - } + void visitConstructorDeclaration(ConstructorDeclaration node) => + _checkScope(node.parameters, node.body); @override - void visitFunctionDeclaration( - FunctionDeclaration node, - ) { - _checkScope( - node.functionExpression.parameters, - node.functionExpression.body, - ); - } + void visitFunctionDeclaration(FunctionDeclaration node) => _checkScope( + node.functionExpression.parameters, + node.functionExpression.body, + ); void _checkScope( FormalParameterList? parameters, @@ -51,12 +41,10 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { _collector.variables.clear(); body.accept(_collector); - final variables = [ + _compareVariables([ ..._extractParameters(parameters), ..._collector.variables, - ]; - - _compareVariables(variables); + ]); _reportedNodes.clear(); } @@ -74,9 +62,7 @@ class AvoidSimilarNamesVisitor extends SimpleAstVisitor { variable, ]; - void _compareVariables( - List variables, - ) { + void _compareVariables(List variables) { for (var i = 0; i < variables.length; i++) { for (var j = i + 1; j < variables.length; j++) { _comparePair(variables[i], variables[j]); From 1d20eeb5f4363d5a430308ed86206593769dea14 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 11:10:56 +0300 Subject: [PATCH 13/13] refactor: simplify avoid_similar_names test suite using helper methods --- .../avoid_similar_names_rule_test.dart | 129 +++++------------- 1 file changed, 33 insertions(+), 96 deletions(-) diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart index 8b2cd274..2e4aa28f 100644 --- a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -25,74 +25,47 @@ class AvoidSimilarNamesRuleTest extends AnalysisRuleTest ); } - Future test_reports_on_similar_names_in_function() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_similar_names_in_function() => _assert(''' int ${expectLint('someClass1')} = 1; int ${expectLint('someClass2')} = 2; -} '''); - } - Future test_does_not_report_on_different_types() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_different_types() => _assertNo(''' String user1 = 'Alice'; int user2 = 123; -} '''); - } - Future - test_reports_on_similar_names_with_different_nullability() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_similar_names_with_different_nullability() => + _assert(''' int ${expectLint('user1')} = 1; int? ${expectLint('user2')} = 2; -} '''); - } - Future test_reports_on_borderline_short_names() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_borderline_short_names() => _assert(''' int ${expectLint('id1')} = 1; int ${expectLint('id2')} = 2; -} '''); - } - Future test_does_not_report_on_different_descriptive_tokens() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_different_descriptive_tokens() => + _assertNo(''' int minHeight = 10; int maxHeight = 20; -} '''); - } - Future test_does_not_report_on_short_names() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_short_names() => _assertNo(''' int x1 = 1; int x2 = 2; int dx = 5; int dy = 10; -} '''); - } - Future test_does_not_report_on_allowed_coordinates() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_allowed_coordinates() => _assertNo(''' double pointX = 1.0; double pointY = 2.0; -} '''); - } - Future test_reports_on_digits_in_parameters() async { - await assertAutoDiagnostics(''' + Future test_reports_on_digits_in_parameters() => + assertAutoDiagnostics(''' bool isEqual( int ${expectLint('someClass1')}, int ${expectLint('someClass2')}, @@ -100,10 +73,9 @@ bool isEqual( return someClass1 == someClass2; } '''); - } - Future test_reports_on_similar_names_in_method() async { - await assertAutoDiagnostics(''' + Future test_reports_on_similar_names_in_method() => + assertAutoDiagnostics(''' class A { void test() { String ${expectLint('tempA')} = "a"; @@ -111,111 +83,76 @@ class A { } } '''); - } - Future test_reports_on_subset_with_extra_digit() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_subset_with_extra_digit() => _assert(''' String ${expectLint('data')} = "a"; String ${expectLint('data1')} = "b"; -} '''); - } - Future test_reports_on_subset_with_multi_digit_number() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_subset_with_multi_digit_number() => _assert(''' String ${expectLint('data')} = "a"; String ${expectLint('data10')} = "b"; -} '''); - } - Future test_reports_on_mixed_non_descriptive_suffixes() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_mixed_non_descriptive_suffixes() => _assert(''' String ${expectLint('user1')} = "a"; String ${expectLint('userA')} = "b"; -} '''); - } - Future test_reports_on_subset_with_extra_letter() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_subset_with_extra_letter() => _assert(''' String ${expectLint('user')} = "a"; String ${expectLint('userA')} = "b"; -} '''); - } - Future test_does_not_report_on_subset_with_descriptive_token() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_subset_with_descriptive_token() => + _assertNo(''' String user = "a"; String userProfile = "b"; String data = "c"; String dataFetch = "d"; -} '''); - } - Future test_reports_on_three_similar_names() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_three_similar_names() => _assert(''' int ${expectLint('id1')} = 1; int ${expectLint('id2')} = 2; int ${expectLint('id3')} = 3; -} '''); - } - Future test_does_not_report_on_anonymous_lambda() async { - await assertNoDiagnostics(''' -void test() { + Future test_does_not_report_on_anonymous_lambda() => _assertNo(''' void process(int Function(int, int) callback) {} process((day1, day2) => day1 + day2); -} '''); - } - Future test_reports_on_acronym_and_camel_case_suffix() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_acronym_and_camel_case_suffix() => _assert(''' int ${expectLint('APIRequest')} = 1; int ${expectLint('apiRequest1')} = 2; -} '''); - } - Future test_reports_on_similar_names_in_for_in_loops() async { - await assertAutoDiagnostics(''' -void test() { + Future test_reports_on_similar_names_in_for_in_loops() => _assert(''' final users = [1, 2]; for (final ${expectLint('user1')} in users) { int ${expectLint('user2')} = user1; } -} '''); - } Future - test_reports_on_similar_names_in_pattern_variable_declarations() async { - await assertAutoDiagnostics(''' -void test() { + test_reports_on_similar_names_in_pattern_variable_declarations() => + _assert(''' final (${expectLint('user1')}, ${expectLint('user2')}) = (1, 2); -} '''); - } - Future - test_reports_on_similar_names_in_pattern_matching_if_case() async { - await assertAutoDiagnostics(''' + Future test_reports_on_similar_names_in_pattern_matching_if_case() => + assertAutoDiagnostics(''' void test(Object obj) { if (obj case [int ${expectLint('user1')}, int ${expectLint('user2')}]) { // ... } } '''); - } + + Future _assert(String body) => + assertAutoDiagnostics('''void test() {$body}'''); + + Future _assertNo(String body) => + assertNoDiagnostics('''void test() {$body}'''); }