redup 0.3.7__tar.gz → 0.3.9__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.7 → redup-0.3.9}/PKG-INFO +7 -7
- {redup-0.3.7 → redup-0.3.9}/README.md +6 -6
- {redup-0.3.7 → redup-0.3.9}/pyproject.toml +1 -1
- {redup-0.3.7 → redup-0.3.9}/src/redup/__init__.py +1 -1
- {redup-0.3.7 → redup-0.3.9}/src/redup/cli_app/main.py +15 -2
- redup-0.3.9/src/redup/core/hash_cache.py +104 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/scanner.py +50 -4
- redup-0.3.9/src/redup/reporters/code2llm_reporter.py +209 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/PKG-INFO +7 -7
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/SOURCES.txt +4 -1
- {redup-0.3.7 → redup-0.3.9}/tests/test_scanner.py +3 -3
- redup-0.3.9/tests/test_ts_extractor.py +225 -0
- {redup-0.3.7 → redup-0.3.9}/LICENSE +0 -0
- {redup-0.3.7 → redup-0.3.9}/setup.cfg +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/__main__.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/__init__.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/config.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/differ.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/hasher.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/matcher.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/models.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/parallel_scanner.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/pipeline.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/planner.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/ts_extractor.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/requires.txt +0 -0
- {redup-0.3.7 → redup-0.3.9}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_e2e.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_hasher.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_matcher.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_models.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_pipeline.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_planner.py +0 -0
- {redup-0.3.7 → redup-0.3.9}/tests/test_reporters.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.9
|
|
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
|
|
|
@@ -92,7 +92,7 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
92
92
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
93
93
|
- **Structural clone detection** — same AST shape, different variable names
|
|
94
94
|
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
95
|
-
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C
|
|
95
|
+
- **Multi-language support** — 35+ languages via tree-sitter (Python, JavaScript, TypeScript, Go, Rust, Java, C/C++, C#, Ruby, PHP, Bash, SQL, HTML, CSS, Lua, Scala, Kotlin, Swift, Objective-C, JSON, YAML, TOML, XML, Markdown, GraphQL, Dockerfile, Makefile, Nginx, Vim, Svelte, Vue, and more)
|
|
96
96
|
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
97
97
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
98
98
|
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
@@ -133,8 +133,8 @@ redup scan ./src --format json --output ./reports/
|
|
|
133
133
|
# Parallel scanning for large projects
|
|
134
134
|
redup scan . --parallel --max-workers 4
|
|
135
135
|
|
|
136
|
-
# Multi-language scanning
|
|
137
|
-
redup scan . --ext ".py,.js,.ts,.go"
|
|
136
|
+
# Multi-language scanning with 35+ supported languages
|
|
137
|
+
redup scan . --ext ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
138
138
|
|
|
139
139
|
# CI gate with thresholds
|
|
140
140
|
redup check . --max-groups 10 --max-lines 100
|
|
@@ -166,7 +166,7 @@ Create a `redup.toml` file:
|
|
|
166
166
|
|
|
167
167
|
```toml
|
|
168
168
|
[scan]
|
|
169
|
-
extensions = ".py,.js,.ts,.go,.rs,.java"
|
|
169
|
+
extensions = ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
170
170
|
min_lines = 3
|
|
171
171
|
min_similarity = 0.85
|
|
172
172
|
include_tests = false
|
|
@@ -201,7 +201,7 @@ from redup.reporters.json_reporter import to_json
|
|
|
201
201
|
|
|
202
202
|
config = ScanConfig(
|
|
203
203
|
root=Path("./my_project"),
|
|
204
|
-
extensions=[".py"],
|
|
204
|
+
extensions=[".py", ".js", ".ts", ".go", ".rs", ".java", ".rb", ".php", ".html", ".css"],
|
|
205
205
|
min_block_lines=3,
|
|
206
206
|
min_similarity=0.85,
|
|
207
207
|
)
|
|
@@ -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
|
|
|
@@ -14,7 +14,7 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
14
14
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
15
15
|
- **Structural clone detection** — same AST shape, different variable names
|
|
16
16
|
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
17
|
-
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C
|
|
17
|
+
- **Multi-language support** — 35+ languages via tree-sitter (Python, JavaScript, TypeScript, Go, Rust, Java, C/C++, C#, Ruby, PHP, Bash, SQL, HTML, CSS, Lua, Scala, Kotlin, Swift, Objective-C, JSON, YAML, TOML, XML, Markdown, GraphQL, Dockerfile, Makefile, Nginx, Vim, Svelte, Vue, and more)
|
|
18
18
|
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
19
19
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
20
20
|
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
@@ -55,8 +55,8 @@ redup scan ./src --format json --output ./reports/
|
|
|
55
55
|
# Parallel scanning for large projects
|
|
56
56
|
redup scan . --parallel --max-workers 4
|
|
57
57
|
|
|
58
|
-
# Multi-language scanning
|
|
59
|
-
redup scan . --ext ".py,.js,.ts,.go"
|
|
58
|
+
# Multi-language scanning with 35+ supported languages
|
|
59
|
+
redup scan . --ext ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
60
60
|
|
|
61
61
|
# CI gate with thresholds
|
|
62
62
|
redup check . --max-groups 10 --max-lines 100
|
|
@@ -88,7 +88,7 @@ Create a `redup.toml` file:
|
|
|
88
88
|
|
|
89
89
|
```toml
|
|
90
90
|
[scan]
|
|
91
|
-
extensions = ".py,.js,.ts,.go,.rs,.java"
|
|
91
|
+
extensions = ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
92
92
|
min_lines = 3
|
|
93
93
|
min_similarity = 0.85
|
|
94
94
|
include_tests = false
|
|
@@ -123,7 +123,7 @@ from redup.reporters.json_reporter import to_json
|
|
|
123
123
|
|
|
124
124
|
config = ScanConfig(
|
|
125
125
|
root=Path("./my_project"),
|
|
126
|
-
extensions=[".py"],
|
|
126
|
+
extensions=[".py", ".js", ".ts", ".go", ".rs", ".java", ".rb", ".php", ".html", ".css"],
|
|
127
127
|
min_block_lines=3,
|
|
128
128
|
min_similarity=0.85,
|
|
129
129
|
)
|
|
@@ -20,6 +20,7 @@ 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
|
|
22
22
|
from redup.reporters.yaml_reporter import to_yaml # noqa: E402
|
|
23
|
+
from redup.reporters.code2llm_reporter import export_code2llm, to_code2llm_context, to_code2llm_toon # noqa: E402
|
|
23
24
|
from redup.reporters.enhanced_reporter import EnhancedReporter # noqa: E402
|
|
24
25
|
|
|
25
26
|
app = typer.Typer(
|
|
@@ -29,7 +30,7 @@ app = typer.Typer(
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
OutputFormat = Literal["json", "yaml", "toon", "markdown", "all", "enhanced"]
|
|
33
|
+
OutputFormat = Literal["json", "yaml", "toon", "markdown", "all", "enhanced", "code2llm"]
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
DEFAULT_PATH = Path(".")
|
|
@@ -157,6 +158,18 @@ def _write_results(
|
|
|
157
158
|
target = output_dir if output_dir and output_dir.suffix else output_dir / "duplication.enhanced.json"
|
|
158
159
|
reporter.save_enhanced_report(target)
|
|
159
160
|
typer.echo(f" → {target}")
|
|
161
|
+
elif format == "code2llm":
|
|
162
|
+
# Export code2llm format (analysis.toon + context.md)
|
|
163
|
+
if output_dir is None:
|
|
164
|
+
output_dir = Path("code2llm_output")
|
|
165
|
+
toon_path, context_path = export_code2llm(
|
|
166
|
+
dup_map,
|
|
167
|
+
output_dir,
|
|
168
|
+
files_scanned=dup_map.stats.files_scanned,
|
|
169
|
+
total_lines=dup_map.stats.total_lines,
|
|
170
|
+
)
|
|
171
|
+
typer.echo(f" → {toon_path}")
|
|
172
|
+
typer.echo(f" → {context_path}")
|
|
160
173
|
|
|
161
174
|
|
|
162
175
|
@app.command()
|
|
@@ -171,7 +184,7 @@ def scan(
|
|
|
171
184
|
format: str = typer.Option(
|
|
172
185
|
"toon",
|
|
173
186
|
"--format", "-f",
|
|
174
|
-
help="Output format (json, yaml, toon, markdown, all, enhanced).",
|
|
187
|
+
help="Output format (json, yaml, toon, markdown, code2llm, all, enhanced).",
|
|
175
188
|
),
|
|
176
189
|
output: Path | None = typer.Option(
|
|
177
190
|
DEFAULT_OUTPUT,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Hash cache for incremental reDUP scans.
|
|
2
|
+
|
|
3
|
+
Stores SHA-256 hashes of files to skip unchanged files in subsequent scans.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HashCache:
|
|
15
|
+
"""Cache for file hashes to enable incremental scanning."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, cache_path: Path | None = None):
|
|
18
|
+
if cache_path is None:
|
|
19
|
+
cache_path = Path(".redup_cache.json")
|
|
20
|
+
self.cache_path = cache_path
|
|
21
|
+
self._cache: dict[str, dict[str, Any]] = {}
|
|
22
|
+
self._load()
|
|
23
|
+
|
|
24
|
+
def _load(self) -> None:
|
|
25
|
+
"""Load cache from disk."""
|
|
26
|
+
if self.cache_path.exists():
|
|
27
|
+
try:
|
|
28
|
+
with open(self.cache_path, 'r', encoding='utf-8') as f:
|
|
29
|
+
self._cache = json.load(f)
|
|
30
|
+
except (json.JSONDecodeError, IOError):
|
|
31
|
+
self._cache = {}
|
|
32
|
+
|
|
33
|
+
def save(self) -> None:
|
|
34
|
+
"""Save cache to disk."""
|
|
35
|
+
try:
|
|
36
|
+
with open(self.cache_path, 'w', encoding='utf-8') as f:
|
|
37
|
+
json.dump(self._cache, f, indent=2)
|
|
38
|
+
except IOError:
|
|
39
|
+
pass # Cache save is best-effort
|
|
40
|
+
|
|
41
|
+
def get_file_hash(self, file_path: Path) -> str:
|
|
42
|
+
"""Calculate hash of file content."""
|
|
43
|
+
try:
|
|
44
|
+
with open(file_path, 'rb') as f:
|
|
45
|
+
return hashlib.sha256(f.read()).hexdigest()[:16]
|
|
46
|
+
except (IOError, OSError):
|
|
47
|
+
return ""
|
|
48
|
+
|
|
49
|
+
def is_unchanged(self, file_path: Path, config_hash: str = "") -> bool:
|
|
50
|
+
"""Check if file is unchanged since last scan."""
|
|
51
|
+
path_str = str(file_path)
|
|
52
|
+
|
|
53
|
+
if path_str not in self._cache:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
entry = self._cache[path_str]
|
|
57
|
+
current_hash = self.get_file_hash(file_path)
|
|
58
|
+
|
|
59
|
+
# Check content hash and config
|
|
60
|
+
return (
|
|
61
|
+
entry.get("content_hash") == current_hash and
|
|
62
|
+
entry.get("config_hash") == config_hash
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def update(self, file_path: Path, file_results: dict[str, Any], config_hash: str = "") -> None:
|
|
66
|
+
"""Update cache entry for a file."""
|
|
67
|
+
path_str = str(file_path)
|
|
68
|
+
content_hash = self.get_file_hash(file_path)
|
|
69
|
+
|
|
70
|
+
self._cache[path_str] = {
|
|
71
|
+
"content_hash": content_hash,
|
|
72
|
+
"config_hash": config_hash,
|
|
73
|
+
"mtime": file_path.stat().st_mtime if file_path.exists() else 0,
|
|
74
|
+
"results": file_results,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def invalidate(self, file_path: Path | None = None) -> None:
|
|
78
|
+
"""Invalidate cache for a file or entire cache."""
|
|
79
|
+
if file_path is None:
|
|
80
|
+
self._cache = {}
|
|
81
|
+
else:
|
|
82
|
+
path_str = str(file_path)
|
|
83
|
+
self._cache.pop(path_str, None)
|
|
84
|
+
|
|
85
|
+
def get_cached_results(self, file_path: Path) -> dict[str, Any] | None:
|
|
86
|
+
"""Get cached results for unchanged file."""
|
|
87
|
+
path_str = str(file_path)
|
|
88
|
+
|
|
89
|
+
if path_str in self._cache:
|
|
90
|
+
return self._cache[path_str].get("results")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def clear(self) -> None:
|
|
94
|
+
"""Clear entire cache."""
|
|
95
|
+
self._cache = {}
|
|
96
|
+
if self.cache_path.exists():
|
|
97
|
+
self.cache_path.unlink()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _config_to_hash(config: Any) -> str:
|
|
101
|
+
"""Convert config to a hash string for cache validation."""
|
|
102
|
+
import json
|
|
103
|
+
config_str = json.dumps(config, sort_keys=True, default=str)
|
|
104
|
+
return hashlib.sha256(config_str.encode()).hexdigest()[:8]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import functools
|
|
6
7
|
import time
|
|
7
8
|
from collections.abc import Iterator
|
|
8
9
|
from dataclasses import dataclass, field
|
|
@@ -40,7 +41,8 @@ class ScannedFile:
|
|
|
40
41
|
return len(self.lines)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
@functools.lru_cache(maxsize=1000)
|
|
45
|
+
def _should_exclude(path: Path, patterns: tuple[str, ...]) -> bool:
|
|
44
46
|
"""Check if a path matches any exclusion pattern."""
|
|
45
47
|
name = path.name
|
|
46
48
|
str_path = str(path)
|
|
@@ -63,7 +65,7 @@ def _collect_files(config: ScanConfig) -> Iterator[Path]:
|
|
|
63
65
|
root = config.root.resolve()
|
|
64
66
|
for ext in config.extensions:
|
|
65
67
|
for path in root.rglob(f"*{ext}"):
|
|
66
|
-
if _should_exclude(path, config.exclude_patterns):
|
|
68
|
+
if _should_exclude(path, tuple(config.exclude_patterns)):
|
|
67
69
|
continue
|
|
68
70
|
if not config.include_tests and _is_test_file(path):
|
|
69
71
|
continue
|
|
@@ -100,12 +102,54 @@ def _extract_blocks_sliding(lines: list[str], min_lines: int) -> list[tuple[int,
|
|
|
100
102
|
chunk = non_empty[start_idx : start_idx + window_size]
|
|
101
103
|
first_line = chunk[0][0] + 1
|
|
102
104
|
last_line = chunk[-1][0] + 1
|
|
103
|
-
|
|
105
|
+
# Optimized: reduce string operations
|
|
106
|
+
lines = [line.strip() for _, line in chunk]
|
|
107
|
+
text = "\n".join(lines)
|
|
104
108
|
blocks.append((first_line, last_line, text))
|
|
105
109
|
|
|
106
110
|
return blocks
|
|
107
111
|
|
|
108
112
|
|
|
113
|
+
def _extract_class_blocks_python(source: str, filepath: str) -> list[CodeBlock]:
|
|
114
|
+
"""Extract class-level blocks from Python source using ast.
|
|
115
|
+
|
|
116
|
+
This enables detection of duplicate classes across files.
|
|
117
|
+
"""
|
|
118
|
+
import ast
|
|
119
|
+
|
|
120
|
+
blocks: list[CodeBlock] = []
|
|
121
|
+
try:
|
|
122
|
+
tree = ast.parse(source)
|
|
123
|
+
except SyntaxError:
|
|
124
|
+
return blocks
|
|
125
|
+
|
|
126
|
+
for node in ast.walk(tree):
|
|
127
|
+
if isinstance(node, ast.ClassDef):
|
|
128
|
+
start = node.lineno
|
|
129
|
+
end = node.end_lineno or start
|
|
130
|
+
lines = source.splitlines()
|
|
131
|
+
text = "\n".join(lines[start - 1 : end])
|
|
132
|
+
|
|
133
|
+
# Get method names for context
|
|
134
|
+
method_names = [
|
|
135
|
+
n.name for n in ast.walk(node)
|
|
136
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
blocks.append(
|
|
140
|
+
CodeBlock(
|
|
141
|
+
file=filepath,
|
|
142
|
+
line_start=start,
|
|
143
|
+
line_end=end,
|
|
144
|
+
text=text,
|
|
145
|
+
function_name=None, # It's a class, not a function
|
|
146
|
+
class_name=node.name,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return blocks
|
|
151
|
+
|
|
152
|
+
|
|
109
153
|
def _extract_function_blocks_python(source: str, filepath: str) -> list[CodeBlock]:
|
|
110
154
|
"""Extract function/method-level blocks from Python source using ast."""
|
|
111
155
|
import ast
|
|
@@ -171,6 +215,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
171
215
|
# Extract function-level blocks for Python and other supported languages
|
|
172
216
|
if filepath.suffix == ".py":
|
|
173
217
|
func_blocks = _extract_function_blocks_python(source, rel_path)
|
|
218
|
+
class_blocks = _extract_class_blocks_python(source, rel_path)
|
|
174
219
|
else:
|
|
175
220
|
# Try tree-sitter extraction for other languages
|
|
176
221
|
try:
|
|
@@ -181,6 +226,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
181
226
|
func_blocks = []
|
|
182
227
|
except ImportError:
|
|
183
228
|
func_blocks = []
|
|
229
|
+
class_blocks = []
|
|
184
230
|
|
|
185
231
|
# Also extract sliding-window blocks for line-level matching
|
|
186
232
|
sliding = _extract_blocks_sliding(lines, config.min_block_lines)
|
|
@@ -189,7 +235,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
189
235
|
for s, e, t in sliding
|
|
190
236
|
]
|
|
191
237
|
|
|
192
|
-
all_blocks = func_blocks + line_blocks
|
|
238
|
+
all_blocks = func_blocks + class_blocks + line_blocks
|
|
193
239
|
|
|
194
240
|
sf = ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
|
|
195
241
|
scanned.append(sf)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""code2llm reporter — generates LLM-optimized project analysis.
|
|
2
|
+
|
|
3
|
+
This reporter produces output compatible with code2llm format:
|
|
4
|
+
- analysis.toon: compact summary for LLM context windows
|
|
5
|
+
- context.md: detailed architecture documentation
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from redup.core.models import DuplicationMap
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _calculate_avg_cc(dup_map: DuplicationMap) -> float:
|
|
18
|
+
"""Calculate average cyclomatic complexity (placeholder)."""
|
|
19
|
+
return 3.5 # Placeholder - would need actual CC calculation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _count_critical_functions(dup_map: DuplicationMap) -> int:
|
|
23
|
+
"""Count functions with CC >= 10."""
|
|
24
|
+
return 0 # Placeholder
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_layers(dup_map: DuplicationMap) -> list[dict]:
|
|
28
|
+
"""Extract layer information from project structure."""
|
|
29
|
+
layers = []
|
|
30
|
+
|
|
31
|
+
# Group files by directory
|
|
32
|
+
from collections import defaultdict
|
|
33
|
+
dir_stats = defaultdict(lambda: {"files": 0, "lines": 0, "functions": 0})
|
|
34
|
+
|
|
35
|
+
# This is a simplified version - full implementation would need file stats
|
|
36
|
+
layers.append({
|
|
37
|
+
"name": "src/",
|
|
38
|
+
"cc_avg": _calculate_avg_cc(dup_map),
|
|
39
|
+
"imports_in": 0,
|
|
40
|
+
"imports_out": 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return layers
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def to_code2llm_toon(
|
|
47
|
+
dup_map: DuplicationMap,
|
|
48
|
+
files_scanned: int = 0,
|
|
49
|
+
total_lines: int = 0,
|
|
50
|
+
functions_count: int = 0,
|
|
51
|
+
classes_count: int = 0,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Generate code2llm-compatible TOON format.
|
|
54
|
+
|
|
55
|
+
This format is optimized for LLM context windows with compact representation.
|
|
56
|
+
"""
|
|
57
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
58
|
+
cc_avg = _calculate_avg_cc(dup_map)
|
|
59
|
+
critical = _count_critical_functions(dup_map)
|
|
60
|
+
|
|
61
|
+
lines = [
|
|
62
|
+
f"# code2llm | {files_scanned}f {total_lines}L | py:{files_scanned} | {now}",
|
|
63
|
+
f"# CC̄={cc_avg:.1f} | critical:{critical}/{functions_count} | dups:{dup_map.total_groups} | cycles:0",
|
|
64
|
+
"",
|
|
65
|
+
"HEALTH[0]: ok",
|
|
66
|
+
"",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Add duplication info
|
|
70
|
+
if dup_map.total_groups > 0:
|
|
71
|
+
lines.append(f"DUPLICATES[{dup_map.total_groups}]: see context.md")
|
|
72
|
+
lines.append("")
|
|
73
|
+
|
|
74
|
+
for group in dup_map.sorted_by_impact()[:5]: # Top 5 by impact
|
|
75
|
+
marker = "!!" if group.impact_score > 100 else "!" if group.impact_score > 30 else " "
|
|
76
|
+
lines.append(
|
|
77
|
+
f" [{group.id}] {marker} {group.duplicate_type.value.upper()[:4]} "
|
|
78
|
+
f"L={group.total_lines} N={group.occurrences} saved={group.saved_lines_potential}"
|
|
79
|
+
)
|
|
80
|
+
lines.append("")
|
|
81
|
+
else:
|
|
82
|
+
lines.append("DUPLICATES[0]: none")
|
|
83
|
+
lines.append("")
|
|
84
|
+
|
|
85
|
+
lines.append("REFACTOR[0]: none needed")
|
|
86
|
+
lines.append("")
|
|
87
|
+
lines.append("COUPLING: no cross-package imports detected")
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def to_code2llm_context(
|
|
94
|
+
dup_map: DuplicationMap,
|
|
95
|
+
files_scanned: int = 0,
|
|
96
|
+
total_lines: int = 0,
|
|
97
|
+
functions_count: int = 0,
|
|
98
|
+
classes_count: int = 0,
|
|
99
|
+
modules: list[str] | None = None,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Generate code2llm-compatible context.md format.
|
|
102
|
+
|
|
103
|
+
This provides detailed architecture analysis for LLMs.
|
|
104
|
+
"""
|
|
105
|
+
if modules is None:
|
|
106
|
+
modules = []
|
|
107
|
+
|
|
108
|
+
lines = [
|
|
109
|
+
"# System Architecture Analysis",
|
|
110
|
+
"",
|
|
111
|
+
"## Overview",
|
|
112
|
+
"",
|
|
113
|
+
f"- **Project**: {dup_map.project_path}",
|
|
114
|
+
f"- **Primary Language**: python",
|
|
115
|
+
f"- **Total Functions**: {functions_count}",
|
|
116
|
+
f"- **Total Classes**: {classes_count}",
|
|
117
|
+
f"- **Modules**: {len(modules)}",
|
|
118
|
+
"",
|
|
119
|
+
"## Duplication Analysis",
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
if dup_map.total_groups == 0:
|
|
124
|
+
lines.append("No code duplication detected.")
|
|
125
|
+
lines.append("")
|
|
126
|
+
else:
|
|
127
|
+
lines.append(f"**Total Duplicate Groups**: {dup_map.total_groups}")
|
|
128
|
+
lines.append(f"**Total Fragments**: {dup_map.total_fragments}")
|
|
129
|
+
lines.append(f"**Recoverable Lines**: {dup_map.total_saved_lines}")
|
|
130
|
+
lines.append("")
|
|
131
|
+
|
|
132
|
+
lines.append("### Duplicate Groups by Impact")
|
|
133
|
+
lines.append("")
|
|
134
|
+
|
|
135
|
+
for group in dup_map.sorted_by_impact():
|
|
136
|
+
lines.append(f"#### {group.id}: {group.normalized_name or 'Unnamed'}")
|
|
137
|
+
lines.append("")
|
|
138
|
+
lines.append(f"- **Type**: {group.duplicate_type.value}")
|
|
139
|
+
lines.append(f"- **Similarity**: {group.similarity_score:.2f}")
|
|
140
|
+
lines.append(f"- **Occurrences**: {group.occurrences}")
|
|
141
|
+
lines.append(f"- **Lines**: {group.total_lines}")
|
|
142
|
+
lines.append(f"- **Impact Score**: {group.impact_score:.1f}")
|
|
143
|
+
lines.append("")
|
|
144
|
+
lines.append("**Locations**:")
|
|
145
|
+
for frag in group.fragments:
|
|
146
|
+
fn_info = f" (`{frag.function_name}`)" if frag.function_name else ""
|
|
147
|
+
lines.append(f"- `{frag.file}:{frag.line_start}-{frag.line_end}`{fn_info}")
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
if dup_map.suggestions:
|
|
151
|
+
lines.append("## Refactoring Suggestions")
|
|
152
|
+
lines.append("")
|
|
153
|
+
|
|
154
|
+
for suggestion in dup_map.suggestions[:10]: # Top 10
|
|
155
|
+
lines.append(f"### Priority {suggestion.priority}: {suggestion.action.value}")
|
|
156
|
+
lines.append("")
|
|
157
|
+
lines.append(f"- **Target**: `{suggestion.new_module}`")
|
|
158
|
+
lines.append(f"- **Risk Level**: {suggestion.risk_level.value}")
|
|
159
|
+
lines.append(f"- **Rationale**: {suggestion.rationale}")
|
|
160
|
+
lines.append("")
|
|
161
|
+
lines.append("**Affected Files**:")
|
|
162
|
+
for f in suggestion.original_files[:5]:
|
|
163
|
+
lines.append(f"- `{f}`")
|
|
164
|
+
if len(suggestion.original_files) > 5:
|
|
165
|
+
lines.append(f"- ... and {len(suggestion.original_files) - 5} more")
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
return "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def export_code2llm(
|
|
172
|
+
dup_map: DuplicationMap,
|
|
173
|
+
output_dir: Path,
|
|
174
|
+
files_scanned: int = 0,
|
|
175
|
+
total_lines: int = 0,
|
|
176
|
+
functions_count: int = 0,
|
|
177
|
+
classes_count: int = 0,
|
|
178
|
+
) -> tuple[Path, Path]:
|
|
179
|
+
"""Export both code2llm files to the specified directory.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Tuple of (analysis_toon_path, context_md_path)
|
|
183
|
+
"""
|
|
184
|
+
output_dir = Path(output_dir)
|
|
185
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
# Generate analysis.toon
|
|
188
|
+
toon_content = to_code2llm_toon(
|
|
189
|
+
dup_map,
|
|
190
|
+
files_scanned=files_scanned,
|
|
191
|
+
total_lines=total_lines,
|
|
192
|
+
functions_count=functions_count,
|
|
193
|
+
classes_count=classes_count,
|
|
194
|
+
)
|
|
195
|
+
toon_path = output_dir / "analysis.toon"
|
|
196
|
+
toon_path.write_text(toon_content, encoding="utf-8")
|
|
197
|
+
|
|
198
|
+
# Generate context.md
|
|
199
|
+
context_content = to_code2llm_context(
|
|
200
|
+
dup_map,
|
|
201
|
+
files_scanned=files_scanned,
|
|
202
|
+
total_lines=total_lines,
|
|
203
|
+
functions_count=functions_count,
|
|
204
|
+
classes_count=classes_count,
|
|
205
|
+
)
|
|
206
|
+
context_path = output_dir / "context.md"
|
|
207
|
+
context_path.write_text(context_content, encoding="utf-8")
|
|
208
|
+
|
|
209
|
+
return (toon_path, context_path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redup
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
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
|
|
|
@@ -92,7 +92,7 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
|
|
|
92
92
|
- **Exact duplicate detection** via SHA-256 block hashing
|
|
93
93
|
- **Structural clone detection** — same AST shape, different variable names
|
|
94
94
|
- **LSH near-duplicate detection** for large code blocks (>50 lines)
|
|
95
|
-
- **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C
|
|
95
|
+
- **Multi-language support** — 35+ languages via tree-sitter (Python, JavaScript, TypeScript, Go, Rust, Java, C/C++, C#, Ruby, PHP, Bash, SQL, HTML, CSS, Lua, Scala, Kotlin, Swift, Objective-C, JSON, YAML, TOML, XML, Markdown, GraphQL, Dockerfile, Makefile, Nginx, Vim, Svelte, Vue, and more)
|
|
96
96
|
- **Parallel scanning** for large projects (2x+ performance improvement)
|
|
97
97
|
- **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
|
|
98
98
|
- **Function-level analysis** using Python AST and tree-sitter extraction
|
|
@@ -133,8 +133,8 @@ redup scan ./src --format json --output ./reports/
|
|
|
133
133
|
# Parallel scanning for large projects
|
|
134
134
|
redup scan . --parallel --max-workers 4
|
|
135
135
|
|
|
136
|
-
# Multi-language scanning
|
|
137
|
-
redup scan . --ext ".py,.js,.ts,.go"
|
|
136
|
+
# Multi-language scanning with 35+ supported languages
|
|
137
|
+
redup scan . --ext ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
138
138
|
|
|
139
139
|
# CI gate with thresholds
|
|
140
140
|
redup check . --max-groups 10 --max-lines 100
|
|
@@ -166,7 +166,7 @@ Create a `redup.toml` file:
|
|
|
166
166
|
|
|
167
167
|
```toml
|
|
168
168
|
[scan]
|
|
169
|
-
extensions = ".py,.js,.ts,.go,.rs,.java"
|
|
169
|
+
extensions = ".py,.js,.ts,.go,.rs,.java,.rb,.php,.html,.css,.sql,.lua,.scala,.kt,.swift,.m,.json,.yaml,.toml,.xml,.md,.graphql,.dockerfile,.svelte,.vue"
|
|
170
170
|
min_lines = 3
|
|
171
171
|
min_similarity = 0.85
|
|
172
172
|
include_tests = false
|
|
@@ -201,7 +201,7 @@ from redup.reporters.json_reporter import to_json
|
|
|
201
201
|
|
|
202
202
|
config = ScanConfig(
|
|
203
203
|
root=Path("./my_project"),
|
|
204
|
-
extensions=[".py"],
|
|
204
|
+
extensions=[".py", ".js", ".ts", ".go", ".rs", ".java", ".rb", ".php", ".html", ".css"],
|
|
205
205
|
min_block_lines=3,
|
|
206
206
|
min_similarity=0.85,
|
|
207
207
|
)
|
|
@@ -14,6 +14,7 @@ src/redup/cli_app/main.py
|
|
|
14
14
|
src/redup/core/__init__.py
|
|
15
15
|
src/redup/core/config.py
|
|
16
16
|
src/redup/core/differ.py
|
|
17
|
+
src/redup/core/hash_cache.py
|
|
17
18
|
src/redup/core/hasher.py
|
|
18
19
|
src/redup/core/lsh_matcher.py
|
|
19
20
|
src/redup/core/matcher.py
|
|
@@ -28,6 +29,7 @@ src/redup/core/utils/duplicate_finders.py
|
|
|
28
29
|
src/redup/core/utils/function_extractor.py
|
|
29
30
|
src/redup/core/utils/hash_utils.py
|
|
30
31
|
src/redup/reporters/__init__.py
|
|
32
|
+
src/redup/reporters/code2llm_reporter.py
|
|
31
33
|
src/redup/reporters/enhanced_reporter.py
|
|
32
34
|
src/redup/reporters/json_reporter.py
|
|
33
35
|
src/redup/reporters/markdown_reporter.py
|
|
@@ -40,4 +42,5 @@ tests/test_models.py
|
|
|
40
42
|
tests/test_pipeline.py
|
|
41
43
|
tests/test_planner.py
|
|
42
44
|
tests/test_reporters.py
|
|
43
|
-
tests/test_scanner.py
|
|
45
|
+
tests/test_scanner.py
|
|
46
|
+
tests/test_ts_extractor.py
|
|
@@ -13,15 +13,15 @@ from redup.core.scanner import (
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def test_should_exclude_git():
|
|
16
|
-
assert _should_exclude(Path(".git/config"),
|
|
16
|
+
assert _should_exclude(Path(".git/config"), (".git",))
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_should_exclude_venv():
|
|
20
|
-
assert _should_exclude(Path("project/venv/lib/site.py"),
|
|
20
|
+
assert _should_exclude(Path("project/venv/lib/site.py"), ("venv",))
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_should_not_exclude_normal():
|
|
24
|
-
assert not _should_exclude(Path("src/main.py"),
|
|
24
|
+
assert not _should_exclude(Path("src/main.py"), (".git", "venv"))
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_is_test_file():
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Tests for tree-sitter multi-language extraction."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from redup.core.ts_extractor import (
|
|
8
|
+
_LANGUAGE_MAPPING,
|
|
9
|
+
_language_registry,
|
|
10
|
+
extract_functions_treesitter,
|
|
11
|
+
get_supported_languages,
|
|
12
|
+
is_language_supported,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestLanguageMapping:
|
|
17
|
+
"""Test language extension mappings."""
|
|
18
|
+
|
|
19
|
+
def test_language_mapping_has_common_extensions(self):
|
|
20
|
+
"""Test that common language extensions are mapped."""
|
|
21
|
+
assert ".py" in _LANGUAGE_MAPPING
|
|
22
|
+
assert ".js" in _LANGUAGE_MAPPING
|
|
23
|
+
assert ".ts" in _LANGUAGE_MAPPING
|
|
24
|
+
assert ".go" in _LANGUAGE_MAPPING
|
|
25
|
+
assert ".rs" in _LANGUAGE_MAPPING
|
|
26
|
+
assert ".java" in _LANGUAGE_MAPPING
|
|
27
|
+
|
|
28
|
+
def test_language_mapping_has_new_web_languages(self):
|
|
29
|
+
"""Test that new web language extensions are mapped."""
|
|
30
|
+
assert ".html" in _LANGUAGE_MAPPING
|
|
31
|
+
assert ".css" in _LANGUAGE_MAPPING
|
|
32
|
+
assert ".svelte" in _LANGUAGE_MAPPING
|
|
33
|
+
assert ".vue" in _LANGUAGE_MAPPING
|
|
34
|
+
|
|
35
|
+
def test_language_mapping_has_data_formats(self):
|
|
36
|
+
"""Test that data format extensions are mapped."""
|
|
37
|
+
assert ".json" in _LANGUAGE_MAPPING
|
|
38
|
+
assert ".yaml" in _LANGUAGE_MAPPING
|
|
39
|
+
assert ".yml" in _LANGUAGE_MAPPING
|
|
40
|
+
assert ".toml" in _LANGUAGE_MAPPING
|
|
41
|
+
assert ".xml" in _LANGUAGE_MAPPING
|
|
42
|
+
assert ".md" in _LANGUAGE_MAPPING
|
|
43
|
+
|
|
44
|
+
def test_language_mapping_has_sql(self):
|
|
45
|
+
"""Test that SQL extensions are mapped."""
|
|
46
|
+
assert ".sql" in _LANGUAGE_MAPPING
|
|
47
|
+
assert ".ddl" in _LANGUAGE_MAPPING
|
|
48
|
+
assert ".dml" in _LANGUAGE_MAPPING
|
|
49
|
+
|
|
50
|
+
def test_language_mapping_has_dsl_languages(self):
|
|
51
|
+
"""Test that DSL language extensions are mapped."""
|
|
52
|
+
assert ".graphql" in _LANGUAGE_MAPPING
|
|
53
|
+
assert ".gql" in _LANGUAGE_MAPPING
|
|
54
|
+
assert "Dockerfile" in _LANGUAGE_MAPPING
|
|
55
|
+
assert ".dockerfile" in _LANGUAGE_MAPPING
|
|
56
|
+
assert "Makefile" in _LANGUAGE_MAPPING
|
|
57
|
+
assert ".mk" in _LANGUAGE_MAPPING
|
|
58
|
+
assert ".nginx" in _LANGUAGE_MAPPING
|
|
59
|
+
assert ".vim" in _LANGUAGE_MAPPING
|
|
60
|
+
assert ".svelte" in _LANGUAGE_MAPPING
|
|
61
|
+
assert ".vue" in _LANGUAGE_MAPPING
|
|
62
|
+
|
|
63
|
+
def test_language_mapping_has_additional_programming(self):
|
|
64
|
+
"""Test that additional programming language extensions are mapped."""
|
|
65
|
+
assert ".rb" in _LANGUAGE_MAPPING
|
|
66
|
+
assert ".php" in _LANGUAGE_MAPPING
|
|
67
|
+
assert ".sh" in _LANGUAGE_MAPPING
|
|
68
|
+
assert ".lua" in _LANGUAGE_MAPPING
|
|
69
|
+
assert ".scala" in _LANGUAGE_MAPPING
|
|
70
|
+
assert ".kt" in _LANGUAGE_MAPPING
|
|
71
|
+
assert ".swift" in _LANGUAGE_MAPPING
|
|
72
|
+
assert ".m" in _LANGUAGE_MAPPING
|
|
73
|
+
assert ".cs" in _LANGUAGE_MAPPING
|
|
74
|
+
|
|
75
|
+
def test_language_mapping_count(self):
|
|
76
|
+
"""Test that we have expected number of language mappings."""
|
|
77
|
+
# Should have at least 80 mappings (33 languages with multiple extensions)
|
|
78
|
+
assert len(_LANGUAGE_MAPPING) >= 80
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestLanguageRegistry:
|
|
82
|
+
"""Test language registry for tree-sitter parsers."""
|
|
83
|
+
|
|
84
|
+
def test_registry_has_core_languages(self):
|
|
85
|
+
"""Test that core programming languages are in registry."""
|
|
86
|
+
assert "javascript" in _language_registry._languages
|
|
87
|
+
assert "typescript" in _language_registry._languages
|
|
88
|
+
assert "go" in _language_registry._languages
|
|
89
|
+
assert "rust" in _language_registry._languages
|
|
90
|
+
assert "java" in _language_registry._languages
|
|
91
|
+
|
|
92
|
+
def test_registry_has_web_languages(self):
|
|
93
|
+
"""Test that web languages are in registry."""
|
|
94
|
+
assert "html" in _language_registry._languages
|
|
95
|
+
assert "css" in _language_registry._languages
|
|
96
|
+
assert "svelte" in _language_registry._languages
|
|
97
|
+
assert "vue" in _language_registry._languages
|
|
98
|
+
|
|
99
|
+
def test_registry_has_new_languages(self):
|
|
100
|
+
"""Test that newly added languages are in registry."""
|
|
101
|
+
assert "ruby" in _language_registry._languages
|
|
102
|
+
assert "php" in _language_registry._languages
|
|
103
|
+
assert "bash" in _language_registry._languages
|
|
104
|
+
assert "lua" in _language_registry._languages
|
|
105
|
+
assert "sql" in _language_registry._languages
|
|
106
|
+
assert "scala" in _language_registry._languages
|
|
107
|
+
assert "kotlin" in _language_registry._languages
|
|
108
|
+
assert "swift" in _language_registry._languages
|
|
109
|
+
assert "objc" in _language_registry._languages
|
|
110
|
+
assert "c_sharp" in _language_registry._languages
|
|
111
|
+
|
|
112
|
+
def test_registry_has_dsl_languages(self):
|
|
113
|
+
"""Test that DSL languages are in registry."""
|
|
114
|
+
assert "graphql" in _language_registry._languages
|
|
115
|
+
assert "dockerfile" in _language_registry._languages
|
|
116
|
+
assert "make" in _language_registry._languages
|
|
117
|
+
assert "nginx" in _language_registry._languages
|
|
118
|
+
assert "vim" in _language_registry._languages
|
|
119
|
+
|
|
120
|
+
def test_registry_has_data_formats(self):
|
|
121
|
+
"""Test that data format languages are in registry."""
|
|
122
|
+
assert "json" in _language_registry._languages
|
|
123
|
+
assert "yaml" in _language_registry._languages
|
|
124
|
+
assert "toml" in _language_registry._languages
|
|
125
|
+
assert "xml" in _language_registry._languages
|
|
126
|
+
assert "markdown" in _language_registry._languages
|
|
127
|
+
|
|
128
|
+
def test_registry_count(self):
|
|
129
|
+
"""Test that we have expected number of registered languages."""
|
|
130
|
+
# Should have at least 33 registered languages
|
|
131
|
+
assert len(_language_registry._languages) >= 33
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestLanguageSupportFunctions:
|
|
135
|
+
"""Test language support checking functions."""
|
|
136
|
+
|
|
137
|
+
def test_is_language_supported_python(self):
|
|
138
|
+
"""Python is not supported by tree-sitter (uses AST instead)."""
|
|
139
|
+
assert not is_language_supported("test.py")
|
|
140
|
+
|
|
141
|
+
def test_is_language_supported_unknown_extension(self):
|
|
142
|
+
"""Unknown extensions are not supported."""
|
|
143
|
+
assert not is_language_supported("test.unknown")
|
|
144
|
+
|
|
145
|
+
def test_is_language_supported_known_languages(self):
|
|
146
|
+
"""Known language extensions are supported."""
|
|
147
|
+
# Note: This will return False if tree-sitter packages are not installed
|
|
148
|
+
# but the function should work without errors
|
|
149
|
+
result = is_language_supported("test.js")
|
|
150
|
+
# Result depends on whether tree-sitter is installed
|
|
151
|
+
assert result in [True, False]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestExtractFunctions:
|
|
155
|
+
"""Test function extraction for different languages."""
|
|
156
|
+
|
|
157
|
+
def test_extract_python_returns_empty(self):
|
|
158
|
+
"""Python extraction returns empty (uses AST instead)."""
|
|
159
|
+
source = "def hello(): pass"
|
|
160
|
+
result = extract_functions_treesitter(source, "test.py")
|
|
161
|
+
assert result == []
|
|
162
|
+
|
|
163
|
+
def test_extract_unknown_extension_returns_empty(self):
|
|
164
|
+
"""Unknown extension returns empty list."""
|
|
165
|
+
source = "some content"
|
|
166
|
+
result = extract_functions_treesitter(source, "test.unknown")
|
|
167
|
+
assert result == []
|
|
168
|
+
|
|
169
|
+
def test_extract_javascript_no_treesitter(self):
|
|
170
|
+
"""JavaScript extraction without tree-sitter returns empty."""
|
|
171
|
+
# This test will pass if tree-sitter is not installed
|
|
172
|
+
source = "function hello() { return 42; }"
|
|
173
|
+
result = extract_functions_treesitter(source, "test.js")
|
|
174
|
+
# Returns empty if tree-sitter is not installed
|
|
175
|
+
assert isinstance(result, list)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestHTMLExtraction:
|
|
179
|
+
"""Test HTML block extraction."""
|
|
180
|
+
|
|
181
|
+
def test_html_extraction_structure(self):
|
|
182
|
+
"""Test that HTML extraction function exists."""
|
|
183
|
+
from redup.core.ts_extractor import _extract_blocks_html_xml
|
|
184
|
+
# Function should exist and be callable
|
|
185
|
+
assert callable(_extract_blocks_html_xml)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestCSSExtraction:
|
|
189
|
+
"""Test CSS block extraction."""
|
|
190
|
+
|
|
191
|
+
def test_css_extraction_structure(self):
|
|
192
|
+
"""Test that CSS extraction function exists."""
|
|
193
|
+
from redup.core.ts_extractor import _extract_blocks_css
|
|
194
|
+
# Function should exist and be callable
|
|
195
|
+
assert callable(_extract_blocks_css)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestSQLExtraction:
|
|
199
|
+
"""Test SQL block extraction."""
|
|
200
|
+
|
|
201
|
+
def test_sql_extraction_structure(self):
|
|
202
|
+
"""Test that SQL extraction function exists."""
|
|
203
|
+
from redup.core.ts_extractor import _extract_blocks_sql
|
|
204
|
+
# Function should exist and be callable
|
|
205
|
+
assert callable(_extract_blocks_sql)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class TestCExtraction:
|
|
209
|
+
"""Test C/C++ function extraction."""
|
|
210
|
+
|
|
211
|
+
def test_c_extraction_structure(self):
|
|
212
|
+
"""Test that C extraction function exists."""
|
|
213
|
+
from redup.core.ts_extractor import _extract_functions_c_cpp
|
|
214
|
+
# Function should exist and be callable
|
|
215
|
+
assert callable(_extract_functions_c_cpp)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestLuaExtraction:
|
|
219
|
+
"""Test Lua function extraction."""
|
|
220
|
+
|
|
221
|
+
def test_lua_extraction_structure(self):
|
|
222
|
+
"""Test that Lua extraction function exists."""
|
|
223
|
+
from redup.core.ts_extractor import _extract_functions_lua
|
|
224
|
+
# Function should exist and be callable
|
|
225
|
+
assert callable(_extract_functions_lua)
|
|
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
|