diff --git a/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart b/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart index 688a31ad..f96785d5 100644 --- a/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart +++ b/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart @@ -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() { /* ... */ } +/// ``` +/// 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: /// @@ -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 { diff --git a/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart b/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart index e8b3676c..963dccc0 100644 --- a/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart +++ b/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart @@ -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; - /// 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;