Skip to main content

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"):

Type 2 Clones

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.

Sequential Variable Indexing

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

Structural Hashing

Builds an AST subtree fingerprint using Bob Jenkins' One-at-a-time hash algorithm (structural hashing).

Nested Clone Suppression

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

warning

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:

  1. Ignore specific occurrences in code: Use inline analysis ignore comments above the affected method:
    // ignore: avoid_duplicate_code
    void myBoilerplateMethod() { /* ... */ }
  2. Exclude methods globally: Add the method name to the exclude list in your analysis_options.yaml config (e.g., excluding initState or dispose).
  3. Increase min_tokens threshold: Raise the min_tokens parameter (e.g., to 40 or 50) 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.

What is a Token?

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 numbers 0.20 and 0.15 differ.
  • 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 inner if block.
  • 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.