Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'upstream/master' into refactor/simplify…
…-avoid-duplicate-code
  • Loading branch information
Illia Aihistov
Illia Aihistov committed Aug 21, 2026
commit 9d67cdc5f173047a475a2fcdd3577da8db5d0c53
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:analyzer/error/error.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart';
import 'package:solid_lints/src/models/solid_multi_lint_rule.dart';
import 'package:solid_lints/src/utils/ignore_matcher.dart';

/// A lint rule that detects duplicated code blocks (clones) across the project.
///
Expand Down Expand Up @@ -103,14 +104,13 @@ import 'package:solid_lints/src/models/solid_multi_lint_rule.dart';
/// ### Example config:
///
/// ```yaml
/// plugins:
/// solid_lints:
/// diagnostics:
/// avoid_duplicate_code:
/// min_tokens: 30
/// exclude:
/// - method_name: initState
/// - method_name: dispose
/// solid_lints:
/// diagnostics:
/// avoid_duplicate_code:
/// min_tokens: 30
/// exclude:
/// - method_name: initState
/// - method_name: dispose
/// ```
class AvoidDuplicateCodeRule
extends SolidMultiLintRule<AvoidDuplicateCodeParameters> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ class GlobalHashRegistry {
AvoidDuplicateCodeParameters? parameters,
String? packageRoot,
}) {
if (entries.isEmpty) {
removeFile(
filePath,
resourceProvider: resourceProvider,
parameters: parameters,
packageRoot: packageRoot,
);
return;
}

final root = _getRoot(packageRoot);
final absoluteFilePath = _resolveAndLoad(
filePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:collection/collection.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/models/analyzed_candidate.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart';
Expand All @@ -14,10 +16,10 @@ import 'package:solid_lints/src/lints/avoid_duplicate_code/reporters/avoid_dupli
import 'package:solid_lints/src/lints/avoid_duplicate_code/reporters/duplicate_report_context.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/services/differing_literals_analyzer.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/services/global_hash_registry.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/token_utils.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart';
import 'package:solid_lints/src/utils/ignore_matcher.dart';

/// A visitor that detects duplicate code blocks (at the function level and/or
/// statement block level) within a single compilation unit and across files,
Expand All @@ -30,6 +32,8 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
final ContextRoot? _contextRoot;
final ResourceProvider _resourceProvider;
final AvoidDuplicateCodeReporter _reporter;
final AnalysisOptionsLoader? _analysisOptionsLoader;
final IgnoreMatcher _ignoreMatcher;

/// Creates a new instance of [AvoidDuplicateCodeVisitor].
factory AvoidDuplicateCodeVisitor(
Expand All @@ -40,6 +44,7 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
required IgnoreMatcher ignoreMatcher,
ContextRoot? contextRoot,
ResourceProvider? resourceProvider,
AnalysisOptionsLoader? analysisOptionsLoader,
}) {
final effectiveResourceProvider =
resourceProvider ?? PhysicalResourceProvider.INSTANCE;
Expand All @@ -57,6 +62,8 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
contextRoot: contextRoot,
resourceProvider: effectiveResourceProvider,
reporter: reporter,
analysisOptionsLoader: analysisOptionsLoader,
ignoreMatcher: ignoreMatcher,
);
}

Expand All @@ -67,39 +74,58 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
required ContextRoot? contextRoot,
required ResourceProvider resourceProvider,
required AvoidDuplicateCodeReporter reporter,
required AnalysisOptionsLoader? analysisOptionsLoader,
required IgnoreMatcher ignoreMatcher,
}) : _parameters = parameters,
_filePath = filePath,
_modificationStamp = modificationStamp,
_contextRoot = contextRoot,
_resourceProvider = resourceProvider,
_reporter = reporter;
_reporter = reporter,
_analysisOptionsLoader = analysisOptionsLoader,
_ignoreMatcher = ignoreMatcher;

@override
void visitCompilationUnit(CompilationUnit node) {
if (_filePath.isEmpty) return;
final filePath = _filePath;

final packageRoot =
_contextRoot?.root.path ??
GlobalHashRegistry.instance.findPackageRoot(
_filePath,
filePath,
resourceProvider: _resourceProvider,
) ??
'';

if (_tryReportFromCache(_filePath, packageRoot)) return;
final isExcluded =
_analysisOptionsLoader?.isFileExcludedForFile(filePath) ?? false;

final rawCandidates = _collectCandidates(node);
if (rawCandidates.isEmpty) return;
if (isExcluded || _ignoreMatcher.isFileIgnored(node)) {
GlobalHashRegistry.instance.removeFile(
filePath,
resourceProvider: _resourceProvider,
parameters: _parameters,
packageRoot: packageRoot,
);
return;
}

if (_tryReportFromCache(filePath, packageRoot)) return;

final rawCandidates = _collectCandidates(node);
final candidates = _analyzeCandidates(node, rawCandidates);

final crossFileDuplicatesByHash = _findAndSaveCrossFileMatches(
_filePath,
filePath,
candidates.map((c) => c.entry).toList(),
packageRoot,
);

if (candidates.isEmpty) return;

_reporter.report(
filePath: _filePath,
filePath: filePath,
contexts: DuplicateReportContext.fromAstCandidates(candidates),
crossFileDuplicatesByHash: crossFileDuplicatesByHash,
);
Expand Down Expand Up @@ -130,6 +156,19 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
}).toList();
}

List<BodyCandidate> _collectCandidates(CompilationUnit node) {
final collector = CandidateVisitor(_parameters);
node.accept(collector);
return collector.candidates
.whereNot(
(c) => _ignoreMatcher.isCandidateIgnored(
c.node,
c.enclosingDeclaration,
),
)
.toList();
}

bool _tryReportFromCache(String filePath, String packageRoot) {
if (packageRoot.isEmpty) return false;

Expand Down Expand Up @@ -181,8 +220,6 @@ class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor<void> {
List<HashEntry> hashEntries,
String packageRoot,
) {
if (packageRoot.isEmpty) return const {};

final registry = GlobalHashRegistry.instance;
final crossMatches = registry.findCrossFileMatches(
filePath,
Expand Down
16 changes: 11 additions & 5 deletions lib/src/utils/ignore_matcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ final class IgnoreMatcher {
].commentLexemes.any(_fileIgnoreRegex.hasMatch);

/// Checks if a candidate [node] or its enclosing [declaration] is ignored.
bool isCandidateIgnored(AstNode node, [Declaration? declaration]) => [
?declaration?.beginToken,
?declaration?.firstTokenAfterCommentAndMetadata,
node.beginToken,
].commentLexemes.any(_lineIgnoreRegex.hasMatch);
bool isCandidateIgnored(AstNode node, [Declaration? declaration]) {

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.

not really sure why we are changing this

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 added upTo to support ignore comments for cases like:

void foo(
  int a,
  int b,
  // ignore: $ruleName
) {
  final x = 1;
}

Also simplified the function:

 bool isCandidateIgnored(AstNode node, [Declaration? declaration]) => [
    ?declaration?.beginToken,
    ...?declaration?.firstTokenAfterCommentAndMetadata.upTo(node.beginToken),
    node.beginToken,
  ].commentLexemes.any(_lineIgnoreRegex.hasMatch);

final tokens = [
if (declaration case final decl?) ...[
decl.beginToken,
...decl.firstTokenAfterCommentAndMetadata.upTo(node.beginToken),
],
node.beginToken,
];

return tokens.commentLexemes.any(_lineIgnoreRegex.hasMatch);
}
}
13 changes: 13 additions & 0 deletions lib/src/utils/token_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ extension TokenUtils on Token {
yield c;
}
}

/// Returns an iterable sequence of tokens starting from this token up to
/// (and including) [end].
Iterable<Token> upTo(Token end) sync* {

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 can extract a function that we can reuse for other cases where we are generating an iterable from iterator like objects:

  Iterable<Token> upTo(Token end) => generate(
    this,
    (e) => e == end || e.next == e ? null : e.next,
  );
}

Iterable<T> generate<T>(T seed, T? Function(T) next) sync* {
  var current = seed;
  while (true) {
    yield current;
    final n = next(current);
    if (n == null) break;
    current = n;
  }
}

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.

Thank you!
I decided to name the new function iterate instead of generate, as I think it better reflects its purpose.

var current = this;
while (true) {
yield current;
if (current == end) break;
final next = current.next;
if (next == null || next == current) break;
current = next;
}
}
}

/// Extension methods for [Iterable<Token>] manipulation.
Expand Down
79 changes: 79 additions & 0 deletions test/src/common/utils/ignore_matcher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,85 @@ void foo() {
expect(matcher.isCandidateIgnored(innerBlock, null), isTrue);
});

test(
'returns true when inline ignore is placed inside parameter list',
() {
final (body, decl) = _parseFunction('''
void foo(
int a,
// ignore: $ruleName
int b,
) {
final x = 1;
}
''');

expect(matcher.isCandidateIgnored(body, decl), isTrue);
},
);

test(
'returns true when inline ignore is placed before closing parenthesis',
() {
final (body, decl) = _parseFunction('''
void foo(
int a,
int b,
// ignore: $ruleName
) {
final x = 1;
}
''');

expect(matcher.isCandidateIgnored(body, decl), isTrue);
},
);

test(
'returns true when package-prefixed inline ignore is placed before closing parenthesis',
() {
final (body, decl) = _parseFunction('''
void foo(
int a,
int b,
// ignore: solid_lints/$ruleName
) {
final x = 1;
}
''');

expect(matcher.isCandidateIgnored(body, decl), isTrue);
},
);

test(
'returns true for method inside class with ignore in parameters',
() {
final result = parseString(
content:
'''
abstract class Foo {
static void bar(
int a,
int b,
// ignore: solid_lints/$ruleName
) {
final x = 1;
}
}
''',
);
final classDecl = result.unit.declarations.first as ClassDeclaration;
final methodDecl = switch (classDecl.body) {
BlockClassBody(:final members) =>
members.first as MethodDeclaration,
_ => throw StateError('Expected BlockClassBody'),
};
final body = (methodDecl.body as BlockFunctionBody).block;
expect(matcher.isCandidateIgnored(body, methodDecl), isTrue);
},
);

test('returns false when declaration is not ignored', () {
final (body, decl) = _parseFunction('''
void foo() {
Expand Down
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.