avoid_duplicate_code
A lint rule that detects duplicated code blocks (clones) across the project.
When two or more function, method, or constructor bodies or nested code
blocks (such as if blocks or loops) have structurally identical AST
subtrees, the rule reports all currently known copies and provides context
messages linking to the other occurrences.
Clone Detection Algorithm
The rule is built upon the fundamental code clone classification by Roy & Cordy (2007) ("A Survey on Software Clone Detection Research"):
The rule focuses on Type 2 clones (syntactic clones): structurally
identical AST subtrees where names of local variables, formal parameters,
or literal values may differ (if configured via ignore_identifiers or
ignore_literals). While plain text diff tools only catch exact
copies (Type 1), this rule operates on the AST level to detect copy-pasted
logic even after variable renaming or code formatting changes.
Local variable and parameter names in the AST subtree are replaced with
sequential positional IDs (0, 1, 2, ...) based on their first appearance
in the code block. This enables the
algorithm to recognize identical underlying logic even if variables were
renamed (e.g., x to item).
Builds an AST subtree fingerprint using Bob Jenkins' One-at-a-time hash algorithm (structural hashing).
The algorithm intelligently suppresses overlapping or nested warnings. If a
large code block (e.g., an entire function) is identified as a clone, its
inner blocks (such as if statements or loops) will not be reported
separately, preventing warning spam.
Best-Effort Sequential Analysis & Caching
- Cross-File Analysis (Best-Effort): Because the Dart Analyzer processes project files sequentially, during the initial analysis pass, only the second (and subsequent) clone is highlighted immediately. This happens because the first file was analyzed before information about its copy entered the global registry. The first file will be highlighted upon its next edit, save, or re-analysis.
- Persistent Disk Cache: Candidate hash entries and their metadata are
cached on disk at
.dart_tool/solid_lints/duplicate_index.json. Consequently, subsequent IDE sessions or re-analysis passes skip unchanged files, making duplicate code detection significantly faster.
Handling False Positives
Important Context on Code Duplication
Because this rule analyzes the AST structure, you may encounter warnings in scenarios where extracting the code is actually undesirable:
- Boilerplate Code: Repetitive structures like form initializers, DTO
mappers, or standard lifecycle hooks (e.g.,
initState,dispose) often share identical AST flows by necessity. - Coincidental Duplication: Not all duplication is harmful. If two identical code blocks serve completely distinct business purposes, merging them creates a forced dependency. This can lead to hasty abstractions and tight coupling.
In such justified cases, rather than forcing an unnatural abstraction, we encourage you to consider the following solutions:
- Ignore specific occurrences in code: Use inline analysis ignore
comments above the affected method:
// ignore: avoid_duplicate_codevoid myBoilerplateMethod() { /* ... */ }
- Exclude methods globally: Add the method name to the
excludelist in youranalysis_options.yamlconfig (e.g., excludinginitStateordispose). - Increase
min_tokensthreshold: Raise themin_tokensparameter (e.g., to40or50) to ignore shorter structural clones across the project.
Example config:
plugins:
solid_lints:
diagnostics:
avoid_duplicate_code:
min_tokens: 30
ignore_literals: false
ignore_identifiers: true
check_blocks: true
exclude:
- method_name: initState
- method_name: dispose
Parameters
min_tokens (int)
Minimum number of tokens in a function body or block required for it to be included in clone detection. Shorter bodies/blocks are ignored.
The smallest indivisible syntactic unit of code emitted by the compiler's
lexer (keywords final, if, switch; identifiers; operators =, =>;
punctuation {, }, ; and literals).
Considering the modern and concise syntax of Dart 3+ (switch expressions, pattern matching, record destructuring), the optimal default threshold was determined to be 30 tokens (approximately 4-6 lines of meaningful code). This automatically filters out trivial single-line expressions and focuses exclusively on substantial logic blocks.
Example 1: Less than 30 tokens (Ignored): 26 tokens
A concise switch expression in Dart 3 syntax contains 26 tokens and is ignored:
Color getShapeColor(Shape shape) => switch (shape) { // 6 tokens
Circle(:final color) => color, // 9 tokens
Square(:final color) => color, // 9 tokens
}; // 2 tokens
Example 2: 30+ tokens (Checked for duplicates): 34 tokens
A function with record destructuring and pattern matching in Dart 3 syntax contains 34 tokens and is checked for duplicates:
String formatUserRole(Object user) => switch (user) { // 6 tokens
User(isAdmin: true, isVerified: true) => 'Admin', // 13 tokens
User(isVerified: true) => 'User', // 9 tokens
_ => 'Guest User', // 4 tokens
}; // 2 tokens
ignore_literals (bool)
When true, literal values (strings, numbers, booleans) are excluded
from the structural hash, ignoring literal differences during duplicate
search.
Example:
// Function A
double calculateTax(double amount) {
final tax = amount * 0.20;
return amount + tax;
}
// Function B (differs only by literal 0.15 vs 0.20)
double calculateDiscount(double amount) {
final tax = amount * 0.15;
return amount + tax;
}
- When
ignore_literals: false(default): NOT reported because numbers0.20and0.15differ. - When
ignore_literals: true: Reported as duplicate because literal values are ignored.
ignore_identifiers (bool)
When true, local variable and parameter names are excluded from the
structural hash (using Sequential Variable Indexing). This enables
detection of renamed variable clones (Type 2). Note that method, class,
and field names are NOT ignored to prevent excessive false positives.
Example:
// Function A
double calcTotal(double price, int count) {
final subtotal = price * count;
return subtotal > 100 ? subtotal * 0.9 : subtotal;
}
// Function B (renamed: price->amount, count->qty, subtotal->total)
double calcTotal(double amount, int qty) {
final total = amount * qty;
return total > 100 ? total * 0.9 : total;
}
- When
ignore_identifiers: true(default): Reported as duplicate (Type 2 Clone). - When
ignore_identifiers: false: NOT reported as duplicate because local names differ.
check_blocks (bool)
When true, statement blocks (such as if blocks or loops) inside
functions are also checked for duplication.
Example:
// Function A
void processUser(User user) {
print('Starting user process...');
if (user.isActive) {
logger.log('Processing user');
user.lastActive = DateTime.now();
user.status = UserStatus.active;
repository.save(user);
analytics.track('user_processed', user.id);
}
}
// Function B (different function, same inner if block)
void processAdmin(User user) {
validateAdmin(user);
if (user.isActive) {
logger.log('Processing user');
user.lastActive = DateTime.now();
user.status = UserStatus.active;
repository.save(user);
analytics.track('user_processed', user.id);
}
}
- When
check_blocks: true(default): Reported as duplicate for the innerifblock. - When
check_blocks: false: NOT reported as duplicate because nested{ ... }block nodes are skipped.
exclude (String | Map | List<String | Map>)
A list of methods/functions that should be excluded from clone detection.