redup 0.3.16__tar.gz → 0.3.18__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.16 → redup-0.3.18}/PKG-INFO +2 -2
- {redup-0.3.16 → redup-0.3.18}/README.md +1 -1
- {redup-0.3.16 → redup-0.3.18}/pyproject.toml +1 -1
- {redup-0.3.16 → redup-0.3.18}/src/redup/__init__.py +1 -1
- {redup-0.3.16 → redup-0.3.18}/src/redup/cli_app/main.py +18 -13
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/hasher.py +1 -1
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/lazy_grouper.py +4 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/memory_scanner.py +34 -30
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/pipeline.py +13 -6
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/scanner.py +1 -3
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/ultra_fast_scanner.py +1 -5
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/PKG-INFO +2 -2
- {redup-0.3.16 → redup-0.3.18}/LICENSE +0 -0
- {redup-0.3.16 → redup-0.3.18}/setup.cfg +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/__main__.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/__init__.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/cache.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/config.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/differ.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/hash_cache.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/matcher.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/models.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/parallel_scanner.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/planner.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/SOURCES.txt +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/requires.txt +0 -0
- {redup-0.3.16 → redup-0.3.18}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_e2e.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_hasher.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_matcher.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_models.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_pipeline.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_planner.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_reporters.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/tests/test_scanner.py +0 -0
- {redup-0.3.16 → redup-0.3.18}/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.18
|
|
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
|
|
@@ -83,7 +83,7 @@ Dynamic: license-file
|
|
|
83
83
|
[](https://pypi.org/project/redup/)
|
|
84
84
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
85
85
|
[](https://python.org)
|
|
86
|
-
[](https://pypi.org/project/redup/)
|
|
87
87
|
|
|
88
88
|
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
89
|
|
|
@@ -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
|
|
|
@@ -100,8 +100,10 @@ def _build_config_with_file_support(
|
|
|
100
100
|
# Apply performance options
|
|
101
101
|
if parallel:
|
|
102
102
|
config.parallel_workers = max_workers
|
|
103
|
+
config._parallel_enabled = True
|
|
103
104
|
elif max_workers is not None:
|
|
104
105
|
config.parallel_workers = max_workers
|
|
106
|
+
config._parallel_enabled = True
|
|
105
107
|
|
|
106
108
|
if incremental:
|
|
107
109
|
config.enable_cache = True
|
|
@@ -241,9 +243,9 @@ def scan(
|
|
|
241
243
|
help="Only analyze function-level duplicates (default: enabled for speed).",
|
|
242
244
|
),
|
|
243
245
|
parallel: bool = typer.Option(
|
|
244
|
-
|
|
246
|
+
False,
|
|
245
247
|
"--parallel/--no-parallel",
|
|
246
|
-
help="Use parallel scanning for large projects (default:
|
|
248
|
+
help="Use parallel scanning for large projects (default: disabled due to issues).",
|
|
247
249
|
),
|
|
248
250
|
max_workers: int = typer.Option(
|
|
249
251
|
None,
|
|
@@ -251,9 +253,9 @@ def scan(
|
|
|
251
253
|
help="Number of parallel workers (default: CPU count).",
|
|
252
254
|
),
|
|
253
255
|
incremental: bool = typer.Option(
|
|
254
|
-
|
|
256
|
+
False,
|
|
255
257
|
"--incremental/--no-incremental",
|
|
256
|
-
help="Use hash cache to skip unchanged files (default:
|
|
258
|
+
help="Use hash cache to skip unchanged files (default: disabled due to issues).",
|
|
257
259
|
),
|
|
258
260
|
memory_cache: bool = typer.Option(
|
|
259
261
|
True,
|
|
@@ -268,13 +270,13 @@ def scan(
|
|
|
268
270
|
) -> None:
|
|
269
271
|
"""Scan a project for code duplicates and generate a refactoring map.
|
|
270
272
|
|
|
271
|
-
Performance optimizations
|
|
272
|
-
- ✅ --functions-only: Faster function-level analysis
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
- ✅ --memory-cache: RAM preload for speed
|
|
273
|
+
Performance optimizations:
|
|
274
|
+
- ✅ --functions-only: Faster function-level analysis (enabled)
|
|
275
|
+
- ⚠️ --parallel: Multi-core processing (disabled due to issues)
|
|
276
|
+
- ⚠️ --incremental: Skip unchanged files (disabled due to issues)
|
|
277
|
+
- ✅ --memory-cache: RAM preload for speed (enabled)
|
|
276
278
|
|
|
277
|
-
Use --
|
|
279
|
+
Use --parallel and/or --incremental when needed.
|
|
278
280
|
"""
|
|
279
281
|
try:
|
|
280
282
|
config = _build_config_with_file_support(
|
|
@@ -285,16 +287,19 @@ def scan(
|
|
|
285
287
|
_print_scan_header(path, config.extensions, config.min_block_lines, config.min_similarity)
|
|
286
288
|
|
|
287
289
|
# Choose analysis method based on optimization flags
|
|
288
|
-
if parallel
|
|
290
|
+
if parallel and not incremental:
|
|
291
|
+
# Pure parallel mode (no cache)
|
|
292
|
+
dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
|
|
293
|
+
elif parallel or incremental:
|
|
294
|
+
# Optimized mode with cache and/or parallel
|
|
289
295
|
dup_map = analyze_optimized(
|
|
290
296
|
config=config,
|
|
291
297
|
function_level_only=functions_only,
|
|
292
298
|
use_memory_cache=getattr(config, '_memory_cache', True),
|
|
293
299
|
max_cache_mb=getattr(config, '_max_cache_mb', 512)
|
|
294
300
|
)
|
|
295
|
-
elif parallel:
|
|
296
|
-
dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
|
|
297
301
|
else:
|
|
302
|
+
# Standard mode
|
|
298
303
|
dup_map = analyze(config=config, function_level_only=functions_only)
|
|
299
304
|
|
|
300
305
|
_print_scan_summary(dup_map)
|
|
@@ -39,7 +39,7 @@ def _normalize_text(text: str) -> str:
|
|
|
39
39
|
# Vectorized: single list comprehension pass instead of loop
|
|
40
40
|
lines = text.splitlines()
|
|
41
41
|
result_lines = [
|
|
42
|
-
|
|
42
|
+
processed for line in lines
|
|
43
43
|
if (line.strip() and not _MULTILINE_STRING_RE.match(line))
|
|
44
44
|
for processed in [_COMMENT_RE.sub("", line).strip()]
|
|
45
45
|
if processed # Skip empty lines (only comments)
|
|
@@ -74,11 +74,15 @@ def _create_duplicate_group(
|
|
|
74
74
|
for hb in blocks
|
|
75
75
|
]
|
|
76
76
|
|
|
77
|
+
# Extract function name from first fragment if available
|
|
78
|
+
normalized_name = fragments[0].function_name if fragments and fragments[0].function_name else None
|
|
79
|
+
|
|
77
80
|
return DuplicateGroup(
|
|
78
81
|
id=hash_val,
|
|
79
82
|
duplicate_type=duplicate_type,
|
|
80
83
|
fragments=fragments,
|
|
81
84
|
normalized_hash=hash_val,
|
|
85
|
+
normalized_name=normalized_name,
|
|
82
86
|
)
|
|
83
87
|
|
|
84
88
|
|
|
@@ -144,16 +144,18 @@ def scan_project_memory_optimized(
|
|
|
144
144
|
func_blocks = []
|
|
145
145
|
class_blocks = []
|
|
146
146
|
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
147
|
+
# Add sliding window blocks only if not functions_only
|
|
148
|
+
all_blocks = func_blocks + class_blocks
|
|
149
|
+
if not (config.functions_only or getattr(config, 'functions_only', False)):
|
|
150
|
+
# Also extract sliding-window blocks
|
|
151
|
+
from redup.core.scanner import _extract_blocks_sliding
|
|
152
|
+
lines = content.splitlines()
|
|
153
|
+
sliding = _extract_blocks_sliding(lines, config.min_block_lines)
|
|
154
|
+
line_blocks = [
|
|
155
|
+
CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
|
|
156
|
+
for s, e, t in sliding
|
|
157
|
+
]
|
|
158
|
+
all_blocks.extend(line_blocks)
|
|
157
159
|
|
|
158
160
|
sf = ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
|
|
159
161
|
scanned.append(sf)
|
|
@@ -202,22 +204,18 @@ def scan_project_parallel_memory_optimized(
|
|
|
202
204
|
if len(files) < 10 or workers == 1:
|
|
203
205
|
return scan_project_memory_optimized(config, max_cache_mb)
|
|
204
206
|
|
|
205
|
-
# Initialize
|
|
207
|
+
# Initialize memory cache and preload files in main process
|
|
206
208
|
cache = MemoryFileCache(max_cache_mb)
|
|
207
|
-
|
|
208
|
-
# Preload files into cache
|
|
209
209
|
cache.preload_files(files)
|
|
210
210
|
print(f"📁 Preloaded {cache.get_stats()['cached_files']} files for parallel processing")
|
|
211
211
|
|
|
212
212
|
scanned: list[ScannedFile] = []
|
|
213
213
|
|
|
214
|
-
def
|
|
215
|
-
"""Worker function that
|
|
214
|
+
def process_file_with_content(args: tuple[Path, bytes]) -> ScannedFile:
|
|
215
|
+
"""Worker function that receives content directly."""
|
|
216
|
+
filepath, content_bytes = args
|
|
216
217
|
try:
|
|
217
|
-
content_bytes = cache.get_file_content(filepath)
|
|
218
218
|
content = content_bytes.decode('utf-8', errors='replace')
|
|
219
|
-
|
|
220
|
-
# Use existing _process_file logic but with cached content
|
|
221
219
|
rel_path = str(filepath.relative_to(config.root.resolve()))
|
|
222
220
|
|
|
223
221
|
# Extract blocks
|
|
@@ -226,6 +224,7 @@ def scan_project_parallel_memory_optimized(
|
|
|
226
224
|
func_blocks = _extract_function_blocks_python(content, rel_path)
|
|
227
225
|
class_blocks = _extract_class_blocks_python(content, rel_path)
|
|
228
226
|
else:
|
|
227
|
+
# Try tree-sitter for other languages
|
|
229
228
|
try:
|
|
230
229
|
from redup.core.ts_extractor import extract_functions_treesitter, is_language_supported
|
|
231
230
|
if is_language_supported(rel_path):
|
|
@@ -236,27 +235,32 @@ def scan_project_parallel_memory_optimized(
|
|
|
236
235
|
func_blocks = []
|
|
237
236
|
class_blocks = []
|
|
238
237
|
|
|
239
|
-
#
|
|
240
|
-
|
|
238
|
+
# Add sliding window blocks only if not functions_only
|
|
239
|
+
all_blocks = func_blocks + class_blocks
|
|
241
240
|
lines = content.splitlines()
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
if not (config.functions_only or getattr(config, 'functions_only', False)):
|
|
242
|
+
# Sliding window blocks for line-level duplicates
|
|
243
|
+
from redup.core.scanner import _extract_blocks_sliding
|
|
244
|
+
sliding = _extract_blocks_sliding(lines, config.min_block_lines)
|
|
245
|
+
line_blocks = [
|
|
246
|
+
CodeBlock(file=rel_path, line_start=s, line_end=e, text=t)
|
|
247
|
+
for s, e, t in sliding
|
|
248
|
+
]
|
|
249
|
+
all_blocks.extend(line_blocks)
|
|
249
250
|
|
|
250
251
|
return ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
|
|
251
252
|
|
|
252
253
|
except (OSError, PermissionError):
|
|
253
254
|
return ScannedFile(path=str(filepath), lines=[], blocks=[])
|
|
254
255
|
|
|
255
|
-
#
|
|
256
|
+
# Prepare (filepath, content) pairs from cache
|
|
257
|
+
file_content_pairs = [(f, cache.get_file_content(f)) for f in files]
|
|
258
|
+
|
|
259
|
+
# Process files in parallel using cached content passed as arguments
|
|
256
260
|
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
257
261
|
future_to_file = {
|
|
258
|
-
executor.submit(
|
|
259
|
-
for
|
|
262
|
+
executor.submit(process_file_with_content, pair): pair[0]
|
|
263
|
+
for pair in file_content_pairs
|
|
260
264
|
}
|
|
261
265
|
|
|
262
266
|
for future in as_completed(future_to_file):
|
|
@@ -30,7 +30,6 @@ from redup.core.scanner import CodeBlock, ScannedFile, scan_project
|
|
|
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
32
|
from redup.core.memory_scanner import scan_project_memory_optimized, scan_project_parallel_memory_optimized
|
|
33
|
-
from redup.core.ultra_fast_scanner import scan_project_ultra_fast
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def _blocks_to_group(
|
|
@@ -184,12 +183,23 @@ def analyze_optimized(
|
|
|
184
183
|
try:
|
|
185
184
|
# Phase 1: Optimized scanning with memory cache
|
|
186
185
|
if config.parallel_workers and config.parallel_workers > 1:
|
|
186
|
+
# Explicit worker count specified
|
|
187
187
|
if use_memory_cache:
|
|
188
188
|
scanned_files, stats = scan_project_parallel_memory_optimized(
|
|
189
189
|
config, config.parallel_workers, max_cache_mb
|
|
190
190
|
)
|
|
191
191
|
else:
|
|
192
192
|
scanned_files, stats = _scan_phase_parallel(config, config.parallel_workers)
|
|
193
|
+
elif config.parallel_workers is None and (getattr(config, '_parallel_enabled', False) or config.parallel_workers is None):
|
|
194
|
+
# Auto-detect CPU count when parallel_workers is None (default behavior)
|
|
195
|
+
import multiprocessing as mp
|
|
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)
|
|
193
203
|
elif use_memory_cache:
|
|
194
204
|
scanned_files, stats = scan_project_memory_optimized(config, max_cache_mb)
|
|
195
205
|
else:
|
|
@@ -243,11 +253,8 @@ def _ensure_config(config: ScanConfig | None) -> ScanConfig:
|
|
|
243
253
|
|
|
244
254
|
|
|
245
255
|
def _scan_phase(config: ScanConfig, function_level_only: bool = False) -> tuple[list[ScannedFile], ScanStats]:
|
|
246
|
-
"""Phase 1: Scan project files
|
|
247
|
-
# Use
|
|
248
|
-
if getattr(config, 'use_ultra_fast_scanner', False):
|
|
249
|
-
return scan_project_ultra_fast(config)
|
|
250
|
-
# Use optimized scan_project with RAM preload
|
|
256
|
+
"""Phase 1: Scan project files."""
|
|
257
|
+
# Use standard scan_project (ultra_fast_scanner disabled - doesn't extract function names)
|
|
251
258
|
return scan_project(config, function_level_only=function_level_only)
|
|
252
259
|
|
|
253
260
|
|
|
@@ -63,7 +63,7 @@ def _should_exclude(path: Path, patterns: tuple[str, ...]) -> bool:
|
|
|
63
63
|
|
|
64
64
|
def _collect_files(config: ScanConfig) -> Iterator[Path]:
|
|
65
65
|
"""Yield all files matching the scan configuration."""
|
|
66
|
-
root = config.root.resolve()
|
|
66
|
+
root = Path(config.root).resolve() if isinstance(config.root, str) else config.root.resolve()
|
|
67
67
|
for ext in config.extensions:
|
|
68
68
|
for path in root.rglob(f"*{ext}"):
|
|
69
69
|
if _should_exclude(path, tuple(config.exclude_patterns)):
|
|
@@ -249,7 +249,6 @@ def _preload_files_to_ram(config: ScanConfig) -> list[tuple[Path, str]]:
|
|
|
249
249
|
Returns list of (path, content) tuples. Filters out files exceeding size limit.
|
|
250
250
|
"""
|
|
251
251
|
files_content: list[tuple[Path, str]] = []
|
|
252
|
-
total_size = 0
|
|
253
252
|
|
|
254
253
|
for filepath in _collect_files(config):
|
|
255
254
|
# Check size first to avoid loading huge files
|
|
@@ -263,7 +262,6 @@ def _preload_files_to_ram(config: ScanConfig) -> list[tuple[Path, str]]:
|
|
|
263
262
|
source = _read_file_optimized(filepath, config.max_file_size_kb)
|
|
264
263
|
if source is not None:
|
|
265
264
|
files_content.append((filepath, source))
|
|
266
|
-
total_size += len(source)
|
|
267
265
|
|
|
268
266
|
return files_content
|
|
269
267
|
|
|
@@ -83,15 +83,11 @@ def preload_to_ram(config: ScanConfig, show_progress: bool = True) -> dict[Path,
|
|
|
83
83
|
size_mb = total_size / 1e6
|
|
84
84
|
print(f"📁 Preloaded {len(sources)} files to RAM ({size_mb:.1f}MB) in {load_time:.2f}s")
|
|
85
85
|
|
|
86
|
-
# Early exit: if no files
|
|
86
|
+
# Early exit: if no files, return immediately
|
|
87
87
|
if len(sources) == 0:
|
|
88
88
|
print("🔍 No files to scan")
|
|
89
89
|
return {}
|
|
90
90
|
|
|
91
|
-
if total_size < 1000: # Less than 1KB total
|
|
92
|
-
print("🔍 Project too small for meaningful analysis")
|
|
93
|
-
return {}
|
|
94
|
-
|
|
95
91
|
return sources
|
|
96
92
|
|
|
97
93
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.18
|
|
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
|
|
@@ -83,7 +83,7 @@ Dynamic: license-file
|
|
|
83
83
|
[](https://pypi.org/project/redup/)
|
|
84
84
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
85
85
|
[](https://python.org)
|
|
86
|
-
[](https://pypi.org/project/redup/)
|
|
87
87
|
|
|
88
88
|
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
89
|
|
|
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
|
|
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
|