redup 0.1.1__tar.gz → 0.1.2__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.1.1 → redup-0.1.2}/PKG-INFO +1 -1
- {redup-0.1.1 → redup-0.1.2}/pyproject.toml +2 -1
- {redup-0.1.1 → redup-0.1.2}/src/redup/__init__.py +2 -2
- {redup-0.1.1 → redup-0.1.2}/src/redup/cli_app/main.py +12 -8
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/hasher.py +19 -14
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/models.py +6 -7
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/pipeline.py +2 -4
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/planner.py +1 -2
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/scanner.py +1 -1
- {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/yaml_reporter.py +1 -1
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/PKG-INFO +1 -1
- {redup-0.1.1 → redup-0.1.2}/LICENSE +0 -0
- {redup-0.1.1 → redup-0.1.2}/README.md +0 -0
- {redup-0.1.1 → redup-0.1.2}/setup.cfg +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/__main__.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/__init__.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/core/matcher.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/__init__.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/SOURCES.txt +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/requires.txt +0 -0
- {redup-0.1.1 → redup-0.1.2}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_e2e.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_hasher.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_matcher.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_models.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_pipeline.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_planner.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_reporters.py +0 -0
- {redup-0.1.1 → redup-0.1.2}/tests/test_scanner.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "redup"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Code duplication analyzer and refactoring planner for LLMs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -66,6 +66,7 @@ line-length = 100
|
|
|
66
66
|
|
|
67
67
|
[tool.ruff.lint]
|
|
68
68
|
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
69
|
+
ignore = ["B008"]
|
|
69
70
|
|
|
70
71
|
[tool.mypy]
|
|
71
72
|
python_version = "3.10"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.1.
|
|
5
|
+
__version__ = "0.1.2"
|
|
6
6
|
|
|
7
7
|
from redup.core.models import (
|
|
8
8
|
DuplicateFragment,
|
|
@@ -11,8 +11,8 @@ from redup.core.models import (
|
|
|
11
11
|
RefactorSuggestion,
|
|
12
12
|
ScanConfig,
|
|
13
13
|
)
|
|
14
|
-
from redup.core.scanner import scan_project
|
|
15
14
|
from redup.core.pipeline import analyze
|
|
15
|
+
from redup.core.scanner import scan_project
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"analyze",
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
|
|
@@ -29,6 +28,11 @@ class OutputFormat(str, Enum):
|
|
|
29
28
|
all = "all"
|
|
30
29
|
|
|
31
30
|
|
|
31
|
+
DEFAULT_PATH = Path(".")
|
|
32
|
+
DEFAULT_FORMAT = OutputFormat.toon
|
|
33
|
+
DEFAULT_OUTPUT: Path | None = None
|
|
34
|
+
|
|
35
|
+
|
|
32
36
|
def _write_output(content: str, output: Path | None, suffix: str) -> None:
|
|
33
37
|
"""Write content to file or stdout."""
|
|
34
38
|
if output:
|
|
@@ -43,19 +47,19 @@ def _write_output(content: str, output: Path | None, suffix: str) -> None:
|
|
|
43
47
|
@app.command()
|
|
44
48
|
def scan(
|
|
45
49
|
path: Path = typer.Argument(
|
|
46
|
-
|
|
50
|
+
DEFAULT_PATH,
|
|
47
51
|
help="Project root directory to scan.",
|
|
48
52
|
exists=True,
|
|
49
53
|
dir_okay=True,
|
|
50
54
|
file_okay=False,
|
|
51
55
|
),
|
|
52
56
|
format: OutputFormat = typer.Option(
|
|
53
|
-
|
|
57
|
+
DEFAULT_FORMAT,
|
|
54
58
|
"--format", "-f",
|
|
55
59
|
help="Output format.",
|
|
56
60
|
),
|
|
57
|
-
output:
|
|
58
|
-
|
|
61
|
+
output: Path | None = typer.Option(
|
|
62
|
+
DEFAULT_OUTPUT,
|
|
59
63
|
"--output", "-o",
|
|
60
64
|
help="Output directory or file path. Defaults to stdout.",
|
|
61
65
|
),
|
|
@@ -118,7 +122,7 @@ def scan(
|
|
|
118
122
|
output_dir = path / "redup_output"
|
|
119
123
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
120
124
|
|
|
121
|
-
for
|
|
125
|
+
for _fmt, renderer, suffix in [
|
|
122
126
|
("JSON", to_json, "json"),
|
|
123
127
|
("YAML", to_yaml, "yaml"),
|
|
124
128
|
("TOON", to_toon, "toon"),
|
|
@@ -139,8 +143,8 @@ def scan(
|
|
|
139
143
|
def info() -> None:
|
|
140
144
|
"""Show reDUP version and configuration info."""
|
|
141
145
|
typer.echo(f"reDUP v{redup.__version__}")
|
|
142
|
-
typer.echo(
|
|
143
|
-
typer.echo(
|
|
146
|
+
typer.echo(" Python package: redup")
|
|
147
|
+
typer.echo(" Repo: https://github.com/semcod/redup")
|
|
144
148
|
typer.echo("")
|
|
145
149
|
typer.echo("Optional dependencies:")
|
|
146
150
|
|
|
@@ -23,7 +23,7 @@ def _normalize_text(text: str) -> str:
|
|
|
23
23
|
continue
|
|
24
24
|
if stripped.startswith('"""') or stripped.startswith("'''"):
|
|
25
25
|
continue
|
|
26
|
-
stripped = re.sub(r
|
|
26
|
+
stripped = re.sub(r'#.*$', "", stripped)
|
|
27
27
|
stripped = stripped.strip()
|
|
28
28
|
if stripped:
|
|
29
29
|
lines.append(stripped)
|
|
@@ -49,7 +49,7 @@ def _normalize_ast_text(text: str) -> str:
|
|
|
49
49
|
normalized = _normalize_text(text)
|
|
50
50
|
normalized = re.sub(r'"[^"]*"', '"__STR__"', normalized)
|
|
51
51
|
normalized = re.sub(r"'[^']*'", "'__STR__'", normalized)
|
|
52
|
-
normalized = re.sub(r
|
|
52
|
+
normalized = re.sub(r'\b\d+\.?\d*\b', "__NUM__", normalized)
|
|
53
53
|
return normalized
|
|
54
54
|
|
|
55
55
|
|
|
@@ -122,16 +122,20 @@ def _ast_to_normalized_string(tree: object) -> str:
|
|
|
122
122
|
return " ".join(parts)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def _hash_text(text: str, normalizer: callable) -> str:
|
|
126
|
+
"""Generic SHA-256 hash function with configurable normalizer."""
|
|
127
|
+
normalized = normalizer(text)
|
|
128
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
|
|
129
|
+
|
|
130
|
+
|
|
125
131
|
def hash_block(text: str) -> str:
|
|
126
132
|
"""SHA-256 hash of normalized text."""
|
|
127
|
-
|
|
128
|
-
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
|
|
133
|
+
return _hash_text(text, _normalize_text)
|
|
129
134
|
|
|
130
135
|
|
|
131
136
|
def hash_block_structural(text: str) -> str:
|
|
132
137
|
"""SHA-256 hash of deeply normalized text (variable names replaced)."""
|
|
133
|
-
|
|
134
|
-
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
|
|
138
|
+
return _hash_text(text, _normalize_ast_text)
|
|
135
139
|
|
|
136
140
|
|
|
137
141
|
@dataclass
|
|
@@ -175,22 +179,23 @@ def build_hash_index(blocks: list[CodeBlock], min_lines: int = 3) -> HashIndex:
|
|
|
175
179
|
return index
|
|
176
180
|
|
|
177
181
|
|
|
178
|
-
def
|
|
179
|
-
"""
|
|
182
|
+
def _find_duplicates(hash_dict: dict[str, list[HashedBlock]]) -> dict[str, list[HashedBlock]]:
|
|
183
|
+
"""Generic duplicate finder for any hash dictionary."""
|
|
180
184
|
return {
|
|
181
185
|
h: blocks
|
|
182
|
-
for h, blocks in
|
|
186
|
+
for h, blocks in hash_dict.items()
|
|
183
187
|
if len(blocks) > 1 and _blocks_from_different_locations(blocks)
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
|
|
191
|
+
def find_exact_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
|
|
192
|
+
"""Find groups of blocks with identical normalized text."""
|
|
193
|
+
return _find_duplicates(index.exact)
|
|
194
|
+
|
|
195
|
+
|
|
187
196
|
def find_structural_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
|
|
188
197
|
"""Find groups of blocks with identical structure (names may differ)."""
|
|
189
|
-
return
|
|
190
|
-
h: blocks
|
|
191
|
-
for h, blocks in index.structural.items()
|
|
192
|
-
if len(blocks) > 1 and _blocks_from_different_locations(blocks)
|
|
193
|
-
}
|
|
198
|
+
return _find_duplicates(index.structural)
|
|
194
199
|
|
|
195
200
|
|
|
196
201
|
def _blocks_from_different_locations(blocks: list[HashedBlock]) -> bool:
|
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class DuplicateType(str, Enum):
|
|
@@ -59,8 +58,8 @@ class DuplicateFragment:
|
|
|
59
58
|
line_start: int
|
|
60
59
|
line_end: int
|
|
61
60
|
text: str = ""
|
|
62
|
-
function_name:
|
|
63
|
-
class_name:
|
|
61
|
+
function_name: str | None = None
|
|
62
|
+
class_name: str | None = None
|
|
64
63
|
|
|
65
64
|
@property
|
|
66
65
|
def line_count(self) -> int:
|
|
@@ -76,7 +75,7 @@ class DuplicateGroup:
|
|
|
76
75
|
fragments: list[DuplicateFragment] = field(default_factory=list)
|
|
77
76
|
similarity_score: float = 1.0
|
|
78
77
|
normalized_hash: str = ""
|
|
79
|
-
normalized_name:
|
|
78
|
+
normalized_name: str | None = None
|
|
80
79
|
|
|
81
80
|
@property
|
|
82
81
|
def occurrences(self) -> int:
|
|
@@ -107,8 +106,8 @@ class RefactorSuggestion:
|
|
|
107
106
|
group_id: str
|
|
108
107
|
action: RefactorAction
|
|
109
108
|
new_module: str
|
|
110
|
-
function_name:
|
|
111
|
-
class_name:
|
|
109
|
+
function_name: str | None = None
|
|
110
|
+
class_name: str | None = None
|
|
112
111
|
original_files: list[str] = field(default_factory=list)
|
|
113
112
|
risk_level: RiskLevel = RiskLevel.LOW
|
|
114
113
|
priority: int = 0
|
|
@@ -131,7 +130,7 @@ class DuplicationMap:
|
|
|
131
130
|
"""Complete result of a reDUP analysis run."""
|
|
132
131
|
|
|
133
132
|
project_path: str = ""
|
|
134
|
-
config:
|
|
133
|
+
config: ScanConfig | None = None
|
|
135
134
|
groups: list[DuplicateGroup] = field(default_factory=list)
|
|
136
135
|
suggestions: list[RefactorSuggestion] = field(default_factory=list)
|
|
137
136
|
stats: ScanStats = field(default_factory=ScanStats)
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections import defaultdict
|
|
6
|
-
|
|
7
5
|
from redup.core.hasher import (
|
|
8
6
|
HashedBlock,
|
|
9
7
|
build_hash_index,
|
|
10
8
|
find_exact_duplicates,
|
|
11
9
|
find_structural_duplicates,
|
|
12
10
|
)
|
|
13
|
-
from redup.core.matcher import
|
|
11
|
+
from redup.core.matcher import refine_structural_matches
|
|
14
12
|
from redup.core.models import (
|
|
15
13
|
DuplicateFragment,
|
|
16
14
|
DuplicateGroup,
|
|
@@ -19,7 +17,7 @@ from redup.core.models import (
|
|
|
19
17
|
ScanConfig,
|
|
20
18
|
)
|
|
21
19
|
from redup.core.planner import generate_suggestions
|
|
22
|
-
from redup.core.scanner import CodeBlock,
|
|
20
|
+
from redup.core.scanner import CodeBlock, scan_project
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
def _blocks_to_group(
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
from collections import Counter
|
|
7
6
|
|
|
8
7
|
from redup.core.models import (
|
|
9
8
|
DuplicateGroup,
|
|
@@ -20,7 +19,7 @@ def _common_prefix(paths: list[str]) -> str:
|
|
|
20
19
|
return ""
|
|
21
20
|
parts_list = [p.split(os.sep) for p in paths]
|
|
22
21
|
prefix: list[str] = []
|
|
23
|
-
for segments in zip(*parts_list):
|
|
22
|
+
for segments in zip(*parts_list, strict=True):
|
|
24
23
|
if len(set(segments)) == 1:
|
|
25
24
|
prefix.append(segments[0])
|
|
26
25
|
else:
|
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Iterator
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Iterator
|
|
10
10
|
|
|
11
11
|
from redup.core.models import ScanConfig, ScanStats
|
|
12
12
|
|
|
@@ -10,7 +10,7 @@ def to_yaml(dup_map: DuplicationMap) -> str:
|
|
|
10
10
|
try:
|
|
11
11
|
import yaml
|
|
12
12
|
except ImportError:
|
|
13
|
-
raise ImportError("pyyaml is required for YAML output: pip install pyyaml")
|
|
13
|
+
raise ImportError("pyyaml is required for YAML output: pip install pyyaml") from None
|
|
14
14
|
|
|
15
15
|
from redup.reporters.json_reporter import _group_to_dict, _suggestion_to_dict
|
|
16
16
|
|
|
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
|