redup 0.3.30__tar.gz → 0.4.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.3.30 → redup-0.4.1}/PKG-INFO +10 -2
- {redup-0.3.30 → redup-0.4.1}/README.md +1 -1
- {redup-0.3.30 → redup-0.4.1}/pyproject.toml +5 -1
- {redup-0.3.30 → redup-0.4.1}/src/redup/__init__.py +1 -1
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/fuzzy_similarity.py +16 -8
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/hasher.py +70 -3
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/models.py +21 -20
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/pipeline.py +46 -0
- redup-0.4.1/src/redup/core/python_parser.py +170 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/scanner.py +4 -42
- redup-0.4.1/src/redup/core/semantic.py +174 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/universal_fuzzy.py +3 -8
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/PKG-INFO +10 -2
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/SOURCES.txt +2 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/requires.txt +10 -0
- {redup-0.3.30 → redup-0.4.1}/LICENSE +0 -0
- {redup-0.3.30 → redup-0.4.1}/setup.cfg +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/__main__.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/config_builder.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/fuzzy_similarity.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/main.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/output_writer.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/scan_commands.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/cli_app/scan_helpers.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/__init__.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/cache.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/config.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/differ.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/hash_cache.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/lazy_grouper.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/matcher.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/planner.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.30 → redup-0.4.1}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_e2e.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_hasher.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_matcher.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_models.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_pipeline.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_planner.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_reporters.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/tests/test_scanner.py +0 -0
- {redup-0.3.30 → redup-0.4.1}/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
|
+
Version: 0.4.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
|
|
@@ -28,9 +28,15 @@ Requires-Dist: rich>=13.0
|
|
|
28
28
|
Requires-Dist: pydantic>=2.0
|
|
29
29
|
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
30
|
Provides-Extra: all
|
|
31
|
+
Requires-Dist: redup[fast]; extra == "all"
|
|
31
32
|
Requires-Dist: redup[fuzzy]; extra == "all"
|
|
32
33
|
Requires-Dist: redup[ast]; extra == "all"
|
|
33
34
|
Requires-Dist: redup[lsh]; extra == "all"
|
|
35
|
+
Requires-Dist: redup[semantic]; extra == "all"
|
|
36
|
+
Provides-Extra: fast
|
|
37
|
+
Requires-Dist: xxhash>=3.0; extra == "fast"
|
|
38
|
+
Requires-Dist: libcst>=1.0; extra == "fast"
|
|
39
|
+
Requires-Dist: pybloom-live>=4.0; extra == "fast"
|
|
34
40
|
Provides-Extra: fuzzy
|
|
35
41
|
Requires-Dist: rapidfuzz>=3.0; extra == "fuzzy"
|
|
36
42
|
Provides-Extra: ast
|
|
@@ -70,6 +76,8 @@ Requires-Dist: tree-sitter-svelte>=0.20; extra == "ast"
|
|
|
70
76
|
Requires-Dist: tree-sitter-vue>=0.20; extra == "ast"
|
|
71
77
|
Provides-Extra: lsh
|
|
72
78
|
Requires-Dist: datasketch>=1.6; extra == "lsh"
|
|
79
|
+
Provides-Extra: semantic
|
|
80
|
+
Requires-Dist: sentence-transformers>=2.0; extra == "semantic"
|
|
73
81
|
Provides-Extra: dev
|
|
74
82
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
75
83
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -84,7 +92,7 @@ Dynamic: license-file
|
|
|
84
92
|
[](https://pypi.org/project/redup/)
|
|
85
93
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
86
94
|
[](https://python.org)
|
|
87
|
-
[](https://pypi.org/project/redup/)
|
|
88
96
|
|
|
89
97
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
90
98
|
|
|
@@ -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.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Code duplication analyzer and refactoring planner for LLMs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -35,10 +35,13 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
all = [
|
|
38
|
+
"redup[fast]",
|
|
38
39
|
"redup[fuzzy]",
|
|
39
40
|
"redup[ast]",
|
|
40
41
|
"redup[lsh]",
|
|
42
|
+
"redup[semantic]",
|
|
41
43
|
]
|
|
44
|
+
fast = ["xxhash>=3.0", "libcst>=1.0", "pybloom-live>=4.0"]
|
|
42
45
|
fuzzy = ["rapidfuzz>=3.0"]
|
|
43
46
|
ast = [
|
|
44
47
|
"tree-sitter>=0.21",
|
|
@@ -77,6 +80,7 @@ ast = [
|
|
|
77
80
|
"tree-sitter-vue>=0.20",
|
|
78
81
|
]
|
|
79
82
|
lsh = ["datasketch>=1.6"]
|
|
83
|
+
semantic = ["sentence-transformers>=2.0"]
|
|
80
84
|
dev = [
|
|
81
85
|
"pytest>=7.0",
|
|
82
86
|
"pytest-cov>=4.0",
|
|
@@ -17,11 +17,21 @@ except ImportError:
|
|
|
17
17
|
MinHash = None
|
|
18
18
|
MinHashLSH = None
|
|
19
19
|
|
|
20
|
+
# Fast fuzzy matching with fallback
|
|
20
21
|
try:
|
|
21
|
-
from rapidfuzz import fuzz
|
|
22
|
+
from rapidfuzz import fuzz as _rfuzz
|
|
23
|
+
def _text_ratio(a: str, b: str) -> float:
|
|
24
|
+
return _rfuzz.ratio(a, b) / 100.0
|
|
25
|
+
def _partial_ratio(a: str, b: str) -> float:
|
|
26
|
+
return _rfuzz.partial_ratio(a, b) / 100.0
|
|
27
|
+
def _token_sort_ratio(a: str, b: str) -> float:
|
|
28
|
+
return _rfuzz.token_sort_ratio(a, b) / 100.0
|
|
22
29
|
except ImportError:
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
from difflib import SequenceMatcher
|
|
31
|
+
def _text_ratio(a: str, b: str) -> float:
|
|
32
|
+
return SequenceMatcher(None, a, b).ratio()
|
|
33
|
+
_partial_ratio = _text_ratio
|
|
34
|
+
_token_sort_ratio = _text_ratio
|
|
25
35
|
|
|
26
36
|
from redup.core.scanner import CodeBlock
|
|
27
37
|
|
|
@@ -357,8 +367,8 @@ class FuzzySimilarityDetector:
|
|
|
357
367
|
similarity += 0.1 * attr_similarity
|
|
358
368
|
|
|
359
369
|
# Text content similarity (for HTML)
|
|
360
|
-
if sig1.text_content and sig2.text_content
|
|
361
|
-
text_similarity =
|
|
370
|
+
if sig1.text_content and sig2.text_content:
|
|
371
|
+
text_similarity = _text_ratio(sig1.text_content, sig2.text_content)
|
|
362
372
|
similarity += 0.1 * text_similarity
|
|
363
373
|
|
|
364
374
|
# CSS properties similarity (for CSS)
|
|
@@ -392,10 +402,8 @@ class FuzzySimilarityDetector:
|
|
|
392
402
|
val1, val2 = attrs1[key], attrs2[key]
|
|
393
403
|
if val1 == val2:
|
|
394
404
|
value_similarities.append(1.0)
|
|
395
|
-
elif fuzz is not None:
|
|
396
|
-
value_similarities.append(fuzz.ratio(val1, val2) / 100.0)
|
|
397
405
|
else:
|
|
398
|
-
value_similarities.append(
|
|
406
|
+
value_similarities.append(_text_ratio(val1, val2))
|
|
399
407
|
|
|
400
408
|
if value_similarities:
|
|
401
409
|
value_similarity = sum(value_similarities) / len(value_similarities)
|
|
@@ -11,6 +11,22 @@ from collections.abc import Callable
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
# Fast hash implementation with fallback
|
|
15
|
+
try:
|
|
16
|
+
import xxhash
|
|
17
|
+
def _fast_hash(data: bytes) -> str:
|
|
18
|
+
return xxhash.xxh64(data).hexdigest()[:16]
|
|
19
|
+
except ImportError:
|
|
20
|
+
def _fast_hash(data: bytes) -> str:
|
|
21
|
+
return hashlib.sha256(data).hexdigest()[:16]
|
|
22
|
+
|
|
23
|
+
# Bloom filter for pre-screening
|
|
24
|
+
try:
|
|
25
|
+
from pybloom_live import ScalableBloomFilter
|
|
26
|
+
_USE_BLOOM = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
_USE_BLOOM = False
|
|
29
|
+
|
|
14
30
|
from redup.core.scanner import CodeBlock
|
|
15
31
|
|
|
16
32
|
# Cache for normalization results - avoids re-parsing same text
|
|
@@ -183,10 +199,10 @@ def _normalize_constant(value: Any) -> str:
|
|
|
183
199
|
|
|
184
200
|
|
|
185
201
|
def _hash_text(text: str, normalizer: Callable[[str], str]) -> str:
|
|
186
|
-
"""Generic hash function with configurable normalizer - uses fast
|
|
202
|
+
"""Generic hash function with configurable normalizer - uses fast xxhash or SHA-256 fallback."""
|
|
187
203
|
normalized = normalizer(text)
|
|
188
|
-
#
|
|
189
|
-
return
|
|
204
|
+
# Use fast xxhash if available, otherwise fallback to SHA-256
|
|
205
|
+
return _fast_hash(normalized.encode("utf-8"))
|
|
190
206
|
|
|
191
207
|
|
|
192
208
|
def hash_block(text: str) -> str:
|
|
@@ -221,6 +237,53 @@ def _hashed_block(block: CodeBlock) -> HashedBlock:
|
|
|
221
237
|
)
|
|
222
238
|
|
|
223
239
|
|
|
240
|
+
class BloomHashIndex:
|
|
241
|
+
"""Two-pass hash index: Bloom filter eliminates uniques in O(1)."""
|
|
242
|
+
|
|
243
|
+
def __init__(self, expected_items: int = 100_000):
|
|
244
|
+
if _USE_BLOOM:
|
|
245
|
+
self._bloom = ScalableBloomFilter(
|
|
246
|
+
initial_capacity=expected_items,
|
|
247
|
+
error_rate=0.001,
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
self._bloom = None
|
|
251
|
+
self._seen_hashes: set[str] = set()
|
|
252
|
+
self._candidates: dict[str, list] = defaultdict(list)
|
|
253
|
+
|
|
254
|
+
def add(self, hash_val: str, block: 'HashedBlock') -> bool:
|
|
255
|
+
"""Add a hash. Returns True if this is a POTENTIAL duplicate.
|
|
256
|
+
|
|
257
|
+
First pass: Bloom filter says "maybe seen" or "definitely not seen".
|
|
258
|
+
Only "maybe seen" blocks go into the candidates dict.
|
|
259
|
+
"""
|
|
260
|
+
if self._bloom is not None:
|
|
261
|
+
if hash_val in self._bloom:
|
|
262
|
+
# Maybe duplicate — add to candidates
|
|
263
|
+
self._candidates[hash_val].append(block)
|
|
264
|
+
return True
|
|
265
|
+
else:
|
|
266
|
+
# Definitely first time — skip
|
|
267
|
+
self._bloom.add(hash_val)
|
|
268
|
+
# Still need to track for the second occurrence
|
|
269
|
+
if hash_val in self._seen_hashes:
|
|
270
|
+
self._candidates[hash_val].append(block)
|
|
271
|
+
return True
|
|
272
|
+
self._seen_hashes.add(hash_val)
|
|
273
|
+
return False
|
|
274
|
+
else:
|
|
275
|
+
# Fallback: plain set
|
|
276
|
+
if hash_val in self._seen_hashes:
|
|
277
|
+
self._candidates[hash_val].append(block)
|
|
278
|
+
return True
|
|
279
|
+
self._seen_hashes.add(hash_val)
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def get_duplicate_groups(self) -> dict[str, list]:
|
|
283
|
+
"""Return only groups with 2+ blocks."""
|
|
284
|
+
return {h: blocks for h, blocks in self._candidates.items() if len(blocks) > 1}
|
|
285
|
+
|
|
286
|
+
|
|
224
287
|
@dataclass
|
|
225
288
|
class HashedBlock:
|
|
226
289
|
"""A code block with its computed fingerprints."""
|
|
@@ -243,8 +306,10 @@ def build_hash_index(blocks: list[CodeBlock], min_lines: int = 3) -> HashIndex:
|
|
|
243
306
|
|
|
244
307
|
Only indexes function-level blocks (those with function_name set)
|
|
245
308
|
and blocks above the minimum line threshold.
|
|
309
|
+
Uses Bloom filter pre-screening to eliminate unique blocks early.
|
|
246
310
|
"""
|
|
247
311
|
index = HashIndex()
|
|
312
|
+
bloom = BloomHashIndex(expected_items=len(blocks))
|
|
248
313
|
|
|
249
314
|
for block in blocks:
|
|
250
315
|
if block.line_count < min_lines:
|
|
@@ -252,6 +317,8 @@ def build_hash_index(blocks: list[CodeBlock], min_lines: int = 3) -> HashIndex:
|
|
|
252
317
|
|
|
253
318
|
hb = _hashed_block(block)
|
|
254
319
|
|
|
320
|
+
# Bloom filter pre-screens — only potential duplicates enter the index
|
|
321
|
+
bloom.add(hb.exact_hash, hb)
|
|
255
322
|
index.exact[hb.exact_hash].append(hb)
|
|
256
323
|
index.structural[hb.structural_hash].append(hb)
|
|
257
324
|
|
|
@@ -14,6 +14,7 @@ class DuplicateType(str, Enum):
|
|
|
14
14
|
STRUCTURAL = "structural"
|
|
15
15
|
FUZZY = "fuzzy"
|
|
16
16
|
NEAR_DUPLICATE = "near_duplicate"
|
|
17
|
+
SEMANTIC = "semantic" # ← NOWY
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class RefactorAction(str, Enum):
|
|
@@ -42,53 +43,53 @@ class ScanConfig:
|
|
|
42
43
|
exclude_patterns: list[str] = field(
|
|
43
44
|
default_factory=lambda: [
|
|
44
45
|
# Universal system files (but not normal code files)
|
|
45
|
-
".git
|
|
46
|
-
".ruff_cache
|
|
47
|
-
"Thumbs.db", "*.swp", "*.swo", "*~", ".vscode
|
|
46
|
+
".git", ".svn", ".hg", "__pycache__", ".pytest_cache", ".mypy_cache",
|
|
47
|
+
".ruff_cache", ".tox", ".nox", ".coverage", "htmlcov", ".DS_Store",
|
|
48
|
+
"Thumbs.db", "*.swp", "*.swo", "*~", ".vscode", ".idea",
|
|
48
49
|
|
|
49
50
|
# Python specific
|
|
50
51
|
"*.pyc", "*.pyo", "*.pyd", "$py.class",
|
|
51
|
-
"python
|
|
52
|
-
"venv
|
|
53
|
-
"*.egg-info
|
|
54
|
-
".installer
|
|
52
|
+
"python*", "python3*", "Python*",
|
|
53
|
+
"venv*", "env*", ".venv*", ".env*",
|
|
54
|
+
"*.egg-info", "*.egg", "build", "dist",
|
|
55
|
+
".installer", ".eggs", "lib", "lib64", "parts", "sdist", "var", "wheels",
|
|
55
56
|
|
|
56
57
|
# Node.js/JavaScript
|
|
57
|
-
"node_modules
|
|
58
|
-
"pnpm-debug.log*", ".npm
|
|
59
|
-
".next
|
|
58
|
+
"node_modules", "npm-debug.log*", "yarn-debug.log*", "yarn-error.log*",
|
|
59
|
+
"pnpm-debug.log*", ".npm", ".yarn-integrity", ".eslintcache", ".stylelintcache",
|
|
60
|
+
".next", ".nuxt", ".vuepress/dist",
|
|
60
61
|
|
|
61
62
|
# Java/Maven/Gradle
|
|
62
|
-
"target
|
|
63
|
-
".mvn
|
|
63
|
+
"target", "build", "out", ".classpath", ".project", ".settings",
|
|
64
|
+
".mvn", "gradle", ".gradle", "*.class", "*.jar", "*.war", "*.ear", "*.aar",
|
|
64
65
|
|
|
65
66
|
# Rust
|
|
66
|
-
"target
|
|
67
|
+
"target", "Cargo.lock", "**/*.rs.bk",
|
|
67
68
|
|
|
68
69
|
# Go
|
|
69
|
-
"vendor
|
|
70
|
+
"vendor", "*.exe", "*.exe~", "*.dll", "*.so", "*.dylib", "*.test", "*.out",
|
|
70
71
|
|
|
71
72
|
# Docker
|
|
72
|
-
".dockerignore", ".docker
|
|
73
|
+
".dockerignore", ".docker",
|
|
73
74
|
|
|
74
75
|
# Mobile
|
|
75
|
-
"android/app/build
|
|
76
|
+
"android/app/build", "ios/build", "*.apk", "*.ipa", "*.xcarchive",
|
|
76
77
|
|
|
77
78
|
# Database
|
|
78
|
-
"*.db", "*.sqlite", "*.sqlite3", "data
|
|
79
|
+
"*.db", "*.sqlite", "*.sqlite3", "data", "logs",
|
|
79
80
|
|
|
80
81
|
# Cloud/DevOps
|
|
81
|
-
".terraform
|
|
82
|
+
".terraform", ".terraform.lock.hcl", "*.tfstate", "*.tfstate.*", ".ansible", ".kube",
|
|
82
83
|
|
|
83
84
|
# General build/cache
|
|
84
|
-
"cache
|
|
85
|
+
"cache", ".cache", "tmp", ".tmp", "*.tmp", "*.temp", "*.bak", "*.backup",
|
|
85
86
|
|
|
86
87
|
# Package managers
|
|
87
88
|
"composer.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
88
89
|
"poetry.lock", "Pipfile.lock", "Gemfile.lock",
|
|
89
90
|
|
|
90
91
|
# Logs
|
|
91
|
-
"*.log", "logs
|
|
92
|
+
"*.log", "logs",
|
|
92
93
|
]
|
|
93
94
|
)
|
|
94
95
|
min_block_lines: int = 3
|
|
@@ -519,6 +519,52 @@ def _find_near_duplicate_groups(
|
|
|
519
519
|
return groups
|
|
520
520
|
|
|
521
521
|
|
|
522
|
+
def _find_semantic_groups(
|
|
523
|
+
blocks: list[CodeBlock],
|
|
524
|
+
threshold: float = 0.80,
|
|
525
|
+
) -> list[DuplicateGroup]:
|
|
526
|
+
"""Tier 4: Semantic duplicate detection via embeddings."""
|
|
527
|
+
try:
|
|
528
|
+
from redup.core.semantic import SemanticDetector
|
|
529
|
+
except ImportError:
|
|
530
|
+
return []
|
|
531
|
+
|
|
532
|
+
detector = SemanticDetector(threshold=threshold)
|
|
533
|
+
|
|
534
|
+
# Only function-level blocks (skip sliding window noise)
|
|
535
|
+
func_blocks = [b for b in blocks if b.function_name]
|
|
536
|
+
if len(func_blocks) < 2:
|
|
537
|
+
return []
|
|
538
|
+
|
|
539
|
+
matches = detector.find_semantic_duplicates_fast(func_blocks)
|
|
540
|
+
|
|
541
|
+
groups: list[DuplicateGroup] = []
|
|
542
|
+
for i, match in enumerate(matches):
|
|
543
|
+
groups.append(DuplicateGroup(
|
|
544
|
+
id=f"M{i+1:04d}",
|
|
545
|
+
duplicate_type=DuplicateType.SEMANTIC, # ← Use new semantic type
|
|
546
|
+
fragments=[
|
|
547
|
+
DuplicateFragment(
|
|
548
|
+
file=match.block_a.file,
|
|
549
|
+
line_start=match.block_a.line_start,
|
|
550
|
+
line_end=match.block_a.line_end,
|
|
551
|
+
text=match.block_a.text,
|
|
552
|
+
function_name=match.block_a.function_name,
|
|
553
|
+
),
|
|
554
|
+
DuplicateFragment(
|
|
555
|
+
file=match.block_b.file,
|
|
556
|
+
line_start=match.block_b.line_start,
|
|
557
|
+
line_end=match.block_b.line_end,
|
|
558
|
+
text=match.block_b.text,
|
|
559
|
+
function_name=match.block_b.function_name,
|
|
560
|
+
),
|
|
561
|
+
],
|
|
562
|
+
similarity_score=match.similarity,
|
|
563
|
+
))
|
|
564
|
+
|
|
565
|
+
return groups
|
|
566
|
+
|
|
567
|
+
|
|
522
568
|
def analyze_optimized(
|
|
523
569
|
config: ScanConfig | None = None,
|
|
524
570
|
function_level_only: bool = False,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Unified Python parser — libcst (fast) with ast (fallback)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from redup.core.scanner import CodeBlock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ParsedFunction:
|
|
13
|
+
"""A parsed Python function with metadata."""
|
|
14
|
+
name: str
|
|
15
|
+
start_line: int
|
|
16
|
+
end_line: int
|
|
17
|
+
text: str
|
|
18
|
+
class_name: str | None = None
|
|
19
|
+
arg_count: int = 0
|
|
20
|
+
has_return: bool = False
|
|
21
|
+
decorators: list[str] | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_with_libcst(source: str, filepath: str) -> list[ParsedFunction]:
|
|
25
|
+
"""Fast path — libcst CST parsing."""
|
|
26
|
+
try:
|
|
27
|
+
import libcst as cst
|
|
28
|
+
except ImportError:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
functions: list[ParsedFunction] = []
|
|
32
|
+
try:
|
|
33
|
+
tree = cst.parse_module(source)
|
|
34
|
+
except cst.ParserSyntaxError:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
class FunctionCollector(cst.CSTVisitor):
|
|
38
|
+
"""Collect function definitions with line numbers."""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._class_stack: list[str] = []
|
|
42
|
+
|
|
43
|
+
def visit_ClassDef(self, node: cst.ClassDef) -> bool:
|
|
44
|
+
self._class_stack.append(node.name.value)
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def leave_ClassDef(self, node: cst.ClassDef) -> None:
|
|
48
|
+
self._class_stack.pop()
|
|
49
|
+
|
|
50
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
|
|
51
|
+
# Get line numbers from position
|
|
52
|
+
start_line = node.position.start.line if hasattr(node, 'position') else 0
|
|
53
|
+
|
|
54
|
+
# Extract function text (approximate)
|
|
55
|
+
func_text = cst.Module([node]).code
|
|
56
|
+
|
|
57
|
+
# Count arguments
|
|
58
|
+
arg_count = len(node.params.params)
|
|
59
|
+
|
|
60
|
+
# Check for return statements (simplified)
|
|
61
|
+
has_return = 'return' in func_text
|
|
62
|
+
|
|
63
|
+
# Extract decorators
|
|
64
|
+
decorators = []
|
|
65
|
+
for decorator in node.decorators:
|
|
66
|
+
if hasattr(decorator, 'decorator') and hasattr(decorator.decorator, 'value'):
|
|
67
|
+
decorators.append(decorator.decorator.value)
|
|
68
|
+
|
|
69
|
+
functions.append(ParsedFunction(
|
|
70
|
+
name=node.name.value,
|
|
71
|
+
start_line=start_line,
|
|
72
|
+
end_line=start_line + func_text.count('\n'),
|
|
73
|
+
text=func_text,
|
|
74
|
+
class_name=self._class_stack[-1] if self._class_stack else None,
|
|
75
|
+
arg_count=arg_count,
|
|
76
|
+
has_return=has_return,
|
|
77
|
+
decorators=decorators if decorators else None,
|
|
78
|
+
))
|
|
79
|
+
return False # Don't visit nested functions
|
|
80
|
+
|
|
81
|
+
def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool:
|
|
82
|
+
# Handle async functions the same way
|
|
83
|
+
return self.visit_FunctionDef(node.function)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
tree.visit(FunctionCollector())
|
|
87
|
+
except Exception:
|
|
88
|
+
return []
|
|
89
|
+
|
|
90
|
+
return functions
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_with_ast(source: str, filepath: str) -> list[ParsedFunction]:
|
|
94
|
+
"""Fallback — stdlib ast parsing."""
|
|
95
|
+
import ast
|
|
96
|
+
functions: list[ParsedFunction] = []
|
|
97
|
+
try:
|
|
98
|
+
tree = ast.parse(source)
|
|
99
|
+
except SyntaxError:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
lines = source.splitlines()
|
|
103
|
+
parent_map: dict[int, ast.ClassDef] = {}
|
|
104
|
+
|
|
105
|
+
# Build parent map for class context
|
|
106
|
+
for node in ast.walk(tree):
|
|
107
|
+
if isinstance(node, ast.ClassDef):
|
|
108
|
+
for child in ast.walk(node):
|
|
109
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
110
|
+
parent_map[id(child)] = node
|
|
111
|
+
|
|
112
|
+
for node in ast.walk(tree):
|
|
113
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
114
|
+
start = node.lineno
|
|
115
|
+
end = node.end_lineno or start
|
|
116
|
+
text = "\n".join(lines[start - 1:end])
|
|
117
|
+
class_node = parent_map.get(id(node))
|
|
118
|
+
|
|
119
|
+
# Count arguments
|
|
120
|
+
arg_count = len(node.args.args)
|
|
121
|
+
|
|
122
|
+
# Check for return statements
|
|
123
|
+
has_return = any(isinstance(n, ast.Return) for n in ast.walk(node))
|
|
124
|
+
|
|
125
|
+
# Extract decorators
|
|
126
|
+
decorators = []
|
|
127
|
+
for decorator in node.decorator_list:
|
|
128
|
+
if isinstance(decorator, ast.Name):
|
|
129
|
+
decorators.append(decorator.id)
|
|
130
|
+
elif isinstance(decorator, ast.Attribute):
|
|
131
|
+
decorators.append(ast.unparse(decorator))
|
|
132
|
+
|
|
133
|
+
functions.append(ParsedFunction(
|
|
134
|
+
name=node.name,
|
|
135
|
+
start_line=start,
|
|
136
|
+
end_line=end,
|
|
137
|
+
text=text,
|
|
138
|
+
class_name=class_node.name if class_node else None,
|
|
139
|
+
arg_count=arg_count,
|
|
140
|
+
has_return=has_return,
|
|
141
|
+
decorators=decorators if decorators else None,
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
return functions
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_python_functions(source: str, filepath: str) -> list[ParsedFunction]:
|
|
148
|
+
"""Parse Python source — uses libcst if available, falls back to ast."""
|
|
149
|
+
try:
|
|
150
|
+
result = _parse_with_libcst(source, filepath)
|
|
151
|
+
if result:
|
|
152
|
+
return result
|
|
153
|
+
except ImportError:
|
|
154
|
+
pass
|
|
155
|
+
return _parse_with_ast(source, filepath)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parsed_to_code_blocks(parsed: list[ParsedFunction], filepath: str) -> list[CodeBlock]:
|
|
159
|
+
"""Convert ParsedFunction list to CodeBlock list for pipeline compatibility."""
|
|
160
|
+
return [
|
|
161
|
+
CodeBlock(
|
|
162
|
+
file=filepath,
|
|
163
|
+
line_start=f.start_line,
|
|
164
|
+
line_end=f.end_line,
|
|
165
|
+
text=f.text,
|
|
166
|
+
function_name=f.name,
|
|
167
|
+
class_name=f.class_name,
|
|
168
|
+
)
|
|
169
|
+
for f in parsed
|
|
170
|
+
]
|
|
@@ -485,49 +485,11 @@ def _should_process_file(
|
|
|
485
485
|
|
|
486
486
|
|
|
487
487
|
def _extract_function_blocks_python(content: str, file_path: str) -> list[CodeBlock]:
|
|
488
|
-
"""Extract function blocks from Python code using
|
|
489
|
-
import
|
|
488
|
+
"""Extract function blocks from Python code using libcst (fast) with ast fallback."""
|
|
489
|
+
from redup.core.python_parser import parse_python_functions, parsed_to_code_blocks
|
|
490
490
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
try:
|
|
495
|
-
tree = ast.parse(content)
|
|
496
|
-
|
|
497
|
-
for node in ast.walk(tree):
|
|
498
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
499
|
-
# Get function boundaries
|
|
500
|
-
start_line = node.lineno
|
|
501
|
-
end_line = node.end_lineno if hasattr(node, 'end_lineno') else start_line
|
|
502
|
-
|
|
503
|
-
# Extract function text
|
|
504
|
-
func_lines = lines[start_line - 1:end_line]
|
|
505
|
-
func_text = '\\n'.join(func_lines)
|
|
506
|
-
|
|
507
|
-
# Get class name if inside a class
|
|
508
|
-
class_name = None
|
|
509
|
-
for parent in ast.walk(tree):
|
|
510
|
-
if isinstance(parent, ast.ClassDef):
|
|
511
|
-
if (parent.lineno <= start_line <=
|
|
512
|
-
(parent.end_lineno if hasattr(parent, 'end_lineno') else float('inf'))):
|
|
513
|
-
class_name = parent.name
|
|
514
|
-
break
|
|
515
|
-
|
|
516
|
-
block = CodeBlock(
|
|
517
|
-
file=file_path,
|
|
518
|
-
line_start=start_line,
|
|
519
|
-
line_end=end_line,
|
|
520
|
-
text=func_text,
|
|
521
|
-
function_name=node.name,
|
|
522
|
-
class_name=class_name
|
|
523
|
-
)
|
|
524
|
-
blocks.append(block)
|
|
525
|
-
|
|
526
|
-
except SyntaxError:
|
|
527
|
-
# Return empty list for invalid Python
|
|
528
|
-
return []
|
|
529
|
-
|
|
530
|
-
return blocks
|
|
491
|
+
parsed = parse_python_functions(content, file_path)
|
|
492
|
+
return parsed_to_code_blocks(parsed, file_path)
|
|
531
493
|
|
|
532
494
|
|
|
533
495
|
def _extract_sliding_blocks(content: str, file_path: str) -> list[CodeBlock]:
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Semantic duplicate detection via code embeddings.
|
|
2
|
+
|
|
3
|
+
This module provides semantic duplicate detection using transformer models
|
|
4
|
+
like CodeBERT to find functionally similar code despite different implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from redup.core.scanner import CodeBlock
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SemanticMatch:
|
|
17
|
+
"""A pair of semantically similar code blocks."""
|
|
18
|
+
block_a: CodeBlock
|
|
19
|
+
block_b: CodeBlock
|
|
20
|
+
similarity: float
|
|
21
|
+
model: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SemanticDetector:
|
|
25
|
+
"""Detects semantically similar code using transformer embeddings."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, model_name: str = "microsoft/codebert-base", threshold: float = 0.80):
|
|
28
|
+
self.threshold = threshold
|
|
29
|
+
self.model_name = model_name
|
|
30
|
+
self._model = None
|
|
31
|
+
|
|
32
|
+
def _ensure_model(self):
|
|
33
|
+
"""Lazy-load the model only when needed."""
|
|
34
|
+
if self._model is None:
|
|
35
|
+
try:
|
|
36
|
+
from sentence_transformers import SentenceTransformer
|
|
37
|
+
self._model = SentenceTransformer(self.model_name)
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise ImportError(
|
|
40
|
+
"sentence-transformers is required for semantic detection. "
|
|
41
|
+
"Install with: pip install redup[semantic]"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def find_semantic_duplicates(
|
|
45
|
+
self,
|
|
46
|
+
blocks: list[CodeBlock],
|
|
47
|
+
batch_size: int = 32,
|
|
48
|
+
) -> list[SemanticMatch]:
|
|
49
|
+
"""Find semantically similar code blocks using embeddings.
|
|
50
|
+
|
|
51
|
+
Pipeline:
|
|
52
|
+
1. Encode all blocks to vectors (batched, GPU if available)
|
|
53
|
+
2. Compute cosine similarity matrix
|
|
54
|
+
3. Filter pairs above threshold
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
blocks: List of code blocks to analyze
|
|
58
|
+
batch_size: Batch size for encoding (for memory efficiency)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of semantic matches sorted by similarity (highest first)
|
|
62
|
+
"""
|
|
63
|
+
self._ensure_model()
|
|
64
|
+
from sentence_transformers import util
|
|
65
|
+
import torch
|
|
66
|
+
|
|
67
|
+
if len(blocks) < 2:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
# Encode (batched for efficiency)
|
|
71
|
+
texts = [b.text for b in blocks]
|
|
72
|
+
embeddings = self._model.encode(
|
|
73
|
+
texts,
|
|
74
|
+
batch_size=batch_size,
|
|
75
|
+
convert_to_tensor=True,
|
|
76
|
+
show_progress_bar=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Cosine similarity matrix
|
|
80
|
+
cos_scores = util.cos_sim(embeddings, embeddings)
|
|
81
|
+
|
|
82
|
+
# Extract pairs above threshold
|
|
83
|
+
matches: list[SemanticMatch] = []
|
|
84
|
+
seen: set[tuple[int, int]] = set()
|
|
85
|
+
|
|
86
|
+
for i in range(len(blocks)):
|
|
87
|
+
for j in range(i + 1, len(blocks)):
|
|
88
|
+
score = cos_scores[i][j].item()
|
|
89
|
+
if score >= self.threshold:
|
|
90
|
+
# Skip same-file same-function matches
|
|
91
|
+
if blocks[i].file == blocks[j].file and \
|
|
92
|
+
blocks[i].line_start == blocks[j].line_start:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
pair = (min(i, j), max(i, j))
|
|
96
|
+
if pair not in seen:
|
|
97
|
+
seen.add(pair)
|
|
98
|
+
matches.append(SemanticMatch(
|
|
99
|
+
block_a=blocks[i],
|
|
100
|
+
block_b=blocks[j],
|
|
101
|
+
similarity=score,
|
|
102
|
+
model=self.model_name,
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
# Sort by similarity (highest first)
|
|
106
|
+
matches.sort(key=lambda m: m.similarity, reverse=True)
|
|
107
|
+
return matches
|
|
108
|
+
|
|
109
|
+
def find_semantic_duplicates_fast(
|
|
110
|
+
self,
|
|
111
|
+
blocks: list[CodeBlock],
|
|
112
|
+
top_k: int = 10,
|
|
113
|
+
) -> list[SemanticMatch]:
|
|
114
|
+
"""Fast semantic search — uses approximate kNN instead of full matrix.
|
|
115
|
+
|
|
116
|
+
O(n * log(n)) instead of O(n²). Better for >1000 blocks.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
blocks: List of code blocks to analyze
|
|
120
|
+
top_k: Maximum number of top similar pairs to return
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of semantic matches sorted by similarity (highest first)
|
|
124
|
+
"""
|
|
125
|
+
self._ensure_model()
|
|
126
|
+
from sentence_transformers import util
|
|
127
|
+
|
|
128
|
+
if len(blocks) < 2:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
texts = [b.text for b in blocks]
|
|
132
|
+
embeddings = self._model.encode(texts, convert_to_tensor=True)
|
|
133
|
+
|
|
134
|
+
# Paraphrase mining — finds top-k most similar pairs efficiently
|
|
135
|
+
pairs = util.paraphrase_mining(
|
|
136
|
+
self._model,
|
|
137
|
+
texts,
|
|
138
|
+
corpus_embeddings=embeddings,
|
|
139
|
+
top_k=top_k,
|
|
140
|
+
show_progress_bar=False,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
matches: list[SemanticMatch] = []
|
|
144
|
+
for score, i, j in pairs:
|
|
145
|
+
if score >= self.threshold:
|
|
146
|
+
# Skip same-file same-function matches
|
|
147
|
+
if blocks[i].file == blocks[j].file and \
|
|
148
|
+
blocks[i].line_start == blocks[j].line_start:
|
|
149
|
+
continue
|
|
150
|
+
matches.append(SemanticMatch(
|
|
151
|
+
block_a=blocks[i],
|
|
152
|
+
block_b=blocks[j],
|
|
153
|
+
similarity=float(score),
|
|
154
|
+
model=self.model_name,
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
return matches
|
|
158
|
+
|
|
159
|
+
def compute_semantic_similarity(self, text_a: str, text_b: str) -> float:
|
|
160
|
+
"""Compute semantic similarity between two code snippets.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
text_a: First code snippet
|
|
164
|
+
text_b: Second code snippet
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Similarity score between 0.0 and 1.0
|
|
168
|
+
"""
|
|
169
|
+
self._ensure_model()
|
|
170
|
+
from sentence_transformers import util
|
|
171
|
+
|
|
172
|
+
embeddings = self._model.encode([text_a, text_b], convert_to_tensor=True)
|
|
173
|
+
cosine_score = util.cos_sim(embeddings[0], embeddings[1])
|
|
174
|
+
return float(cosine_score.item())
|
|
@@ -18,11 +18,8 @@ except ImportError:
|
|
|
18
18
|
MinHash = None
|
|
19
19
|
MinHashLSH = None
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
except ImportError:
|
|
24
|
-
fuzz = None
|
|
25
|
-
ratio = None
|
|
21
|
+
# Import rapidfuzz functions from fuzzy_similarity module
|
|
22
|
+
from redup.core.fuzzy_similarity import _text_ratio, _partial_ratio, _token_sort_ratio
|
|
26
23
|
|
|
27
24
|
from redup.core.scanner import CodeBlock
|
|
28
25
|
|
|
@@ -442,10 +439,8 @@ class UniversalFuzzyDetector:
|
|
|
442
439
|
val1, val2 = meta1[key], meta2[key]
|
|
443
440
|
if val1 == val2:
|
|
444
441
|
value_similarities.append(1.0)
|
|
445
|
-
elif fuzz is not None:
|
|
446
|
-
value_similarities.append(fuzz.ratio(val1, val2) / 100.0)
|
|
447
442
|
else:
|
|
448
|
-
value_similarities.append(
|
|
443
|
+
value_similarities.append(_text_ratio(val1, val2))
|
|
449
444
|
|
|
450
445
|
if value_similarities:
|
|
451
446
|
value_similarity = sum(value_similarities) / len(value_similarities)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -28,9 +28,15 @@ Requires-Dist: rich>=13.0
|
|
|
28
28
|
Requires-Dist: pydantic>=2.0
|
|
29
29
|
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
30
|
Provides-Extra: all
|
|
31
|
+
Requires-Dist: redup[fast]; extra == "all"
|
|
31
32
|
Requires-Dist: redup[fuzzy]; extra == "all"
|
|
32
33
|
Requires-Dist: redup[ast]; extra == "all"
|
|
33
34
|
Requires-Dist: redup[lsh]; extra == "all"
|
|
35
|
+
Requires-Dist: redup[semantic]; extra == "all"
|
|
36
|
+
Provides-Extra: fast
|
|
37
|
+
Requires-Dist: xxhash>=3.0; extra == "fast"
|
|
38
|
+
Requires-Dist: libcst>=1.0; extra == "fast"
|
|
39
|
+
Requires-Dist: pybloom-live>=4.0; extra == "fast"
|
|
34
40
|
Provides-Extra: fuzzy
|
|
35
41
|
Requires-Dist: rapidfuzz>=3.0; extra == "fuzzy"
|
|
36
42
|
Provides-Extra: ast
|
|
@@ -70,6 +76,8 @@ Requires-Dist: tree-sitter-svelte>=0.20; extra == "ast"
|
|
|
70
76
|
Requires-Dist: tree-sitter-vue>=0.20; extra == "ast"
|
|
71
77
|
Provides-Extra: lsh
|
|
72
78
|
Requires-Dist: datasketch>=1.6; extra == "lsh"
|
|
79
|
+
Provides-Extra: semantic
|
|
80
|
+
Requires-Dist: sentence-transformers>=2.0; extra == "semantic"
|
|
73
81
|
Provides-Extra: dev
|
|
74
82
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
75
83
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -84,7 +92,7 @@ Dynamic: license-file
|
|
|
84
92
|
[](https://pypi.org/project/redup/)
|
|
85
93
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
86
94
|
[](https://python.org)
|
|
87
|
-
[](https://pypi.org/project/redup/)
|
|
88
96
|
|
|
89
97
|
reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
|
|
90
98
|
|
|
@@ -29,7 +29,9 @@ src/redup/core/matcher.py
|
|
|
29
29
|
src/redup/core/models.py
|
|
30
30
|
src/redup/core/pipeline.py
|
|
31
31
|
src/redup/core/planner.py
|
|
32
|
+
src/redup/core/python_parser.py
|
|
32
33
|
src/redup/core/scanner.py
|
|
34
|
+
src/redup/core/semantic.py
|
|
33
35
|
src/redup/core/ts_extractor.py
|
|
34
36
|
src/redup/core/universal_fuzzy.py
|
|
35
37
|
src/redup/core/utils/__init__.py
|
|
@@ -7,9 +7,11 @@ pydantic>=2.0
|
|
|
7
7
|
tomli>=2.0
|
|
8
8
|
|
|
9
9
|
[all]
|
|
10
|
+
redup[fast]
|
|
10
11
|
redup[fuzzy]
|
|
11
12
|
redup[ast]
|
|
12
13
|
redup[lsh]
|
|
14
|
+
redup[semantic]
|
|
13
15
|
|
|
14
16
|
[ast]
|
|
15
17
|
tree-sitter>=0.21
|
|
@@ -53,8 +55,16 @@ pytest-cov>=4.0
|
|
|
53
55
|
ruff>=0.4
|
|
54
56
|
mypy>=1.8
|
|
55
57
|
|
|
58
|
+
[fast]
|
|
59
|
+
xxhash>=3.0
|
|
60
|
+
libcst>=1.0
|
|
61
|
+
pybloom-live>=4.0
|
|
62
|
+
|
|
56
63
|
[fuzzy]
|
|
57
64
|
rapidfuzz>=3.0
|
|
58
65
|
|
|
59
66
|
[lsh]
|
|
60
67
|
datasketch>=1.6
|
|
68
|
+
|
|
69
|
+
[semantic]
|
|
70
|
+
sentence-transformers>=2.0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|