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.
- {redup-0.3.22 → redup-0.3.24}/PKG-INFO +3 -2
- {redup-0.3.22 → redup-0.3.24}/README.md +1 -1
- {redup-0.3.22 → redup-0.3.24}/pyproject.toml +2 -1
- {redup-0.3.22 → redup-0.3.24}/src/redup/__init__.py +1 -1
- {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/config_builder.py +9 -3
- redup-0.3.24/src/redup/cli_app/fuzzy_similarity.py +160 -0
- redup-0.3.24/src/redup/cli_app/main.py +219 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/scan_commands.py +4 -4
- {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/scan_helpers.py +2 -2
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/pipeline.py +28 -32
- redup-0.3.24/src/redup/core/scanner.py +647 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/PKG-INFO +3 -2
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/SOURCES.txt +1 -3
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/requires.txt +3 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_e2e.py +6 -4
- redup-0.3.22/src/redup/cli_app/main.py +0 -708
- redup-0.3.22/src/redup/core/memory_scanner.py +0 -285
- redup-0.3.22/src/redup/core/parallel_scanner.py +0 -234
- redup-0.3.22/src/redup/core/scanner.py +0 -373
- redup-0.3.22/src/redup/core/ultra_fast_scanner.py +0 -358
- {redup-0.3.22 → redup-0.3.24}/LICENSE +0 -0
- {redup-0.3.22 → redup-0.3.24}/setup.cfg +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/__main__.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/cli_app/output_writer.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/cache.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/config.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/differ.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/fuzzy_similarity.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/hash_cache.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/hasher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/lazy_grouper.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/models.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/planner.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/universal_fuzzy.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.22 → redup-0.3.24}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_hasher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_models.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_pipeline.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_planner.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_reporters.py +0 -0
- {redup-0.3.22 → redup-0.3.24}/tests/test_scanner.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/redup/)
|
|
84
85
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
85
86
|
[](https://python.org)
|
|
86
|
-
[](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
|
[](https://pypi.org/project/redup/)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://python.org)
|
|
8
|
-
[](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.
|
|
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]
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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)
|