redup 0.3.22__tar.gz → 0.3.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {redup-0.3.22 → redup-0.3.24}/PKG-INFO +3 -2
  2. {redup-0.3.22 → redup-0.3.24}/README.md +1 -1
  3. {redup-0.3.22 → redup-0.3.24}/pyproject.toml +2 -1
  4. {redup-0.3.22 → redup-0.3.24}/src/redup/__init__.py +1 -1
  5. {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/config_builder.py +9 -3
  6. redup-0.3.24/src/redup/cli_app/fuzzy_similarity.py +160 -0
  7. redup-0.3.24/src/redup/cli_app/main.py +219 -0
  8. {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/scan_commands.py +4 -4
  9. {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/scan_helpers.py +2 -2
  10. {redup-0.3.22 → redup-0.3.24}/src/redup/core/pipeline.py +28 -32
  11. redup-0.3.24/src/redup/core/scanner.py +647 -0
  12. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/PKG-INFO +3 -2
  13. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/SOURCES.txt +1 -3
  14. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/requires.txt +3 -0
  15. {redup-0.3.22 → redup-0.3.24}/tests/test_e2e.py +6 -4
  16. redup-0.3.22/src/redup/cli_app/main.py +0 -708
  17. redup-0.3.22/src/redup/core/memory_scanner.py +0 -285
  18. redup-0.3.22/src/redup/core/parallel_scanner.py +0 -234
  19. redup-0.3.22/src/redup/core/scanner.py +0 -373
  20. redup-0.3.22/src/redup/core/ultra_fast_scanner.py +0 -358
  21. {redup-0.3.22 → redup-0.3.24}/LICENSE +0 -0
  22. {redup-0.3.22 → redup-0.3.24}/setup.cfg +0 -0
  23. {redup-0.3.22 → redup-0.3.24}/src/redup/__main__.py +0 -0
  24. {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/__init__.py +0 -0
  25. {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/output_writer.py +0 -0
  26. {redup-0.3.22 → redup-0.3.24}/src/redup/core/__init__.py +0 -0
  27. {redup-0.3.22 → redup-0.3.24}/src/redup/core/cache.py +0 -0
  28. {redup-0.3.22 → redup-0.3.24}/src/redup/core/config.py +0 -0
  29. {redup-0.3.22 → redup-0.3.24}/src/redup/core/differ.py +0 -0
  30. {redup-0.3.22 → redup-0.3.24}/src/redup/core/fuzzy_similarity.py +0 -0
  31. {redup-0.3.22 → redup-0.3.24}/src/redup/core/hash_cache.py +0 -0
  32. {redup-0.3.22 → redup-0.3.24}/src/redup/core/hasher.py +0 -0
  33. {redup-0.3.22 → redup-0.3.24}/src/redup/core/lazy_grouper.py +0 -0
  34. {redup-0.3.22 → redup-0.3.24}/src/redup/core/lsh_matcher.py +0 -0
  35. {redup-0.3.22 → redup-0.3.24}/src/redup/core/matcher.py +0 -0
  36. {redup-0.3.22 → redup-0.3.24}/src/redup/core/models.py +0 -0
  37. {redup-0.3.22 → redup-0.3.24}/src/redup/core/planner.py +0 -0
  38. {redup-0.3.22 → redup-0.3.24}/src/redup/core/ts_extractor.py +0 -0
  39. {redup-0.3.22 → redup-0.3.24}/src/redup/core/universal_fuzzy.py +0 -0
  40. {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/__init__.py +0 -0
  41. {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/duplicate_finders.py +0 -0
  42. {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/function_extractor.py +0 -0
  43. {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/hash_utils.py +0 -0
  44. {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/language_dispatcher.py +0 -0
  45. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/__init__.py +0 -0
  46. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/code2llm_reporter.py +0 -0
  47. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/enhanced_reporter.py +0 -0
  48. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/json_reporter.py +0 -0
  49. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/markdown_reporter.py +0 -0
  50. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/toon_reporter.py +0 -0
  51. {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/yaml_reporter.py +0 -0
  52. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/dependency_links.txt +0 -0
  53. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/entry_points.txt +0 -0
  54. {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/top_level.txt +0 -0
  55. {redup-0.3.22 → redup-0.3.24}/tests/test_hasher.py +0 -0
  56. {redup-0.3.22 → redup-0.3.24}/tests/test_matcher.py +0 -0
  57. {redup-0.3.22 → redup-0.3.24}/tests/test_models.py +0 -0
  58. {redup-0.3.22 → redup-0.3.24}/tests/test_pipeline.py +0 -0
  59. {redup-0.3.22 → redup-0.3.24}/tests/test_planner.py +0 -0
  60. {redup-0.3.22 → redup-0.3.24}/tests/test_reporters.py +0 -0
  61. {redup-0.3.22 → redup-0.3.24}/tests/test_scanner.py +0 -0
  62. {redup-0.3.22 → redup-0.3.24}/tests/test_ts_extractor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.3.22
3
+ Version: 0.3.24
4
4
  Summary: Code duplication analyzer and refactoring planner for LLMs
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -26,6 +26,7 @@ Requires-Dist: pyyaml>=6.0
26
26
  Requires-Dist: typer>=0.12.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
29
30
  Provides-Extra: all
30
31
  Requires-Dist: redup[fuzzy]; extra == "all"
31
32
  Requires-Dist: redup[ast]; extra == "all"
@@ -83,7 +84,7 @@ Dynamic: license-file
83
84
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
84
85
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
85
86
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
86
- [![Version](https://img.shields.io/badge/version-0.3.22-green.svg)](https://pypi.org/project/redup/)
87
+ [![Version](https://img.shields.io/badge/version-0.3.24-green.svg)](https://pypi.org/project/redup/)
87
88
 
88
89
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
89
90
 
@@ -5,7 +5,7 @@
5
5
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
8
- [![Version](https://img.shields.io/badge/version-0.3.22-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.24-green.svg)](https://pypi.org/project/redup/)
9
9
 
10
10
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
11
11
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.3.22"
7
+ version = "0.3.24"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "typer>=0.12.0",
31
31
  "rich>=13.0",
32
32
  "pydantic>=2.0",
33
+ "tomli>=2.0; python_version<'3.11'",
33
34
  ]
34
35
 
35
36
  [project.optional-dependencies]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.22"
5
+ __version__ = "0.3.24"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -30,7 +30,9 @@ def build_config_with_file_support(
30
30
  incremental: bool = False,
31
31
  memory_cache: bool = False,
32
32
  max_cache_mb: int = 512,
33
- functions_only: bool = False
33
+ functions_only: bool = False,
34
+ fuzzy: bool = False,
35
+ fuzzy_threshold: float = 0.8
34
36
  ) -> ScanConfig:
35
37
  """Build scan configuration with advanced options."""
36
38
  config = load_config()
@@ -40,12 +42,16 @@ def build_config_with_file_support(
40
42
  if extensions is not None:
41
43
  scan_config.extensions = [e.strip() for e in extensions.split(',')]
42
44
  if min_lines is not None:
43
- scan_config.min_lines = min_lines
45
+ scan_config.min_block_lines = min_lines
44
46
  if min_similarity is not None:
45
47
  scan_config.min_similarity = min_similarity
46
48
  if include_tests is not None:
47
49
  scan_config.include_tests = include_tests
48
50
  if functions_only:
49
- scan_config.function_level_only = functions_only
51
+ scan_config.functions_only = functions_only
52
+
53
+ # Add fuzzy support
54
+ scan_config.fuzzy_enabled = fuzzy
55
+ scan_config.fuzzy_threshold = fuzzy_threshold
50
56
 
51
57
  return scan_config
@@ -0,0 +1,160 @@
1
+ """Fuzzy similarity detection functions."""
2
+
3
+ from pathlib import Path
4
+ import typer
5
+
6
+ from redup.core.models import DuplicationMap
7
+
8
+
9
+ def _apply_fuzzy_similarity(dup_map: DuplicationMap, threshold: float) -> DuplicationMap:
10
+ """Apply fuzzy similarity detection to find similar components across all languages."""
11
+ validation = _validate_fuzzy_input(dup_map, threshold)
12
+ if not validation.is_valid:
13
+ typer.echo(validation.message)
14
+ return dup_map
15
+
16
+ all_blocks = _extract_all_blocks(dup_map)
17
+ if not all_blocks:
18
+ typer.echo("🔍 No code blocks found for fuzzy analysis")
19
+ return dup_map
20
+
21
+ typer.echo(f"🔍 Analyzing {len(all_blocks)} code blocks for universal fuzzy similarity...")
22
+
23
+ html_css_blocks, other_blocks = _separate_blocks_by_type(all_blocks)
24
+ similar_pairs = _analyze_blocks_with_detectors(html_css_blocks, other_blocks, threshold)
25
+
26
+ _report_fuzzy_results(similar_pairs)
27
+ return dup_map
28
+
29
+
30
+ def _validate_fuzzy_input(dup_map: DuplicationMap, threshold: float) -> 'FuzzyValidationResult':
31
+ """Validate input parameters for fuzzy similarity analysis."""
32
+ from dataclasses import dataclass
33
+
34
+ @dataclass
35
+ class FuzzyValidationResult:
36
+ is_valid: bool
37
+ message: str = ""
38
+
39
+ if threshold <= 0 or threshold > 1:
40
+ return FuzzyValidationResult(False, "❌ Invalid threshold. Must be between 0 and 1.")
41
+
42
+ if not dup_map.groups:
43
+ return FuzzyValidationResult(False, "❌ No duplicate groups found.")
44
+
45
+ return FuzzyValidationResult(True)
46
+
47
+
48
+ def _extract_all_blocks(dup_map: DuplicationMap) -> list:
49
+ """Extract all code blocks from duplicate fragments."""
50
+ from redup.core.scanner import CodeBlock
51
+
52
+ all_blocks = []
53
+ for group in dup_map.groups:
54
+ for fragment in group.fragments:
55
+ # Create a CodeBlock from fragment data
56
+ block = CodeBlock(
57
+ file=fragment.file,
58
+ line_start=fragment.line_start,
59
+ line_end=fragment.line_end,
60
+ text=fragment.text,
61
+ function_name=fragment.function_name,
62
+ class_name=fragment.class_name
63
+ )
64
+ all_blocks.append(block)
65
+ return all_blocks
66
+
67
+
68
+ def _separate_blocks_by_type(all_blocks: list) -> tuple[list, list]:
69
+ """Separate blocks into HTML/CSS and other language categories."""
70
+ html_css_blocks = [block for block in all_blocks
71
+ if Path(block.file).suffix.lower() in ['.html', '.htm', '.css', '.scss', '.sass', '.less']]
72
+
73
+ other_blocks = [block for block in all_blocks if block not in html_css_blocks]
74
+
75
+ return html_css_blocks, other_blocks
76
+
77
+
78
+ def _analyze_blocks_with_detectors(html_css_blocks: list, other_blocks: list, threshold: float) -> list:
79
+ """Analyze blocks using appropriate fuzzy similarity detectors."""
80
+ similar_pairs = []
81
+
82
+ # Analyze HTML/CSS with specialized detector
83
+ if html_css_blocks:
84
+ typer.echo(f" 📝 Analyzing {len(html_css_blocks)} HTML/CSS blocks...")
85
+ html_css_similar = _analyze_html_css_blocks(html_css_blocks, threshold)
86
+ similar_pairs.extend(html_css_similar)
87
+ typer.echo(f" ✨ Found {len(html_css_similar)} similar HTML/CSS component pairs")
88
+
89
+ # Analyze other languages with universal detector
90
+ if other_blocks:
91
+ typer.echo(f" 🔧 Analyzing {len(other_blocks)} blocks from other languages...")
92
+ universal_similar = _analyze_other_language_blocks(other_blocks, threshold)
93
+ similar_pairs.extend(universal_similar)
94
+ typer.echo(f" ✨ Found {len(universal_similar)} similar component pairs from other languages")
95
+
96
+ return similar_pairs
97
+
98
+
99
+ def _analyze_html_css_blocks(html_css_blocks: list, threshold: float) -> list:
100
+ """Analyze HTML/CSS blocks using specialized fuzzy similarity detector."""
101
+ from redup.core.fuzzy_similarity import FuzzySimilarityDetector
102
+
103
+ html_css_detector = FuzzySimilarityDetector(similarity_threshold=threshold)
104
+ return html_css_detector.find_similar_components(html_css_blocks)
105
+
106
+
107
+ def _analyze_other_language_blocks(other_blocks: list, threshold: float) -> list:
108
+ """Analyze non-HTML/CSS blocks using universal fuzzy similarity detector."""
109
+ from redup.core.universal_fuzzy import UniversalFuzzyDetector
110
+
111
+ universal_detector = UniversalFuzzyDetector(similarity_threshold=threshold)
112
+ return universal_detector.find_similar_components(other_blocks)
113
+
114
+
115
+ def _report_fuzzy_results(similar_pairs: list) -> None:
116
+ """Report fuzzy similarity analysis results."""
117
+ if similar_pairs:
118
+ typer.echo(f"🎯 Total fuzzy similar components found: {len(similar_pairs)}")
119
+
120
+ # Simplified reporting to reduce complexity
121
+ typer.echo(f" 📊 Language groups: {len(set(Path(b1.file).suffix + '+' + Path(b2.file).suffix for b1, b2, _ in similar_pairs))}")
122
+
123
+ # Show top examples only
124
+ for block1, block2, similarity in similar_pairs[:5]:
125
+ typer.echo(f" {block1.file} ↔ {block2.file}: {similarity:.2f}")
126
+
127
+ if len(similar_pairs) > 5:
128
+ typer.echo(f" ... and {len(similar_pairs) - 5} more")
129
+
130
+ # Note: In production, you'd integrate these into DuplicationMap
131
+ # For now, we just report the findings
132
+
133
+ else:
134
+ typer.echo("🔍 No fuzzy similar components found")
135
+
136
+
137
+ def _report_similarity_by_groups(similar_pairs):
138
+ """Extracted function to reduce complexity."""
139
+ lang_groups = {}
140
+ for block1, block2, similarity in similar_pairs:
141
+ lang1 = Path(block1.file).suffix
142
+ lang2 = Path(block2.file).suffix
143
+ comp_type = "unknown"
144
+
145
+ # Try to get component type
146
+ try:
147
+ from redup.core.universal_fuzzy import UniversalFuzzyExtractor
148
+ extractor = UniversalFuzzyExtractor()
149
+ sig1 = extractor.extract_universal_signature(block1)
150
+ if sig1:
151
+ comp_type = sig1.component_type
152
+ except:
153
+ pass
154
+
155
+ group_key = f"{lang1}+{lang2}:{comp_type}"
156
+ if group_key not in lang_groups:
157
+ lang_groups[group_key] = []
158
+ lang_groups[group_key].append((block1, block2, similarity))
159
+
160
+ return lang_groups
@@ -0,0 +1,219 @@
1
+ """CLI entry point for reDUP — code duplication analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable
7
+ from typing import Literal
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ # Suppress SyntaxWarning from external libraries
13
+ warnings.filterwarnings("ignore", category=SyntaxWarning)
14
+
15
+ import redup # noqa: E402
16
+ from redup.core.config import create_sample_redup_toml, load_config # noqa: E402
17
+ from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
18
+
19
+ # Import refactored modules
20
+ from redup.cli_app.config_builder import build_config, build_config_with_file_support # noqa: E402
21
+ from redup.cli_app.output_writer import write_output, write_results # noqa: E402
22
+ from redup.cli_app.scan_commands import ( # noqa: E402
23
+ scan_command, diff_command, check_command, config_command, info_command
24
+ )
25
+ from redup.cli_app.scan_helpers import print_scan_header, print_scan_summary # noqa: E402
26
+
27
+ app = typer.Typer(
28
+ name="redup",
29
+ help="reDUP — Code duplication analyzer and refactoring planner for LLMs.",
30
+ add_completion=False,
31
+ )
32
+
33
+
34
+ OutputFormat = Literal["json", "yaml", "toon", "markdown", "all", "enhanced", "code2llm"]
35
+
36
+
37
+ DEFAULT_PATH = Path(".")
38
+ DEFAULT_FORMAT: OutputFormat = "toon"
39
+ DEFAULT_OUTPUT: Path | None = None
40
+
41
+
42
+ @app.command()
43
+ def scan(
44
+ path: Path = typer.Argument(
45
+ DEFAULT_PATH,
46
+ help="Project root directory to scan.",
47
+ exists=True,
48
+ dir_okay=True,
49
+ file_okay=False,
50
+ ),
51
+ format: str = typer.Option(
52
+ "toon",
53
+ "--format", "-f",
54
+ help="Output format (json, yaml, toon, markdown, code2llm, all, enhanced).",
55
+ ),
56
+ output: Path | None = typer.Option(
57
+ DEFAULT_OUTPUT,
58
+ "--output", "-o",
59
+ help="Output directory or file path. Defaults to stdout.",
60
+ ),
61
+ extensions: str | None = typer.Option(
62
+ None,
63
+ "--ext", "-e",
64
+ help="Comma-separated file extensions to scan. Overrides config.",
65
+ ),
66
+ min_lines: int | None = typer.Option(
67
+ None,
68
+ "--min-lines",
69
+ help="Minimum block size (lines). Overrides config.",
70
+ ),
71
+ min_similarity: float | None = typer.Option(
72
+ None,
73
+ "--min-sim",
74
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
75
+ ),
76
+ include_tests: bool | None = typer.Option(
77
+ None,
78
+ "--include-tests",
79
+ help="Include test files. Overrides config.",
80
+ ),
81
+ functions_only: bool = typer.Option(
82
+ True,
83
+ "--functions-only/--no-functions-only",
84
+ help="Only analyze function-level duplicates (default: enabled for speed).",
85
+ ),
86
+ parallel: bool = typer.Option(
87
+ False,
88
+ "--parallel/--no-parallel",
89
+ help="Use parallel scanning for large projects (default: disabled due to issues).",
90
+ ),
91
+ max_workers: int | None = typer.Option(
92
+ None,
93
+ "--max-workers",
94
+ help="Maximum number of parallel workers.",
95
+ ),
96
+ incremental: bool = typer.Option(
97
+ False,
98
+ "--incremental",
99
+ help="Use incremental scanning with caching.",
100
+ ),
101
+ memory_cache: bool = typer.Option(
102
+ True,
103
+ "--memory-cache/--no-memory-cache",
104
+ help="Use memory cache for faster scanning.",
105
+ ),
106
+ max_cache_mb: int = typer.Option(
107
+ 512,
108
+ "--max-cache-mb",
109
+ help="Maximum memory cache size in MB.",
110
+ ),
111
+ fuzzy: bool = typer.Option(
112
+ False,
113
+ "--fuzzy",
114
+ help="Enable fuzzy similarity detection.",
115
+ ),
116
+ fuzzy_threshold: float = typer.Option(
117
+ 0.8,
118
+ "--fuzzy-threshold",
119
+ help="Fuzzy similarity threshold (0.0-1.0).",
120
+ ),
121
+ ) -> None:
122
+ """Scan a project for code duplicates."""
123
+ return scan_command(
124
+ path, format, output, extensions, min_lines, min_similarity,
125
+ include_tests, functions_only, parallel, max_workers,
126
+ incremental, memory_cache, max_cache_mb, fuzzy, fuzzy_threshold
127
+ )
128
+
129
+
130
+ @app.command()
131
+ def diff(
132
+ before: Path = typer.Argument(
133
+ ...,
134
+ help="Path to the earlier scan JSON file.",
135
+ exists=True,
136
+ dir_okay=False,
137
+ file_okay=True,
138
+ ),
139
+ after: Path = typer.Argument(
140
+ ...,
141
+ help="Path to the later scan JSON file.",
142
+ exists=True,
143
+ dir_okay=False,
144
+ file_okay=True,
145
+ ),
146
+ ) -> None:
147
+ """Compare two reDUP scans and show the differences."""
148
+ return diff_command(before, after)
149
+
150
+
151
+ @app.command()
152
+ def check(
153
+ path: Path = typer.Argument(
154
+ Path("."),
155
+ help="Project root directory to scan.",
156
+ exists=True,
157
+ dir_okay=True,
158
+ file_okay=False,
159
+ ),
160
+ max_groups: int | None = typer.Option(
161
+ None,
162
+ "--max-groups",
163
+ help="Maximum allowed duplicate groups. Overrides config.",
164
+ ),
165
+ max_saved_lines: int | None = typer.Option(
166
+ None,
167
+ "--max-lines",
168
+ help="Maximum allowed recoverable lines. Overrides config.",
169
+ ),
170
+ extensions: str | None = typer.Option(
171
+ None,
172
+ "--ext", "-e",
173
+ help="Comma-separated file extensions to scan. Overrides config.",
174
+ ),
175
+ min_lines: int | None = typer.Option(
176
+ None,
177
+ "--min-lines",
178
+ help="Minimum block size (lines). Overrides config.",
179
+ ),
180
+ min_similarity: float | None = typer.Option(
181
+ None,
182
+ "--min-sim",
183
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
184
+ ),
185
+ include_tests: bool | None = typer.Option(
186
+ None,
187
+ "--include-tests",
188
+ help="Include test files. Overrides config.",
189
+ ),
190
+ ) -> None:
191
+ """Check project for duplicates and exit with non-zero code if thresholds exceeded."""
192
+ return check_command(path, max_groups, max_saved_lines, extensions, min_lines, min_similarity, include_tests)
193
+
194
+
195
+ @app.command()
196
+ def config(
197
+ init: bool = typer.Option(
198
+ False,
199
+ "--init",
200
+ help="Create a sample redup.toml configuration file.",
201
+ ),
202
+ show: bool = typer.Option(
203
+ False,
204
+ "--show",
205
+ help="Show current configuration (files + env vars).",
206
+ ),
207
+ ) -> None:
208
+ """Manage reDUP configuration."""
209
+ return config_command(init, show)
210
+
211
+
212
+ @app.command()
213
+ def info() -> None:
214
+ """Show reDUP version and configuration info."""
215
+ return info_command()
216
+
217
+
218
+ if __name__ == "__main__":
219
+ app()
@@ -33,16 +33,16 @@ def scan_command(
33
33
  """Scan a project for code duplicates."""
34
34
 
35
35
  # Build configuration
36
- if any([extensions, min_lines, min_similarity, include_tests, functions_only, parallel, max_workers, incremental, memory_cache, max_cache_mb]):
36
+ if any([extensions, min_lines, min_similarity, include_tests, functions_only, parallel, max_workers, incremental, memory_cache, max_cache_mb, fuzzy]):
37
37
  config = build_config_with_file_support(
38
38
  path, extensions, min_lines, min_similarity, include_tests,
39
- parallel, max_workers, incremental, memory_cache, max_cache_mb, functions_only
39
+ parallel, max_workers, incremental, memory_cache, max_cache_mb, functions_only, fuzzy, fuzzy_threshold
40
40
  )
41
41
  else:
42
42
  config = build_config(path, extensions, min_lines, min_similarity, include_tests)
43
43
 
44
44
  # Print scan header
45
- print_scan_header(path, config.extensions, config.min_lines, config.min_similarity)
45
+ print_scan_header(path, config.extensions, config.min_block_lines, config.min_similarity)
46
46
 
47
47
  # Run analysis
48
48
  if parallel and max_workers and max_workers > 1:
@@ -92,7 +92,7 @@ def check_command(
92
92
  path, extensions, min_lines, min_similarity, include_tests
93
93
  )
94
94
 
95
- print_scan_header(path, config.extensions, config.min_lines, config.min_similarity)
95
+ print_scan_header(path, config.extensions, config.min_block_lines, config.min_similarity)
96
96
 
97
97
  dup_map = analyze(config)
98
98
  print_scan_summary(dup_map)
@@ -6,7 +6,6 @@ from typing import List
6
6
  import typer
7
7
 
8
8
  from redup.core.models import DuplicationMap
9
- from redup.cli_app.main import _apply_fuzzy_similarity
10
9
 
11
10
 
12
11
  def print_scan_header(path: Path, ext_list: List[str], min_lines: int, min_similarity: float) -> None:
@@ -30,5 +29,6 @@ def print_scan_summary(dup_map: DuplicationMap) -> None:
30
29
 
31
30
 
32
31
  def apply_fuzzy_similarity(dup_map: DuplicationMap, threshold: float) -> DuplicationMap:
33
- """Apply fuzzy similarity detection (wrapper for main function)."""
32
+ """Apply fuzzy similarity detection."""
33
+ from redup.cli_app.fuzzy_similarity import _apply_fuzzy_similarity
34
34
  return _apply_fuzzy_similarity(dup_map, threshold)
@@ -16,7 +16,7 @@ from redup.core.hasher import (
16
16
  )
17
17
  from redup.core.lsh_matcher import find_near_duplicates
18
18
  from redup.core.matcher import MatchResult, refine_structural_matches
19
- from redup.core.parallel_scanner import scan_project_parallel
19
+ from redup.core.scanner import scan_project, ScanStrategy
20
20
  from redup.core.models import (
21
21
  DuplicateFragment,
22
22
  DuplicateGroup,
@@ -26,10 +26,9 @@ from redup.core.models import (
26
26
  ScanStats,
27
27
  )
28
28
  from redup.core.planner import generate_suggestions
29
- from redup.core.scanner import CodeBlock, ScannedFile, scan_project
29
+ from redup.core.scanner import CodeBlock, ScannedFile
30
30
  from redup.core.cache import HashCache, build_hash_index_with_cache
31
31
  from redup.core.lazy_grouper import find_all_duplicates_lazy, DuplicateGroupCollector
32
- from redup.core.memory_scanner import scan_project_memory_optimized, scan_project_parallel_memory_optimized
33
32
 
34
33
 
35
34
  def _blocks_to_group(
@@ -184,26 +183,29 @@ def analyze_optimized(
184
183
  # Phase 1: Optimized scanning with memory cache
185
184
  if config.parallel_workers and config.parallel_workers > 1:
186
185
  # Explicit worker count specified
187
- if use_memory_cache:
188
- scanned_files, stats = scan_project_parallel_memory_optimized(
189
- config, config.parallel_workers, max_cache_mb
190
- )
191
- else:
192
- scanned_files, stats = _scan_phase_parallel(config, config.parallel_workers)
186
+ strategy = ScanStrategy(
187
+ parallel=True,
188
+ max_workers=config.parallel_workers,
189
+ memory_cache=use_memory_cache,
190
+ max_cache_mb=max_cache_mb if use_memory_cache else 256
191
+ )
192
+ scanned_files, stats = scan_project(config, strategy, function_level_only=True)
193
193
  elif config.parallel_workers is None and (getattr(config, '_parallel_enabled', False) or config.parallel_workers is None):
194
194
  # Auto-detect CPU count when parallel_workers is None (default behavior)
195
195
  import multiprocessing as mp
196
196
  auto_workers = mp.cpu_count()
197
- if use_memory_cache:
198
- scanned_files, stats = scan_project_parallel_memory_optimized(
199
- config, auto_workers, max_cache_mb
200
- )
201
- else:
202
- scanned_files, stats = _scan_phase_parallel(config, auto_workers)
197
+ strategy = ScanStrategy(
198
+ parallel=True,
199
+ max_workers=auto_workers,
200
+ memory_cache=use_memory_cache,
201
+ max_cache_mb=max_cache_mb if use_memory_cache else 256
202
+ )
203
+ scanned_files, stats = scan_project(config, strategy, function_level_only=True)
203
204
  elif use_memory_cache:
204
- scanned_files, stats = scan_project_memory_optimized(config, max_cache_mb)
205
+ strategy = ScanStrategy(memory_cache=True, max_cache_mb=max_cache_mb)
206
+ scanned_files, stats = scan_project(config, strategy, function_level_only=True)
205
207
  else:
206
- scanned_files, stats = _scan_phase(config)
208
+ scanned_files, stats = scan_project(config, function_level_only=True)
207
209
 
208
210
  # Phase 2: Process blocks
209
211
  all_blocks = _process_blocks(scanned_files, function_level_only)
@@ -260,15 +262,8 @@ def _scan_phase(config: ScanConfig, function_level_only: bool = False) -> tuple[
260
262
 
261
263
  def _scan_phase_parallel(config: ScanConfig, max_workers: int | None = None) -> tuple[list[ScannedFile], ScanStats]:
262
264
  """Phase 1: Scan project files in parallel."""
263
- return scan_project_parallel(
264
- root=config.root,
265
- extensions=config.extensions,
266
- exclude_patterns=config.exclude_patterns,
267
- include_tests=config.include_tests,
268
- function_level_only=True, # Always function-level for parallel
269
- max_file_size=config.max_file_size_kb,
270
- max_workers=max_workers,
271
- )
265
+ strategy = ScanStrategy(parallel=True, max_workers=max_workers or 4)
266
+ return scan_project(config, strategy, function_level_only=True)
272
267
 
273
268
 
274
269
  def _process_blocks(
@@ -552,12 +547,13 @@ def analyze_optimized(
552
547
 
553
548
  try:
554
549
  # Phase 1: Optimized Scan (parallel + memory cache)
555
- if use_memory_cache:
556
- scanned_files, stats = scan_project_parallel_memory_optimized(
557
- config, max_workers=None, max_cache_mb=max_cache_mb
558
- )
559
- else:
560
- scanned_files, stats = scan_project_parallel(config)
550
+ strategy = ScanStrategy(
551
+ parallel=True,
552
+ max_workers=None, # Auto-detect
553
+ memory_cache=use_memory_cache,
554
+ max_cache_mb=max_cache_mb
555
+ )
556
+ scanned_files, stats = scan_project(config, strategy, function_level_only=True)
561
557
 
562
558
  # Phase 2: Process blocks
563
559
  all_blocks = _process_blocks(scanned_files, function_level_only)