redup 0.3.22__tar.gz → 0.3.23__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.23}/PKG-INFO +3 -2
- {redup-0.3.22 → redup-0.3.23}/README.md +1 -1
- {redup-0.3.22 → redup-0.3.23}/pyproject.toml +2 -1
- {redup-0.3.22 → redup-0.3.23}/src/redup/__init__.py +1 -1
- {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/config_builder.py +7 -1
- redup-0.3.23/src/redup/cli_app/fuzzy_similarity.py +160 -0
- redup-0.3.23/src/redup/cli_app/main.py +219 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/scan_helpers.py +2 -2
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/PKG-INFO +3 -2
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/SOURCES.txt +1 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/requires.txt +3 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_e2e.py +6 -4
- redup-0.3.22/src/redup/cli_app/main.py +0 -708
- {redup-0.3.22 → redup-0.3.23}/LICENSE +0 -0
- {redup-0.3.22 → redup-0.3.23}/setup.cfg +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/__main__.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/output_writer.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/cli_app/scan_commands.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/cache.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/config.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/differ.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/fuzzy_similarity.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/hash_cache.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/hasher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/lazy_grouper.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/memory_scanner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/models.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/parallel_scanner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/pipeline.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/planner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/scanner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/ultra_fast_scanner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/universal_fuzzy.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.22 → redup-0.3.23}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_hasher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_matcher.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_models.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_pipeline.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_planner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_reporters.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/tests/test_scanner.py +0 -0
- {redup-0.3.22 → redup-0.3.23}/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.23
|
|
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.23"
|
|
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()
|
|
@@ -48,4 +50,8 @@ def build_config_with_file_support(
|
|
|
48
50
|
if functions_only:
|
|
49
51
|
scan_config.function_level_only = functions_only
|
|
50
52
|
|
|
53
|
+
# Add fuzzy support
|
|
54
|
+
scan_config.fuzzy_enabled = fuzzy
|
|
55
|
+
scan_config.fuzzy_threshold = fuzzy_threshold
|
|
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()
|
|
@@ -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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.23
|
|
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
|
|
|
@@ -11,6 +11,7 @@ src/redup.egg-info/requires.txt
|
|
|
11
11
|
src/redup.egg-info/top_level.txt
|
|
12
12
|
src/redup/cli_app/__init__.py
|
|
13
13
|
src/redup/cli_app/config_builder.py
|
|
14
|
+
src/redup/cli_app/fuzzy_similarity.py
|
|
14
15
|
src/redup/cli_app/main.py
|
|
15
16
|
src/redup/cli_app/output_writer.py
|
|
16
17
|
src/redup/cli_app/scan_commands.py
|
|
@@ -355,8 +355,11 @@ class TestFullRoundtrip:
|
|
|
355
355
|
assert json_file.exists()
|
|
356
356
|
|
|
357
357
|
data = json.loads(json_file.read_text())
|
|
358
|
+
self._verify_json_structure(data)
|
|
359
|
+
self._verify_calculate_tax_group(data)
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
def _verify_json_structure(self, data: dict) -> None:
|
|
362
|
+
"""Verify basic JSON structure."""
|
|
360
363
|
assert "project_path" in data
|
|
361
364
|
assert "stats" in data
|
|
362
365
|
assert "summary" in data
|
|
@@ -366,11 +369,10 @@ class TestFullRoundtrip:
|
|
|
366
369
|
# Verify stats
|
|
367
370
|
assert data["stats"]["files_scanned"] == 4
|
|
368
371
|
assert data["stats"]["total_lines"] > 0
|
|
369
|
-
|
|
370
|
-
# Verify duplicates found
|
|
371
372
|
assert data["summary"]["total_groups"] >= 1
|
|
372
373
|
|
|
373
|
-
|
|
374
|
+
def _verify_calculate_tax_group(self, data: dict) -> None:
|
|
375
|
+
"""Verify the calculate_tax group specifically."""
|
|
374
376
|
tax_groups = [
|
|
375
377
|
g for g in data["groups"]
|
|
376
378
|
if g.get("normalized_name") == "calculate_tax"
|