Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0012940
feat: add avoid_duplicate_code lint rule
Jul 8, 2026
77f24f8
feat: increase default min_statements for avoid_duplicate_code from 3…
Jul 8, 2026
d8e267e
feat: enable cross-file duplicate code detection by implementing a gl…
Jul 9, 2026
f57a284
chore: remove avoid_using_api from analysis exclude list
Jul 10, 2026
6c3900b
refactor: introduce SolidDiagnosticMessage to encapsulate diagnostic …
Jul 13, 2026
59e6884
feat: register SolidLintsPlugin in pubspec and remove redundant analy…
Jul 13, 2026
74b6e93
refactor: optimize duplicate code detection with Jenkins hashing, str…
Jul 13, 2026
94b1d74
refactor: simplify avoid_duplicate_code parameters by removing defaul…
Jul 13, 2026
f4918d3
refactor: simplify duplicate code detection logic by extracting visit…
Jul 14, 2026
a18f796
refactor: modularize duplicate detection logic by introducing special…
Jul 14, 2026
f1b870e
chore: remove exclusion for avoid_using_api lint rules
Jul 14, 2026
80f888a
fix: use stable strings instead of runtime hash codes in AST structur…
Jul 14, 2026
d0611f9
fix: improve accuracy and stability of duplicate code detection by im…
Jul 14, 2026
ce36e02
refactor: remove unnecessary empty line in global_hash_registry.dart
Jul 14, 2026
1b55728
refactor: fix indentation in AvoidDuplicateCodeParameters hashCode me…
Jul 14, 2026
5e8e393
docs: update changelog with avoid_duplicate_code rule and formatting …
Jul 15, 2026
2faba00
chore: update default minimum tokens to 30 for avoid_duplicate_code lint
Jul 15, 2026
44de4fa
feat: add equality operators and hashCode overrides to parameter mode…
Jul 15, 2026
6b6fde6
fix: handle corrupted cache files in HashCacheStorage and add regress…
Jul 15, 2026
8a766a6
test: update avoid_duplicate_code_rule_test to include excluded ident…
Jul 15, 2026
4c88728
fix: use set for deletedFiles and skip them when building the inverte…
Jul 15, 2026
c2af6ec
fix: prevent infinite loop in token counting by returning count when …
Jul 15, 2026
46ef176
fix: improve path precision in duplicate code detection to avoid sibl…
Jul 15, 2026
5aff0b5
refactor: enable independent debounce logic for different package roo…
Jul 15, 2026
3ee9826
fix: catch all exceptions during file operations in hash cache storag…
Jul 15, 2026
1a62878
fix: target specific DuplicateLocation for removal in global hash reg…
Jul 15, 2026
89b2051
fix: resolve package-specific parameter retrieval during debounced ca…
Jul 15, 2026
c9f6df2
refactor: inject ResourceProvider into avoid_duplicate_code services …
Jul 15, 2026
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
45 changes: 28 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
## 1.0.0

- feat!: migrate to analyzer_server_plugin
- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast`
- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's
`unnecessary_cast`
- Added `use_nearest_context` rule.
- Added `avoid_duplicate_code` rule.

## 0.3.3

Expand All @@ -17,9 +19,9 @@

- Added `allow_with_comments` parameter for `no_empty_block` lint.
- Added extension support for `avoid_using_api`
- Added `exclude_entity` parameter for `prefer_match_file_name` lint.
It is now possible to configure this lint to ignore `enum`,
`extension` and `mixin` declarations via `analysis_options.yaml`.
- Added `exclude_entity` parameter for `prefer_match_file_name` lint. It is now
possible to configure this lint to ignore `enum`, `extension` and `mixin`
declarations via `analysis_options.yaml`.

## 0.3.0

Expand All @@ -30,7 +32,8 @@
- `function_lines_of_code`
- `no_empty_bloc`
- `number_of_parameters`
- BREAKING CHANGE: Renamed `excludeNames` parameter to `exclude` for `function_lines_of_code` lint.
- BREAKING CHANGE: Renamed `excludeNames` parameter to `exclude` for
`function_lines_of_code` lint.
- Fixed an issue with `prefer_early_retrun` for throw expression
- `number_of_parameters` lint: added `copyWith` to the default exclude list.
- Update dependencies:
Expand All @@ -53,28 +56,36 @@
## 0.2.0

- Added `avoid_final_with_getter` rule
- Improve `avoid_late_keyword` - `ignored_types` to support ignoring subtype of the node type (<https://github.com/solid-software/solid_lints/issues/157>)
- Abstract methods should be omitted by `proper_super_calls` (<https://github.com/solid-software/solid_lints/issues/159>)
- Add a rule prefer_guard_clause for reversing nested if statements (<https://github.com/solid-software/solid_lints/issues/91>)
- add exclude params support to avoid_returning_widgets rule (<https://github.com/solid-software/solid_lints/issues/131>)
- add quick fix to avoid_final_with_getter (<https://github.com/solid-software/solid_lints/pull/164>)
- Improve `avoid_late_keyword` - `ignored_types` to support ignoring subtype of
the node type (<https://github.com/solid-software/solid_lints/issues/157>)
- Abstract methods should be omitted by `proper_super_calls`
(<https://github.com/solid-software/solid_lints/issues/159>)
- Add a rule prefer_guard_clause for reversing nested if statements
(<https://github.com/solid-software/solid_lints/issues/91>)
- add exclude params support to avoid_returning_widgets rule
(<https://github.com/solid-software/solid_lints/issues/131>)
- add quick fix to avoid_final_with_getter
(<https://github.com/solid-software/solid_lints/pull/164>)
- Renamed `avoid_debug_print` to `avoid_debug_print_in_release`
- The `avoid_debug_print_in_release` no longer reports a warning if the `debugPrint` call is wrapped in a `!kReleaseMode` check.
- The `avoid_debug_print_in_release` no longer reports a warning if the
`debugPrint` call is wrapped in a `!kReleaseMode` check.
- Update custom_lints to work with newer Flutter

## 0.1.5

- Added `avoid_debug_print` rule
- Fixed an issue with no_magic_number lint
- Fixed `avoid_unused_parameters` to report positional parameters from typedef if their name are not underscores.
- Fixed `avoid_unused_parameters` to report positional parameters from typedef
if their name are not underscores.
- Improvement for `avoid_returning_widget` lint:
- ignores methods that override ones that return widget (build() for example)
- no longer allows returning widgets from methods/functions named build
- Fixed unexpected avoid_unnecessary_type_assertions
- Added `excludeNames` param for `function_lines_of_code` lint
- Improved `avoid_unrelated_type_assertions` to support true and false results
- Set default `cyclomatic_complexity` to 10 (<https://github.com/solid-software/solid_lints/issues/146>)
Credits: Arthur Miranda (<https://github.com/arthurbcd>)
- Set default `cyclomatic_complexity` to 10
(<https://github.com/solid-software/solid_lints/issues/146>) Credits: Arthur
Miranda (<https://github.com/arthurbcd>)

## 0.1.4

Expand Down Expand Up @@ -108,8 +119,7 @@
- avoid_unnecessary_type_casts
- avoid_unrelated_type_assertions
- avoid_unused_parameters
- avoid_using_api
Credits: getBoolean (<https://github.com/getBoolean>)
- avoid_using_api Credits: getBoolean (<https://github.com/getBoolean>)
- cyclomatic_complexity
- double_literal_format
- function_lines_of_code
Expand Down Expand Up @@ -176,7 +186,8 @@

## 0.0.13

- enable use_colored_box & use_decorated_box + add more comments about specific lints
- enable use_colored_box & use_decorated_box + add more comments about specific
lints

## 0.0.12

Expand Down
8 changes: 6 additions & 2 deletions lib/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
analyzer:
plugins:
- custom_lint
exclude:
# General generated files
- "**/*.g.dart"
Expand Down Expand Up @@ -38,6 +36,12 @@ analyzer:
solid_lints:
diagnostics:
avoid_global_state: true
avoid_duplicate_code:
min_tokens: 30
check_blocks: true
exclude:
- method_name: initState
- method_name: dispose

avoid_late_keyword:
allow_initialized: true
Expand Down
3 changes: 3 additions & 0 deletions lib/analysis_options_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ solid_lints:
avoid_late_keyword: false
# It's acceptable to include stubs or other helper classes into the test file.
prefer_match_file_name: false
# Test files inherently contain a lot of duplicated setups, teardowns, and mock structures.
# Running duplicate code detection on them usually produces a lot of noise.
avoid_duplicate_code: false
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:analysis_server_plugin/plugin.dart';
import 'package:analysis_server_plugin/registry.dart';
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart';
import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart';
import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart';
import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart';
Expand Down Expand Up @@ -70,6 +71,7 @@ class SolidLintsPlugin extends Plugin {
AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader),
avoidUnnecessaryTypeAssertionsRule,
AvoidDebugPrintInReleaseRule(),
AvoidDuplicateCodeRule(analysisOptionsLoader: analysisLoader),
doubleLiteralFormatRule,
ProperSuperCallsRule(),
NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader),
Expand Down
25 changes: 24 additions & 1 deletion lib/src/common/parameters/excluded_identifier_parameter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,31 @@ class ExcludedIdentifierParameter {
Map<dynamic, dynamic> json,
) {
return ExcludedIdentifierParameter(
methodName: json['method_name'] as String,
methodName: json['method_name'] as String?,
className: json['class_name'] as String?,
declarationName: json['declaration_name'] as String?,
);
}

/// Method to convert parameter to JSON Map.
Map<String, Object?> toJson() => {
if (methodName != null) 'method_name': methodName,
if (className != null) 'class_name': className,
if (declarationName != null) 'declaration_name': declarationName,
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ExcludedIdentifierParameter &&
other.methodName == methodName &&
other.className == className &&
other.declarationName == declarationName;

@override
int get hashCode => Object.hash(
methodName,
className,
declarationName,
);
}
13 changes: 13 additions & 0 deletions lib/src/common/parameters/excluded_identifiers_list_parameter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,17 @@ class ExcludedIdentifiersListParameter {
classDeclaration.namePart.typeName.lexeme == className;
}
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ExcludedIdentifiersListParameter &&
const ListEquality<ExcludedIdentifierParameter>().equals(
other.exclude,
exclude,
);

@override
int get hashCode =>
const ListEquality<ExcludedIdentifierParameter>().hash(exclude);
}
81 changes: 81 additions & 0 deletions lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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_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_lint_rule.dart';

/// A lint rule that detects duplicated code blocks (clones) within a single
/// file and across multiple files in the project.
///
/// When two or more function/method/constructor bodies have structurally
/// identical AST subtrees (Type 2 clones — same structure, variable names
/// may differ), the rule reports all copies and provides context messages
/// linking to the other occurrences.
///
/// Cross-file detection works on a "best-effort" basis: duplicates are
/// detected against files that have already been analyzed in the current
/// session. The detection improves as more files are analyzed.
///
/// ### Example config:
///
/// ```yaml
/// plugins:
/// solid_lints:
/// diagnostics:
/// avoid_duplicate_code:
/// min_tokens: 50
/// ignore_literals: false
/// ignore_identifiers: true
/// check_blocks: false
/// exclude:
/// - method_name: build
/// ```
class AvoidDuplicateCodeRule
extends SolidLintRule<AvoidDuplicateCodeParameters> {
/// Name of the lint.
static const lintName = 'avoid_duplicate_code';

static const _code = LintCode(
lintName,
'Perhaps this code is a duplicate.\n'
'Consider extracting the shared logic into a common function.',
);

@override
DiagnosticCode get diagnosticCode => _code;

/// Creates a new instance of [AvoidDuplicateCodeRule].
AvoidDuplicateCodeRule({
required super.analysisOptionsLoader,
}) : super.withParameters(
name: lintName,
description:
'Detects structurally identical function/method bodies '
'within a single file and across files (code clones).',
parametersParser: AvoidDuplicateCodeParameters.fromJson,
);

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
super.registerNodeProcessors(registry, context);

final parameters =
getParametersForContext(context) ??
AvoidDuplicateCodeParameters.empty();

final visitor = AvoidDuplicateCodeVisitor(
this,
parameters,
filePath: context.definingUnit.file.path,
modificationStamp: context.definingUnit.file.modificationStamp,
contextRoot: context.libraryElement?.session.analysisContext.contextRoot,
Comment thread
solid-illiaaihistov marked this conversation as resolved.
resourceProvider: context.definingUnit.file.provider,
);

registry.addCompilationUnit(this, visitor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';

/// Configuration parameters for the avoid_duplicate_code rule.
class AvoidDuplicateCodeParameters {
/// Minimum number of tokens in a function body or block to be considered
/// for clone detection. Bodies/blocks shorter than this are ignored.
final int minTokens;

/// When `true`, literal values (strings, numbers, booleans) are excluded
/// from the structural hash, making the check ignore literal differences.
final bool ignoreLiterals;

/// When `true`, local variable and parameter names are excluded
/// from the structural hash, allowing detection of renamed variables
/// (Type 2). Note that method, class, and field names are NOT ignored
/// to prevent excessive false positives.
final bool ignoreIdentifiers;

/// When `true`, statement blocks (like if-blocks or loops) inside functions
/// are also checked for duplication.
final bool checkBlocks;

/// A list of methods/functions that should be excluded from the lint.
final ExcludedIdentifiersListParameter exclude;

static const _defaultMinTokens = 30;

static final _defaultExclude = ExcludedIdentifiersListParameter(
exclude: const [],
);

/// Constructor for [AvoidDuplicateCodeParameters] model.
const AvoidDuplicateCodeParameters({
required this.minTokens,
required this.ignoreLiterals,
required this.ignoreIdentifiers,
required this.checkBlocks,
required this.exclude,
});

/// Empty [AvoidDuplicateCodeParameters] model with default values.
factory AvoidDuplicateCodeParameters.empty() => AvoidDuplicateCodeParameters(
minTokens: _defaultMinTokens,
ignoreLiterals: false,
ignoreIdentifiers: true,
checkBlocks: true,
exclude: _defaultExclude,
);

/// Creates parameters from JSON configuration.
factory AvoidDuplicateCodeParameters.fromJson(Map<String, Object?> json) {
return AvoidDuplicateCodeParameters(
minTokens: json['min_tokens'] as int? ?? _defaultMinTokens,
ignoreLiterals: json['ignore_literals'] as bool? ?? false,
ignoreIdentifiers: json['ignore_identifiers'] as bool? ?? true,
checkBlocks: json['check_blocks'] as bool? ?? true,
exclude: ExcludedIdentifiersListParameter.defaultFromJson(json),
);
}

/// Converts the parameters to a JSON-compatible Map.
Map<String, Object?> toJson() => {
'min_tokens': minTokens,
'ignore_literals': ignoreLiterals,
'ignore_identifiers': ignoreIdentifiers,
'check_blocks': checkBlocks,
'exclude': exclude.exclude.map((e) => e.toJson()).toList(),
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AvoidDuplicateCodeParameters &&
other.minTokens == minTokens &&
other.ignoreLiterals == ignoreLiterals &&
other.ignoreIdentifiers == ignoreIdentifiers &&
other.checkBlocks == checkBlocks &&
other.exclude == exclude;

@override
int get hashCode => Object.hash(
minTokens,
ignoreLiterals,
ignoreIdentifiers,
checkBlocks,
exclude,
);
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
7 changes: 7 additions & 0 deletions lib/src/lints/avoid_duplicate_code/models/body_candidate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:analyzer/dart/ast/ast.dart';

/// A record representing a block or expression candidate for clone detection.
typedef BodyCandidate = ({
AstNode node,
Declaration? enclosingDeclaration,
});
18 changes: 18 additions & 0 deletions lib/src/lints/avoid_duplicate_code/models/cross_file_match.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:solid_lints/src/lints/avoid_duplicate_code/models/duplicate_location.dart';
import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart';

/// Represents a structural duplicate found in other files.
class CrossFileMatch {
/// The hash entry in the current file.
final HashEntry currentEntry;

/// The list of locations in other files where a duplicate of this entry
/// exists.
final List<DuplicateLocation> duplicates;

/// Creates a new [CrossFileMatch].
const CrossFileMatch({
required this.currentEntry,
required this.duplicates,
});
}
Loading