Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
104 changes: 92 additions & 12 deletions lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,96 @@ import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicat
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.
/// A lint rule that detects duplicated code blocks (clones) across 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.
/// 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.
///
/// 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.
/// ### Clone Detection Algorithm
///
/// The rule is built upon the fundamental code clone classification by
/// **Roy & Cordy (2007)** (*"A Survey on Software Clone Detection Research"*):
///
/// :::note 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.
/// :::
/// :::info 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`).
/// :::
///
/// :::info Structural Hashing
/// Builds an AST subtree fingerprint using **Bob Jenkins' One-at-a-time**
/// **hash** algorithm (structural hashing).
/// :::
///
/// :::info 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][aha] and [tight coupling][coupling].
///
/// :::
///
/// [aha]: https://en.wikipedia.org/wiki/Don't_repeat_yourself#AHA
/// [coupling]: https://en.wikipedia.org/wiki/Coupling_(computer_programming)
///
/// 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:
/// ```dart
/// // ignore: avoid_duplicate_code
/// void myBoilerplateMethod() { /* ... */ }
/// ```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// 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:
///
Expand All @@ -24,12 +103,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// solid_lints:
/// diagnostics:
/// avoid_duplicate_code:
/// min_tokens: 50
/// min_tokens: 30
/// ignore_literals: false
/// ignore_identifiers: true
/// check_blocks: false
/// check_blocks: true
/// exclude:
/// - method_name: build
/// - method_name: initState
/// - method_name: dispose
/// ```
class AvoidDuplicateCodeRule
extends SolidLintRule<AvoidDuplicateCodeParameters> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,128 @@ import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_para

/// 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.
/// Minimum number of tokens in a function body or block required for it to
/// be included in clone detection. Shorter bodies/blocks are ignored.
///
/// :::note 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:
/// ```dart
/// 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:
/// ```dart
/// 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
/// ```
final int minTokens;

/// When `true`, literal values (strings, numbers, booleans) are excluded
/// from the structural hash, making the check ignore literal differences.
/// from the structural hash, ignoring literal differences during duplicate
/// search.
///
/// ##### Example:
/// ```dart
/// // 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.
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.
/// 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:
/// ```dart
/// // 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.
final bool ignoreIdentifiers;

/// When `true`, statement blocks (like if-blocks or loops) inside functions
/// are also checked for duplication.
/// When `true`, statement blocks (such as `if` blocks or loops) inside
/// functions are also checked for duplication.
///
/// ##### Example:
/// ```dart
/// // 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.
final bool checkBlocks;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

static const _defaultMinTokens = 30;
Expand Down