From e1c61ae5a514a070487dab7ea59fc903f584f891 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 28 Jul 2026 16:24:12 +0300 Subject: [PATCH 1/4] docs: improve avoid_duplicate_code rule and parameter documentation with detailed explanations and examples --- .../avoid_duplicate_code_rule.dart | 91 ++++++++++-- .../avoid_duplicate_code_parameters.dart | 130 ++++++++++++++++-- 2 files changed, 200 insertions(+), 21 deletions(-) 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..2989ac01 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,85 @@ 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 +/// 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 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. 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), which is also utilized in +/// classic static analysis tools like **PMD CPD (Copy-Paste Detector)** and +/// **SonarQube**. +/// ::: +/// +/// :::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:** The AST block structures and their calculated +/// hashes 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 +/// Due to the nature of AST structural analysis, false positives may +/// occasionally occur on repetitive boilerplate code sharing identical +/// structural flow (such as form initializers, DTO mappers, UI builder methods, +/// or lifecycle hooks like `initState` and `dispose`). +/// ::: +/// +/// If you encounter false positive warnings in your project, 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 +92,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..99f60d32 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,135 @@ 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. + /// + /// :::info Comparison of Duplicate Thresholds Across Tools + /// + /// | Tool / Analyzer | Language | Default Threshold | Typical Scope | + /// | :--- | :--- | :--- | :--- | + /// | **solid_lints** | Dart 3+ | **30 tokens** | ~4-6 lines | + /// | **DCM** (Dart Metrics) | Dart | **50 tokens** / 10 lines | ~10 lines | + /// | **PMD CPD** | Java, C# | 100 tokens | ~10-15 lines | + /// | **SonarQube** | Java, JS, C# | 100 tokens / 10 lines | ~10 lines | + /// ::: + /// + /// ##### 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 processUser(Object user) { // 1 token + /// if (user case User(:final name, :final age) // 14 tokens + /// when age >= 18) { // 6 tokens + /// final status = 'Adult'; // 5 tokens + /// return '$status: $name ($age)'; // 3 tokens + /// } // 1 token + /// return 'Guest'; // 3 tokens + /// } // 1 token + /// ``` 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'); + /// repository.save(user); + /// } + /// } + /// + /// // Function B (different function, same inner if block) + /// void processAdmin(User user) { + /// validateAdmin(user); + /// if (user.isActive) { + /// logger.log('Processing user'); + /// repository.save(user); + /// } + /// } + /// ``` + /// * **When `check_blocks: true` (default):** **Reported as duplicate** + /// for the inner `if` block. + /// * **When `check_blocks: false`:** **NOT reported as duplicate** + /// because only top-level function bodies are checked. 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; From ad3024e85e75d99a5bfd4801cfebcc63a3c79b17 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 28 Jul 2026 17:38:17 +0300 Subject: [PATCH 2/4] docs: update avoid_duplicate_code rule documentation with improved usage context and clarified examples --- .../avoid_duplicate_code_rule.dart | 39 ++++++++++++------- .../avoid_duplicate_code_parameters.dart | 18 ++++----- 2 files changed, 31 insertions(+), 26 deletions(-) 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 2989ac01..58527433 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 @@ -9,8 +9,8 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// 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 copies and provides context messages -/// linking to the other occurrences. +/// subtrees, the rule reports all currently known copies and provides context +/// messages linking to the other occurrences. /// /// ### Clone Detection Algorithm /// @@ -20,7 +20,8 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// :::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. While plain text diff tools only catch exact +/// 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. /// ::: @@ -34,9 +35,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// :::info Structural Hashing /// Builds an AST subtree fingerprint using **Bob Jenkins' One-at-a-time** -/// **hash** algorithm (structural hashing), which is also utilized in -/// classic static analysis tools like **PMD CPD (Copy-Paste Detector)** and -/// **SonarQube**. +/// **hash** algorithm (structural hashing). /// ::: /// /// :::info Nested Clone Suppression @@ -54,8 +53,8 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// 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:** The AST block structures and their calculated -/// hashes are cached on disk at +/// * **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. @@ -63,20 +62,30 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Handling False Positives /// /// :::warning -/// Due to the nature of AST structural analysis, false positives may -/// occasionally occur on repetitive boilerplate code sharing identical -/// structural flow (such as form initializers, DTO mappers, UI builder methods, -/// or lifecycle hooks like `initState` and `dispose`). +/// **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 +/// [premature abstraction](https://medium.com/@ricrivero3/premature-abstraction-is-the-root-of-all-evil-7309762c0635) +/// and [tight coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)). +/// /// ::: /// -/// If you encounter false positive warnings in your project, consider the -/// following solutions: +/// 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() { ... } +/// 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 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 99f60d32..e5d72d30 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 @@ -17,16 +17,6 @@ class AvoidDuplicateCodeParameters { /// code). This automatically filters out trivial single-line expressions and /// focuses exclusively on substantial logic blocks. /// - /// :::info Comparison of Duplicate Thresholds Across Tools - /// - /// | Tool / Analyzer | Language | Default Threshold | Typical Scope | - /// | :--- | :--- | :--- | :--- | - /// | **solid_lints** | Dart 3+ | **30 tokens** | ~4-6 lines | - /// | **DCM** (Dart Metrics) | Dart | **50 tokens** / 10 lines | ~10 lines | - /// | **PMD CPD** | Java, C# | 100 tokens | ~10-15 lines | - /// | **SonarQube** | Java, JS, C# | 100 tokens / 10 lines | ~10 lines | - /// ::: - /// /// ##### Example 1: Less than 30 tokens (Ignored): 26 tokens /// A concise switch expression in Dart 3 syntax contains **26 tokens** and /// is ignored: @@ -111,7 +101,10 @@ class AvoidDuplicateCodeParameters { /// 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); /// } /// } /// @@ -120,14 +113,17 @@ class AvoidDuplicateCodeParameters { /// 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 only top-level function bodies are checked. + /// because nested `{ ... }` block nodes are skipped. final bool checkBlocks; /// A list of methods/functions that should be excluded from clone detection. From 901fb40e9c9e3104526434e32ef16e65d0f6aea1 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 28 Jul 2026 19:07:01 +0300 Subject: [PATCH 3/4] docs: update documentation links and definitions for avoid_duplicate_code rule --- .../avoid_duplicate_code/avoid_duplicate_code_rule.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 58527433..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 @@ -73,11 +73,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// * **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 -/// [premature abstraction](https://medium.com/@ricrivero3/premature-abstraction-is-the-root-of-all-evil-7309762c0635) -/// and [tight coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)). +/// [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: /// From c4e44097e01e2865a18b53d1cb4eb50efed458ba Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 28 Jul 2026 19:58:39 +0300 Subject: [PATCH 4/4] docs: update avoid_duplicate_code_parameters example to use pattern matching switch syntax --- .../models/avoid_duplicate_code_parameters.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 e5d72d30..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 @@ -31,14 +31,11 @@ class AvoidDuplicateCodeParameters { /// A function with record destructuring and pattern matching in Dart 3 syntax /// contains **34 tokens** and is checked for duplicates: /// ```dart - /// String processUser(Object user) { // 1 token - /// if (user case User(:final name, :final age) // 14 tokens - /// when age >= 18) { // 6 tokens - /// final status = 'Adult'; // 5 tokens - /// return '$status: $name ($age)'; // 3 tokens - /// } // 1 token - /// return 'Guest'; // 3 tokens - /// } // 1 token + /// 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;