Add new rule avoid_similar_names#321
Conversation
… in avoid_similar_names lint
There was a problem hiding this comment.
Code Review
This pull request introduces a new lint rule, avoid_similar_names, which warns about variables or parameters with confusingly similar names within the same function scope. It includes the rule implementation, AST visitors, tokenization utilities, and corresponding tests. The feedback highlights three key areas for improvement: refining the tokenization regular expression to correctly handle acronyms followed by capitalized words, extending LocalVariablesVisitor to collect variables from for-in loops, and adjusting the type comparison logic to allow comparisons between nullable and non-nullable versions of the same type.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…ort camelCase suffixes
…e collection logic
…es when comparing variable types
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new lint rule avoid_similar_names to detect confusingly similar variable and parameter names within the same scope. The feedback highlights two important improvements: changing the visitor base class to SimpleAstVisitor to avoid double-traversal performance issues, and extending LocalVariablesVisitor to support Dart 3.0 pattern variable declarations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
… in AvoidSimilarNamesVisitor
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new lint rule, avoid_similar_names, which warns about variables or parameters with confusingly similar names within the same function scope. Feedback on the changes highlights a potential memory leak in AvoidSimilarNamesVisitor due to _reportedNodes never being cleared, and suggests registering FunctionExpression instead of FunctionDeclaration to support analyzing anonymous closures as well as named functions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…tale lint reports
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a new lint rule, avoid_similar_names, to detect variables or parameters with confusingly similar names within the same function scope. The feedback recommends adding an explicit length check in isSubsetWithNonDescriptiveToken to prevent potential out-of-bounds errors. Additionally, it suggests registering and visiting FunctionExpression instead of FunctionDeclaration to extend the analysis to anonymous closures.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
… adding related tests
| } | ||
|
|
||
| /// Compares this type with [other] ignoring nullability where applicable. | ||
| bool isDifferentIgnoringNullability(DartType? other) { |
There was a problem hiding this comment.
other != null && (element ?? this) != (other.element ?? other)
| registry.addMethodDeclaration(this, visitor); | ||
| registry.addConstructorDeclaration(this, visitor); | ||
| registry.addFunctionDeclaration(this, visitor); |
There was a problem hiding this comment.
| registry.addMethodDeclaration(this, visitor); | |
| registry.addConstructorDeclaration(this, visitor); | |
| registry.addFunctionDeclaration(this, visitor); | |
| registry | |
| ..addMethodDeclaration(this, visitor) | |
| ..addConstructorDeclaration(this, visitor) | |
| ..addFunctionDeclaration(this, visitor); |
| void _report(ScopeVariable a, ScopeVariable b) { | ||
| if (_reportedNodes.add(a.node)) { | ||
| _rule.reportAtToken(a.nameToken); | ||
| } | ||
| if (_reportedNodes.add(b.node)) { | ||
| _rule.reportAtToken(b.nameToken); | ||
| } | ||
| } |
There was a problem hiding this comment.
void _report(ScopeVariable a, ScopeVariable b) => [a, b]
.where((e) => _reportedNodes.add(e.node))
.forEach((e) => _rule.reportAtToken(e.nameToken));| final (longer, shorter) = a.tokens.length > b.tokens.length | ||
| ? (a.tokens, b.tokens) | ||
| : (b.tokens, a.tokens); | ||
|
|
||
| if (NameTokenizer.isSubsetWithNonDescriptiveToken( | ||
| longer, | ||
| shorter, | ||
| )) { |
There was a problem hiding this comment.
| final (longer, shorter) = a.tokens.length > b.tokens.length | |
| ? (a.tokens, b.tokens) | |
| : (b.tokens, a.tokens); | |
| if (NameTokenizer.isSubsetWithNonDescriptiveToken( | |
| longer, | |
| shorter, | |
| )) { | |
| final [shorter, longer] = [a.tokens, b.tokens].sortedBy((e) => e.length); | |
| if (NameTokenizer.isSubsetWithNonDescriptiveToken(longer, shorter)) { |
| } | ||
| } | ||
|
|
||
| void _checkSameLengthTokens( |
There was a problem hiding this comment.
final diffs = a.tokens.zipWithIndexed(b.tokens).where((e) => e.$2 != e.$3);
if (diffs.length >= 2 &&
NameTokenizer.isNonDescriptiveToken(a.tokens[diffs.first.$1]) &&
NameTokenizer.isNonDescriptiveToken(b.tokens[diffs.first.$1])) {
_report(a, b);
}extension<T> on Iterable<T> {
Iterable<(T, U)> zipWith<U>(Iterable<U> other) sync* {
for (var i = 0; i < length && i < other.length; i++) {
yield (elementAt(i), other.elementAt(i));
}
}
Iterable<(int, T, U)> zipWithIndexed<U>(Iterable<U> other) sync* {
for (var i = 0; i < length && i < other.length; i++) {
yield (i, elementAt(i), other.elementAt(i));
}
}
}There was a problem hiding this comment.
I replaced the length check with singleOrNull to leverage lazy evaluation (as calculating the diff length would require iterating over all elements). However, for typical variable name scenarios, performance still dropped by 5–10x (depending on the number of tokens in the variable name) compared to a standard loop that doesn't generate collections. I kept the version with zipWithIndexed.
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);
}There was a problem hiding this comment.
5–10x
I think we shouldn't worry about values like that. If we worry that performance decreased dramatically it'd be better to compare runtime over a real code base (e.g. worklog)
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
| 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 | |
| : a.tokens.length == b.tokens.length | |
| ? _checkSameLengthTokens(a, b) | |
| : _checkDifferentLengthTokens(a, b); |
There was a problem hiding this comment.
Implemented the following approach:
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,
};There was a problem hiding this comment.
why not ternary chain?
a
? b
: c
? d
: eis idiom for
if (a) {
b;
} else if (c) {
d;
} else {
e;
}There was a problem hiding this comment.
I think we should make the _checkX return bool instead though.
Then we can completely remove that ternary, something like:
void _comparePair(ScopeVariable a, ScopeVariable b) {
final shouldReport =
a.type?.isDifferentIgnoringNullability(b.type) != true &&
(a.tokens.length == b.tokens.length && _checkSameLengthTokens(a, b) ||
_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
?.$1;
return diff != null &&
NameTokenizer.isNonDescriptiveToken(a.tokens[diff]) &&
NameTokenizer.isNonDescriptiveToken(b.tokens[diff]);
}
bool _checkSingleTokenLengthDifference(ScopeVariable a, ScopeVariable b) {
final [shorter, longer] = [a.tokens, b.tokens].sortedBy((e) => e.length);
return NameTokenizer.isSubsetWithNonDescriptiveToken(longer, shorter);
}| void visitMethodDeclaration( | ||
| MethodDeclaration node, | ||
| ) { | ||
| _checkScope(node.parameters, node.body); | ||
| } |
There was a problem hiding this comment.
| void visitMethodDeclaration( | |
| MethodDeclaration node, | |
| ) { | |
| _checkScope(node.parameters, node.body); | |
| } | |
| void visitMethodDeclaration(MethodDeclaration node) => | |
| _checkScope(node.parameters, node.body); |
| void visitConstructorDeclaration( | ||
| ConstructorDeclaration node, | ||
| ) { | ||
| _checkScope(node.parameters, node.body); | ||
| } |
There was a problem hiding this comment.
| void visitConstructorDeclaration( | |
| ConstructorDeclaration node, | |
| ) { | |
| _checkScope(node.parameters, node.body); | |
| } | |
| void visitConstructorDeclaration(ConstructorDeclaration node) => | |
| _checkScope(node.parameters, node.body); |
| void visitFunctionDeclaration( | ||
| FunctionDeclaration node, | ||
| ) { | ||
| _checkScope( | ||
| node.functionExpression.parameters, | ||
| node.functionExpression.body, | ||
| ); | ||
| } |
There was a problem hiding this comment.
| void visitFunctionDeclaration( | |
| FunctionDeclaration node, | |
| ) { | |
| _checkScope( | |
| node.functionExpression.parameters, | |
| node.functionExpression.body, | |
| ); | |
| } | |
| void visitFunctionDeclaration(FunctionDeclaration node) => _checkScope( | |
| node.functionExpression.parameters, | |
| node.functionExpression.body, | |
| ); |
| final variables = [ | ||
| ..._extractParameters(parameters), | ||
| ..._collector.variables, | ||
| ]; | ||
|
|
||
| _compareVariables(variables); |
There was a problem hiding this comment.
| final variables = [ | |
| ..._extractParameters(parameters), | |
| ..._collector.variables, | |
| ]; | |
| _compareVariables(variables); | |
| _compareVariables([ | |
| ..._extractParameters(parameters), | |
| ..._collector.variables, | |
| ]); | |
| void _compareVariables( | ||
| List<ScopeVariable> variables, | ||
| ) { |
There was a problem hiding this comment.
| void _compareVariables( | |
| List<ScopeVariable> variables, | |
| ) { | |
| void _compareVariables(List<ScopeVariable> variables) { |
There was a problem hiding this comment.
we can extract something like:
Future<void> _assert(String source) =>
assertAutoDiagnostics('''void test() {$source}''');instead of
Future<void> f(...) async {
await f2(...);
}we should do
Future<void> f(...) => f2(...);…o 95-add-avoid_similar_names
24d6168
into
solid-software:analysis_server_migration
Closes #95