redup 0.2.4__tar.gz → 0.3.1__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.2.4 → redup-0.3.1}/PKG-INFO +28 -6
- {redup-0.2.4 → redup-0.3.1}/README.md +9 -5
- {redup-0.2.4 → redup-0.3.1}/pyproject.toml +19 -1
- {redup-0.2.4 → redup-0.3.1}/src/redup/__init__.py +1 -1
- {redup-0.2.4 → redup-0.3.1}/src/redup/cli_app/main.py +21 -3
- redup-0.3.1/src/redup/core/parallel_scanner.py +185 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/pipeline.py +60 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/reporters/json_reporter.py +20 -13
- {redup-0.2.4 → redup-0.3.1}/src/redup.egg-info/PKG-INFO +28 -6
- {redup-0.2.4 → redup-0.3.1}/src/redup.egg-info/SOURCES.txt +1 -0
- redup-0.3.1/src/redup.egg-info/requires.txt +49 -0
- redup-0.2.4/src/redup.egg-info/requires.txt +0 -31
- {redup-0.2.4 → redup-0.3.1}/LICENSE +0 -0
- {redup-0.2.4 → redup-0.3.1}/setup.cfg +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/__main__.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/__init__.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/config.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/differ.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/hasher.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/matcher.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/models.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/planner.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/scanner.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/reporters/__init__.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.2.4 → redup-0.3.1}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_e2e.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_hasher.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_matcher.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_models.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_pipeline.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_planner.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_reporters.py +0 -0
- {redup-0.2.4 → redup-0.3.1}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -41,6 +41,24 @@ Requires-Dist: tree-sitter-rust>=0.20; extra == "ast"
|
|
|
41
41
|
Requires-Dist: tree-sitter-java>=0.20; extra == "ast"
|
|
42
42
|
Requires-Dist: tree-sitter-c>=0.20; extra == "ast"
|
|
43
43
|
Requires-Dist: tree-sitter-cpp>=0.20; extra == "ast"
|
|
44
|
+
Requires-Dist: tree-sitter-html>=0.20; extra == "ast"
|
|
45
|
+
Requires-Dist: tree-sitter-css>=0.20; extra == "ast"
|
|
46
|
+
Requires-Dist: tree-sitter-sql>=0.20; extra == "ast"
|
|
47
|
+
Requires-Dist: tree-sitter-json>=0.20; extra == "ast"
|
|
48
|
+
Requires-Dist: tree-sitter-yaml>=0.20; extra == "ast"
|
|
49
|
+
Requires-Dist: tree-sitter-toml>=0.20; extra == "ast"
|
|
50
|
+
Requires-Dist: tree-sitter-xml>=0.20; extra == "ast"
|
|
51
|
+
Requires-Dist: tree-sitter-markdown>=0.20; extra == "ast"
|
|
52
|
+
Requires-Dist: tree-sitter-bash>=0.20; extra == "ast"
|
|
53
|
+
Requires-Dist: tree-sitter-ruby>=0.20; extra == "ast"
|
|
54
|
+
Requires-Dist: tree-sitter-php>=0.20; extra == "ast"
|
|
55
|
+
Requires-Dist: tree-sitter-embedded-template>=0.20; extra == "ast"
|
|
56
|
+
Requires-Dist: tree-sitter-regex>=0.20; extra == "ast"
|
|
57
|
+
Requires-Dist: tree-sitter-scala>=0.20; extra == "ast"
|
|
58
|
+
Requires-Dist: tree-sitter-kotlin>=0.20; extra == "ast"
|
|
59
|
+
Requires-Dist: tree-sitter-swift>=0.20; extra == "ast"
|
|
60
|
+
Requires-Dist: tree-sitter-objc>=0.20; extra == "ast"
|
|
61
|
+
Requires-Dist: tree-sitter-c-sharp>=0.20; extra == "ast"
|
|
44
62
|
Provides-Extra: lsh
|
|
45
63
|
Requires-Dist: datasketch>=1.6; extra == "lsh"
|
|
46
64
|
Provides-Extra: dev
|
|
@@ -57,7 +75,7 @@ Dynamic: license-file
|
|
|
57
75
|
[](https://pypi.org/project/redup/)
|
|
58
76
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
59
77
|
[](https://python.org)
|
|
60
|
-
[](https://pypi.org/project/redup/)
|
|
61
79
|
|
|
62
80
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
63
81
|
|
|
@@ -65,14 +83,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
65
83
|
|
|
66
84
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
67
85
|
- **Structural clone detection** — same AST shape, different variable names
|
|
86
|
+
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
87
|
+
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
|
|
88
|
+
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
68
89
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
69
|
-
- **Function-level analysis** using Python AST extraction
|
|
90
|
+
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
70
91
|
- **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
|
|
71
92
|
- **Refactoring planner** — generates concrete extract/inline suggestions
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
93
|
+
- **Multiple output formats**: JSON, YAML, TOON, Markdown
|
|
94
|
+
- **Configuration system** — TOML files and environment variables
|
|
95
|
+
- **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
|
|
96
|
+
- **CI integration** with configurable quality gates
|
|
74
97
|
- **Clean output** — no syntax warnings from external libraries
|
|
75
|
-
- **Optimized performance** — reduced complexity and improved maintainability
|
|
76
98
|
|
|
77
99
|
## Installation
|
|
78
100
|
|
|
@@ -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
|
|
|
@@ -13,14 +13,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
13
13
|
|
|
14
14
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
15
15
|
- **Structural clone detection** — same AST shape, different variable names
|
|
16
|
+
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
17
|
+
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
|
|
18
|
+
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
16
19
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
17
|
-
- **Function-level analysis** using Python AST extraction
|
|
20
|
+
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
18
21
|
- **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
|
|
19
22
|
- **Refactoring planner** — generates concrete extract/inline suggestions
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
23
|
+
- **Multiple output formats**: JSON, YAML, TOON, Markdown
|
|
24
|
+
- **Configuration system** — TOML files and environment variables
|
|
25
|
+
- **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
|
|
26
|
+
- **CI integration** with configurable quality gates
|
|
22
27
|
- **Clean output** — no syntax warnings from external libraries
|
|
23
|
-
- **Optimized performance** — reduced complexity and improved maintainability
|
|
24
28
|
|
|
25
29
|
## Installation
|
|
26
30
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "redup"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Code duplication analyzer and refactoring planner for LLMs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -48,6 +48,24 @@ ast = [
|
|
|
48
48
|
"tree-sitter-java>=0.20",
|
|
49
49
|
"tree-sitter-c>=0.20",
|
|
50
50
|
"tree-sitter-cpp>=0.20",
|
|
51
|
+
"tree-sitter-html>=0.20",
|
|
52
|
+
"tree-sitter-css>=0.20",
|
|
53
|
+
"tree-sitter-sql>=0.20",
|
|
54
|
+
"tree-sitter-json>=0.20",
|
|
55
|
+
"tree-sitter-yaml>=0.20",
|
|
56
|
+
"tree-sitter-toml>=0.20",
|
|
57
|
+
"tree-sitter-xml>=0.20",
|
|
58
|
+
"tree-sitter-markdown>=0.20",
|
|
59
|
+
"tree-sitter-bash>=0.20",
|
|
60
|
+
"tree-sitter-ruby>=0.20",
|
|
61
|
+
"tree-sitter-php>=0.20",
|
|
62
|
+
"tree-sitter-embedded-template>=0.20",
|
|
63
|
+
"tree-sitter-regex>=0.20",
|
|
64
|
+
"tree-sitter-scala>=0.20",
|
|
65
|
+
"tree-sitter-kotlin>=0.20",
|
|
66
|
+
"tree-sitter-swift>=0.20",
|
|
67
|
+
"tree-sitter-objc>=0.20",
|
|
68
|
+
"tree-sitter-c-sharp>=0.20",
|
|
51
69
|
]
|
|
52
70
|
lsh = ["datasketch>=1.6"]
|
|
53
71
|
dev = [
|
|
@@ -15,7 +15,7 @@ warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
|
15
15
|
import redup # noqa: E402
|
|
16
16
|
from redup.core.config import config_to_scan_config, create_sample_redup_toml, load_config # noqa: E402
|
|
17
17
|
from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
|
|
18
|
-
from redup.core.pipeline import analyze # noqa: E402
|
|
18
|
+
from redup.core.pipeline import analyze, analyze_parallel # noqa: E402
|
|
19
19
|
from redup.reporters.json_reporter import to_json # noqa: E402
|
|
20
20
|
from redup.reporters.markdown_reporter import to_markdown # noqa: E402
|
|
21
21
|
from redup.reporters.toon_reporter import to_toon # noqa: E402
|
|
@@ -136,11 +136,15 @@ def _write_results(
|
|
|
136
136
|
]
|
|
137
137
|
for _fmt, renderer, suffix in renderers:
|
|
138
138
|
content = renderer(dup_map)
|
|
139
|
+
if suffix == "json":
|
|
140
|
+
include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
|
|
141
|
+
content = to_json(dup_map, include_snippets=include_snippets)
|
|
139
142
|
target = output_dir / f"duplication.{suffix}"
|
|
140
143
|
target.write_text(content, encoding="utf-8")
|
|
141
144
|
typer.echo(f" → {target}")
|
|
142
145
|
elif format == "json":
|
|
143
|
-
|
|
146
|
+
include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
|
|
147
|
+
_write_output(to_json(dup_map, include_snippets=include_snippets), output_dir, "json")
|
|
144
148
|
elif format == "yaml":
|
|
145
149
|
_write_output(to_yaml(dup_map), output_dir, "yaml")
|
|
146
150
|
elif format == "toon":
|
|
@@ -193,13 +197,27 @@ def scan(
|
|
|
193
197
|
"--functions-only",
|
|
194
198
|
help="Only analyze function-level duplicates (faster).",
|
|
195
199
|
),
|
|
200
|
+
parallel: bool = typer.Option(
|
|
201
|
+
False,
|
|
202
|
+
"--parallel",
|
|
203
|
+
help="Use parallel scanning for large projects.",
|
|
204
|
+
),
|
|
205
|
+
max_workers: int = typer.Option(
|
|
206
|
+
None,
|
|
207
|
+
"--max-workers",
|
|
208
|
+
help="Number of parallel workers (default: CPU count).",
|
|
209
|
+
),
|
|
196
210
|
) -> None:
|
|
197
211
|
"""Scan a project for code duplicates and generate a refactoring map."""
|
|
198
212
|
config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
|
|
199
213
|
|
|
200
214
|
_print_scan_header(path, config.extensions, min_lines, min_similarity)
|
|
201
215
|
|
|
202
|
-
|
|
216
|
+
# Choose analysis method
|
|
217
|
+
if parallel:
|
|
218
|
+
dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
|
|
219
|
+
else:
|
|
220
|
+
dup_map = analyze(config=config, function_level_only=functions_only)
|
|
203
221
|
|
|
204
222
|
_print_scan_summary(dup_map)
|
|
205
223
|
_write_results(dup_map, format, output, path)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Parallel scanning for large projects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from redup.core.scanner import CodeBlock, ScannedFile, scan_project
|
|
11
|
+
from redup.core.ts_extractor import extract_functions_treesitter
|
|
12
|
+
from redup.core.hasher import hash_block, hash_block_structural
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _scan_file_worker(args: tuple[Path, str, list[str], bool, bool, int]) -> ScannedFile:
|
|
16
|
+
"""Worker function for parallel file scanning."""
|
|
17
|
+
file_path, project_root, extensions, include_tests, function_level_only, max_file_size = args
|
|
18
|
+
|
|
19
|
+
# Check if file should be processed
|
|
20
|
+
if not _should_process_file(file_path, project_root, extensions, include_tests, max_file_size):
|
|
21
|
+
return ScannedFile(path=str(file_path), blocks=[])
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# Read file content
|
|
25
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
26
|
+
content = f.read()
|
|
27
|
+
|
|
28
|
+
# Extract code blocks
|
|
29
|
+
blocks = []
|
|
30
|
+
|
|
31
|
+
if function_level_only:
|
|
32
|
+
# Extract function-level blocks only
|
|
33
|
+
if file_path.suffix == '.py':
|
|
34
|
+
from redup.core.scanner import _extract_function_blocks_python
|
|
35
|
+
blocks = _extract_function_blocks_python(content, str(file_path))
|
|
36
|
+
else:
|
|
37
|
+
# Try tree-sitter for other languages
|
|
38
|
+
blocks = extract_functions_treesitter(content, str(file_path))
|
|
39
|
+
else:
|
|
40
|
+
# Extract all blocks (functions + sliding window)
|
|
41
|
+
if file_path.suffix == '.py':
|
|
42
|
+
from redup.core.scanner import _extract_function_blocks_python, _extract_sliding_blocks
|
|
43
|
+
blocks = _extract_function_blocks_python(content, str(file_path))
|
|
44
|
+
blocks.extend(_extract_sliding_blocks(content, str(file_path)))
|
|
45
|
+
else:
|
|
46
|
+
# For non-Python files, just extract functions
|
|
47
|
+
blocks = extract_functions_treesitter(content, str(file_path))
|
|
48
|
+
|
|
49
|
+
return ScannedFile(path=str(file_path), blocks=blocks)
|
|
50
|
+
|
|
51
|
+
except Exception:
|
|
52
|
+
# Return empty file if processing fails
|
|
53
|
+
return ScannedFile(path=str(file_path), blocks=[])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _should_process_file(
|
|
57
|
+
file_path: Path,
|
|
58
|
+
project_root: Path,
|
|
59
|
+
extensions: list[str],
|
|
60
|
+
include_tests: bool,
|
|
61
|
+
max_file_size: int
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""Check if file should be processed."""
|
|
64
|
+
# Check extension
|
|
65
|
+
if file_path.suffix not in extensions:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Check file size
|
|
69
|
+
try:
|
|
70
|
+
if file_path.stat().st_size > max_file_size * 1024:
|
|
71
|
+
return False
|
|
72
|
+
except OSError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
# Check if test file (and whether to include)
|
|
76
|
+
if not include_tests and _is_test_file(file_path, project_root):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_test_file(file_path: Path, project_root: Path) -> bool:
|
|
83
|
+
"""Check if file is a test file."""
|
|
84
|
+
relative_path = file_path.relative_to(project_root)
|
|
85
|
+
|
|
86
|
+
# Check if in test directory
|
|
87
|
+
parts = relative_path.parts
|
|
88
|
+
for part in parts:
|
|
89
|
+
if part in ('test', 'tests', 'testing'):
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
# Check filename patterns
|
|
93
|
+
filename = file_path.name
|
|
94
|
+
if filename.startswith('test_') or filename.endswith('_test.py'):
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def scan_project_parallel(
|
|
101
|
+
root: Path,
|
|
102
|
+
extensions: list[str],
|
|
103
|
+
exclude_patterns: list[str] | None = None,
|
|
104
|
+
include_tests: bool = False,
|
|
105
|
+
function_level_only: bool = False,
|
|
106
|
+
max_file_size: int = 1024,
|
|
107
|
+
max_workers: int | None = None,
|
|
108
|
+
min_files_for_parallel: int = 10
|
|
109
|
+
) -> tuple[list[ScannedFile], Any]:
|
|
110
|
+
"""Scan project files in parallel for better performance on large projects."""
|
|
111
|
+
from redup.core.models import ScanStats
|
|
112
|
+
|
|
113
|
+
if max_workers is None:
|
|
114
|
+
max_workers = min(os.cpu_count() or 4, 8) # Cap at 8 workers
|
|
115
|
+
|
|
116
|
+
# Find all files to scan
|
|
117
|
+
all_files = []
|
|
118
|
+
exclude_patterns = exclude_patterns or []
|
|
119
|
+
|
|
120
|
+
for file_path in root.rglob('*'):
|
|
121
|
+
if not file_path.is_file():
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Check exclude patterns
|
|
125
|
+
relative_path = file_path.relative_to(root)
|
|
126
|
+
should_exclude = False
|
|
127
|
+
|
|
128
|
+
for pattern in exclude_patterns:
|
|
129
|
+
if pattern in str(relative_path) or relative_path.match(pattern):
|
|
130
|
+
should_exclude = True
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
if should_exclude:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
all_files.append(file_path)
|
|
137
|
+
|
|
138
|
+
# Use parallel scanning only if we have enough files
|
|
139
|
+
if len(all_files) < min_files_for_parallel:
|
|
140
|
+
# Fall back to sequential scanning
|
|
141
|
+
return scan_project(root, extensions, exclude_patterns, include_tests, function_level_only, max_file_size)
|
|
142
|
+
|
|
143
|
+
# Prepare work items
|
|
144
|
+
work_items = [
|
|
145
|
+
(file_path, root, extensions, include_tests, function_level_only, max_file_size)
|
|
146
|
+
for file_path in all_files
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# Scan files in parallel
|
|
150
|
+
scanned_files = []
|
|
151
|
+
total_blocks = 0
|
|
152
|
+
total_lines = 0
|
|
153
|
+
|
|
154
|
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
155
|
+
# Submit all jobs
|
|
156
|
+
future_to_file = {
|
|
157
|
+
executor.submit(_scan_file_worker, work_item): work_item[0]
|
|
158
|
+
for work_item in work_items
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Collect results
|
|
162
|
+
for future in as_completed(future_to_file):
|
|
163
|
+
try:
|
|
164
|
+
scanned_file = future.result()
|
|
165
|
+
scanned_files.append(scanned_file)
|
|
166
|
+
|
|
167
|
+
# Update statistics
|
|
168
|
+
for block in scanned_file.blocks:
|
|
169
|
+
total_blocks += 1
|
|
170
|
+
total_lines += block.line_count
|
|
171
|
+
|
|
172
|
+
except Exception:
|
|
173
|
+
# Log error but continue processing other files
|
|
174
|
+
file_path = future_to_file[future]
|
|
175
|
+
print(f"Warning: Failed to scan {file_path}")
|
|
176
|
+
|
|
177
|
+
# Create stats
|
|
178
|
+
stats = ScanStats(
|
|
179
|
+
files_scanned=len(scanned_files),
|
|
180
|
+
total_lines=total_lines,
|
|
181
|
+
total_blocks=total_blocks,
|
|
182
|
+
scan_time_ms=0.0, # Will be updated by caller
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return scanned_files, stats
|
|
@@ -11,6 +11,7 @@ from redup.core.hasher import (
|
|
|
11
11
|
)
|
|
12
12
|
from redup.core.lsh_matcher import find_near_duplicates
|
|
13
13
|
from redup.core.matcher import MatchResult, refine_structural_matches
|
|
14
|
+
from redup.core.parallel_scanner import scan_project_parallel
|
|
14
15
|
from redup.core.models import (
|
|
15
16
|
DuplicateFragment,
|
|
16
17
|
DuplicateGroup,
|
|
@@ -143,6 +144,19 @@ def _scan_phase(config: ScanConfig) -> tuple[list[ScannedFile], ScanStats]:
|
|
|
143
144
|
return scan_project(config)
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
def _scan_phase_parallel(config: ScanConfig, max_workers: int | None = None) -> tuple[list[ScannedFile], ScanStats]:
|
|
148
|
+
"""Phase 1: Scan project files in parallel."""
|
|
149
|
+
return scan_project_parallel(
|
|
150
|
+
root=config.root,
|
|
151
|
+
extensions=config.extensions,
|
|
152
|
+
exclude_patterns=config.exclude_patterns,
|
|
153
|
+
include_tests=config.include_tests,
|
|
154
|
+
function_level_only=True, # Always function-level for parallel
|
|
155
|
+
max_file_size=config.max_file_size_kb,
|
|
156
|
+
max_workers=max_workers,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
146
160
|
def _process_blocks(
|
|
147
161
|
scanned_files: list[ScannedFile],
|
|
148
162
|
function_level_only: bool
|
|
@@ -330,3 +344,49 @@ def _find_near_duplicate_groups(
|
|
|
330
344
|
return groups
|
|
331
345
|
|
|
332
346
|
|
|
347
|
+
def analyze_parallel(
|
|
348
|
+
config: ScanConfig | None = None,
|
|
349
|
+
function_level_only: bool = False,
|
|
350
|
+
max_workers: int | None = None,
|
|
351
|
+
) -> DuplicationMap:
|
|
352
|
+
"""Run reDUP analysis with parallel scanning for large projects.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
config: Scan configuration. Defaults to current directory, .py files.
|
|
356
|
+
function_level_only: If True, only analyze function-level blocks.
|
|
357
|
+
max_workers: Number of parallel workers. Defaults to CPU count.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
A DuplicationMap with all duplicate groups and refactoring suggestions.
|
|
361
|
+
"""
|
|
362
|
+
import time
|
|
363
|
+
start_time = time.time()
|
|
364
|
+
|
|
365
|
+
config = _ensure_config(config)
|
|
366
|
+
|
|
367
|
+
# Phase 1: Parallel Scan
|
|
368
|
+
scanned_files, stats = _scan_phase_parallel(config, max_workers)
|
|
369
|
+
|
|
370
|
+
# Phase 2: Process blocks
|
|
371
|
+
all_blocks = _process_blocks(scanned_files, function_level_only)
|
|
372
|
+
|
|
373
|
+
# Phase 3: Hash and find duplicates
|
|
374
|
+
groups = _find_duplicates_phase(all_blocks, config)
|
|
375
|
+
|
|
376
|
+
# Phase 4: Deduplicate and suggest
|
|
377
|
+
final_groups = _deduplicate_phase(groups)
|
|
378
|
+
|
|
379
|
+
# Update scan time
|
|
380
|
+
stats.scan_time_ms = (time.time() - start_time) * 1000
|
|
381
|
+
|
|
382
|
+
dup_map = DuplicationMap(
|
|
383
|
+
project_path=config.root.as_posix(),
|
|
384
|
+
config=config,
|
|
385
|
+
stats=stats,
|
|
386
|
+
groups=final_groups,
|
|
387
|
+
)
|
|
388
|
+
dup_map.suggestions = generate_suggestions(dup_map)
|
|
389
|
+
|
|
390
|
+
return dup_map
|
|
391
|
+
|
|
392
|
+
|
|
@@ -8,7 +8,23 @@ from typing import Any
|
|
|
8
8
|
from redup.core.models import DuplicationMap
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _group_to_dict(group: Any) -> dict:
|
|
11
|
+
def _group_to_dict(group: Any, include_snippets: bool = False) -> dict:
|
|
12
|
+
fragments = []
|
|
13
|
+
for f in group.fragments:
|
|
14
|
+
fragment_data = {
|
|
15
|
+
"file": f.file,
|
|
16
|
+
"line_start": f.line_start,
|
|
17
|
+
"line_end": f.line_end,
|
|
18
|
+
"function_name": f.function_name,
|
|
19
|
+
"class_name": f.class_name,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Include code snippet if requested
|
|
23
|
+
if include_snippets and hasattr(f, 'text') and f.text:
|
|
24
|
+
fragment_data["snippet"] = f.text
|
|
25
|
+
|
|
26
|
+
fragments.append(fragment_data)
|
|
27
|
+
|
|
12
28
|
return {
|
|
13
29
|
"id": group.id,
|
|
14
30
|
"type": group.duplicate_type.value,
|
|
@@ -19,16 +35,7 @@ def _group_to_dict(group: Any) -> dict:
|
|
|
19
35
|
"occurrences": group.occurrences,
|
|
20
36
|
"saved_lines_potential": group.saved_lines_potential,
|
|
21
37
|
"impact_score": round(group.impact_score, 1),
|
|
22
|
-
"fragments":
|
|
23
|
-
{
|
|
24
|
-
"file": f.file,
|
|
25
|
-
"line_start": f.line_start,
|
|
26
|
-
"line_end": f.line_end,
|
|
27
|
-
"function_name": f.function_name,
|
|
28
|
-
"class_name": f.class_name,
|
|
29
|
-
}
|
|
30
|
-
for f in group.fragments
|
|
31
|
-
],
|
|
38
|
+
"fragments": fragments,
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
|
|
@@ -45,7 +52,7 @@ def _suggestion_to_dict(s: Any) -> dict:
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
|
|
48
|
-
def to_json(dup_map: DuplicationMap, indent: int = 2) -> str:
|
|
55
|
+
def to_json(dup_map: DuplicationMap, indent: int = 2, include_snippets: bool = False) -> str:
|
|
49
56
|
"""Serialize a DuplicationMap to JSON string."""
|
|
50
57
|
data = {
|
|
51
58
|
"project_path": dup_map.project_path,
|
|
@@ -60,7 +67,7 @@ def to_json(dup_map: DuplicationMap, indent: int = 2) -> str:
|
|
|
60
67
|
"total_fragments": dup_map.total_fragments,
|
|
61
68
|
"total_saved_lines": dup_map.total_saved_lines,
|
|
62
69
|
},
|
|
63
|
-
"groups": [_group_to_dict(g) for g in dup_map.sorted_by_impact()],
|
|
70
|
+
"groups": [_group_to_dict(g, include_snippets) for g in dup_map.sorted_by_impact()],
|
|
64
71
|
"refactor_suggestions": [_suggestion_to_dict(s) for s in dup_map.suggestions],
|
|
65
72
|
}
|
|
66
73
|
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -41,6 +41,24 @@ Requires-Dist: tree-sitter-rust>=0.20; extra == "ast"
|
|
|
41
41
|
Requires-Dist: tree-sitter-java>=0.20; extra == "ast"
|
|
42
42
|
Requires-Dist: tree-sitter-c>=0.20; extra == "ast"
|
|
43
43
|
Requires-Dist: tree-sitter-cpp>=0.20; extra == "ast"
|
|
44
|
+
Requires-Dist: tree-sitter-html>=0.20; extra == "ast"
|
|
45
|
+
Requires-Dist: tree-sitter-css>=0.20; extra == "ast"
|
|
46
|
+
Requires-Dist: tree-sitter-sql>=0.20; extra == "ast"
|
|
47
|
+
Requires-Dist: tree-sitter-json>=0.20; extra == "ast"
|
|
48
|
+
Requires-Dist: tree-sitter-yaml>=0.20; extra == "ast"
|
|
49
|
+
Requires-Dist: tree-sitter-toml>=0.20; extra == "ast"
|
|
50
|
+
Requires-Dist: tree-sitter-xml>=0.20; extra == "ast"
|
|
51
|
+
Requires-Dist: tree-sitter-markdown>=0.20; extra == "ast"
|
|
52
|
+
Requires-Dist: tree-sitter-bash>=0.20; extra == "ast"
|
|
53
|
+
Requires-Dist: tree-sitter-ruby>=0.20; extra == "ast"
|
|
54
|
+
Requires-Dist: tree-sitter-php>=0.20; extra == "ast"
|
|
55
|
+
Requires-Dist: tree-sitter-embedded-template>=0.20; extra == "ast"
|
|
56
|
+
Requires-Dist: tree-sitter-regex>=0.20; extra == "ast"
|
|
57
|
+
Requires-Dist: tree-sitter-scala>=0.20; extra == "ast"
|
|
58
|
+
Requires-Dist: tree-sitter-kotlin>=0.20; extra == "ast"
|
|
59
|
+
Requires-Dist: tree-sitter-swift>=0.20; extra == "ast"
|
|
60
|
+
Requires-Dist: tree-sitter-objc>=0.20; extra == "ast"
|
|
61
|
+
Requires-Dist: tree-sitter-c-sharp>=0.20; extra == "ast"
|
|
44
62
|
Provides-Extra: lsh
|
|
45
63
|
Requires-Dist: datasketch>=1.6; extra == "lsh"
|
|
46
64
|
Provides-Extra: dev
|
|
@@ -57,7 +75,7 @@ Dynamic: license-file
|
|
|
57
75
|
[](https://pypi.org/project/redup/)
|
|
58
76
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
59
77
|
[](https://python.org)
|
|
60
|
-
[](https://pypi.org/project/redup/)
|
|
61
79
|
|
|
62
80
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
63
81
|
|
|
@@ -65,14 +83,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
65
83
|
|
|
66
84
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
67
85
|
- **Structural clone detection** — same AST shape, different variable names
|
|
86
|
+
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
87
|
+
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
|
|
88
|
+
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
68
89
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
69
|
-
- **Function-level analysis** using Python AST extraction
|
|
90
|
+
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
70
91
|
- **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
|
|
71
92
|
- **Refactoring planner** — generates concrete extract/inline suggestions
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
93
|
+
- **Multiple output formats**: JSON, YAML, TOON, Markdown
|
|
94
|
+
- **Configuration system** — TOML files and environment variables
|
|
95
|
+
- **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
|
|
96
|
+
- **CI integration** with configurable quality gates
|
|
74
97
|
- **Clean output** — no syntax warnings from external libraries
|
|
75
|
-
- **Optimized performance** — reduced complexity and improved maintainability
|
|
76
98
|
|
|
77
99
|
## Installation
|
|
78
100
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
pyyaml>=6.0
|
|
2
|
+
typer>=0.12.0
|
|
3
|
+
rich>=13.0
|
|
4
|
+
pydantic>=2.0
|
|
5
|
+
|
|
6
|
+
[all]
|
|
7
|
+
redup[fuzzy]
|
|
8
|
+
redup[ast]
|
|
9
|
+
redup[lsh]
|
|
10
|
+
|
|
11
|
+
[ast]
|
|
12
|
+
tree-sitter>=0.21
|
|
13
|
+
tree-sitter-javascript>=0.20
|
|
14
|
+
tree-sitter-typescript>=0.20
|
|
15
|
+
tree-sitter-go>=0.20
|
|
16
|
+
tree-sitter-rust>=0.20
|
|
17
|
+
tree-sitter-java>=0.20
|
|
18
|
+
tree-sitter-c>=0.20
|
|
19
|
+
tree-sitter-cpp>=0.20
|
|
20
|
+
tree-sitter-html>=0.20
|
|
21
|
+
tree-sitter-css>=0.20
|
|
22
|
+
tree-sitter-sql>=0.20
|
|
23
|
+
tree-sitter-json>=0.20
|
|
24
|
+
tree-sitter-yaml>=0.20
|
|
25
|
+
tree-sitter-toml>=0.20
|
|
26
|
+
tree-sitter-xml>=0.20
|
|
27
|
+
tree-sitter-markdown>=0.20
|
|
28
|
+
tree-sitter-bash>=0.20
|
|
29
|
+
tree-sitter-ruby>=0.20
|
|
30
|
+
tree-sitter-php>=0.20
|
|
31
|
+
tree-sitter-embedded-template>=0.20
|
|
32
|
+
tree-sitter-regex>=0.20
|
|
33
|
+
tree-sitter-scala>=0.20
|
|
34
|
+
tree-sitter-kotlin>=0.20
|
|
35
|
+
tree-sitter-swift>=0.20
|
|
36
|
+
tree-sitter-objc>=0.20
|
|
37
|
+
tree-sitter-c-sharp>=0.20
|
|
38
|
+
|
|
39
|
+
[dev]
|
|
40
|
+
pytest>=7.0
|
|
41
|
+
pytest-cov>=4.0
|
|
42
|
+
ruff>=0.4
|
|
43
|
+
mypy>=1.8
|
|
44
|
+
|
|
45
|
+
[fuzzy]
|
|
46
|
+
rapidfuzz>=3.0
|
|
47
|
+
|
|
48
|
+
[lsh]
|
|
49
|
+
datasketch>=1.6
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
pyyaml>=6.0
|
|
2
|
-
typer>=0.12.0
|
|
3
|
-
rich>=13.0
|
|
4
|
-
pydantic>=2.0
|
|
5
|
-
|
|
6
|
-
[all]
|
|
7
|
-
redup[fuzzy]
|
|
8
|
-
redup[ast]
|
|
9
|
-
redup[lsh]
|
|
10
|
-
|
|
11
|
-
[ast]
|
|
12
|
-
tree-sitter>=0.21
|
|
13
|
-
tree-sitter-javascript>=0.20
|
|
14
|
-
tree-sitter-typescript>=0.20
|
|
15
|
-
tree-sitter-go>=0.20
|
|
16
|
-
tree-sitter-rust>=0.20
|
|
17
|
-
tree-sitter-java>=0.20
|
|
18
|
-
tree-sitter-c>=0.20
|
|
19
|
-
tree-sitter-cpp>=0.20
|
|
20
|
-
|
|
21
|
-
[dev]
|
|
22
|
-
pytest>=7.0
|
|
23
|
-
pytest-cov>=4.0
|
|
24
|
-
ruff>=0.4
|
|
25
|
-
mypy>=1.8
|
|
26
|
-
|
|
27
|
-
[fuzzy]
|
|
28
|
-
rapidfuzz>=3.0
|
|
29
|
-
|
|
30
|
-
[lsh]
|
|
31
|
-
datasketch>=1.6
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|