Skip to content

Add new rule avoid_similar_names#321

Merged
solid-illiaaihistov merged 14 commits into
solid-software:analysis_server_migrationfrom
solid-illiaaihistov:95-add-avoid_similar_names
Jul 22, 2026
Merged

Add new rule avoid_similar_names#321
solid-illiaaihistov merged 14 commits into
solid-software:analysis_server_migrationfrom
solid-illiaaihistov:95-add-avoid_similar_names

Conversation

@solid-illiaaihistov

Copy link
Copy Markdown
Collaborator

Closes #95

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart Outdated
Comment thread lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart Outdated
@solid-illiaaihistov

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart Outdated
@solid-illiaaihistov

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart Outdated
Comment thread lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart Outdated
@solid-illiaaihistov

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart
Comment thread lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart
Comment thread lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart Outdated
Comment thread lib/src/utils/types_utils.dart Outdated
}

/// Compares this type with [other] ignoring nullability where applicable.
bool isDifferentIgnoringNullability(DartType? other) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other != null && (element ?? this) != (other.element ?? other)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +62 to +64
registry.addMethodDeclaration(this, visitor);
registry.addConstructorDeclaration(this, visitor);
registry.addFunctionDeclaration(this, visitor);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
registry.addMethodDeclaration(this, visitor);
registry.addConstructorDeclaration(this, visitor);
registry.addFunctionDeclaration(this, visitor);
registry
..addMethodDeclaration(this, visitor)
..addConstructorDeclaration(this, visitor)
..addFunctionDeclaration(this, visitor);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +139 to +146
void _report(ScopeVariable a, ScopeVariable b) {
if (_reportedNodes.add(a.node)) {
_rule.reportAtToken(a.nameToken);
}
if (_reportedNodes.add(b.node)) {
_rule.reportAtToken(b.nameToken);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  void _report(ScopeVariable a, ScopeVariable b) => [a, b]
      .where((e) => _reportedNodes.add(e.node))
      .forEach((e) => _rule.reportAtToken(e.nameToken));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +127 to +134
final (longer, shorter) = a.tokens.length > b.tokens.length
? (a.tokens, b.tokens)
: (b.tokens, a.tokens);

if (NameTokenizer.isSubsetWithNonDescriptiveToken(
longer,
shorter,
)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}
}

void _checkSameLengthTokens(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    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));
    }
  }
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +85 to +99
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);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
        };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not ternary chain?

a
? b
: c
? d
: e

is idiom for

if (a) {
  b;
} else if (c) {
  d;
} else {
  e;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
  }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +23 to +27
void visitMethodDeclaration(
MethodDeclaration node,
) {
_checkScope(node.parameters, node.body);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void visitMethodDeclaration(
MethodDeclaration node,
) {
_checkScope(node.parameters, node.body);
}
void visitMethodDeclaration(MethodDeclaration node) =>
_checkScope(node.parameters, node.body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +30 to +34
void visitConstructorDeclaration(
ConstructorDeclaration node,
) {
_checkScope(node.parameters, node.body);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void visitConstructorDeclaration(
ConstructorDeclaration node,
) {
_checkScope(node.parameters, node.body);
}
void visitConstructorDeclaration(ConstructorDeclaration node) =>
_checkScope(node.parameters, node.body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +37 to +44
void visitFunctionDeclaration(
FunctionDeclaration node,
) {
_checkScope(
node.functionExpression.parameters,
node.functionExpression.body,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void visitFunctionDeclaration(
FunctionDeclaration node,
) {
_checkScope(
node.functionExpression.parameters,
node.functionExpression.body,
);
}
void visitFunctionDeclaration(FunctionDeclaration node) => _checkScope(
node.functionExpression.parameters,
node.functionExpression.body,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +54 to +59
final variables = [
..._extractParameters(parameters),
..._collector.variables,
];

_compareVariables(variables);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final variables = [
..._extractParameters(parameters),
..._collector.variables,
];
_compareVariables(variables);
_compareVariables([
..._extractParameters(parameters),
..._collector.variables,
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +77 to +79
void _compareVariables(
List<ScopeVariable> variables,
) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void _compareVariables(
List<ScopeVariable> variables,
) {
void _compareVariables(List<ScopeVariable> variables) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(...);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@solid-danylosafonov solid-danylosafonov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@solid-illiaaihistov
solid-illiaaihistov merged commit 24d6168 into solid-software:analysis_server_migration Jul 22, 2026
1 check passed
@solid-illiaaihistov
solid-illiaaihistov deleted the 95-add-avoid_similar_names branch July 22, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants