redup 0.3.6__tar.gz → 0.3.8__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.6 → redup-0.3.8}/PKG-INFO +7 -7
- {redup-0.3.6 → redup-0.3.8}/README.md +6 -6
- {redup-0.3.6 → redup-0.3.8}/pyproject.toml +1 -1
- {redup-0.3.6 → redup-0.3.8}/src/redup/__init__.py +1 -1
- {redup-0.3.6 → redup-0.3.8}/src/redup/cli_app/main.py +15 -2
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/scanner.py +43 -1
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/ts_extractor.py +3 -30
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/utils/function_extractor.py +5 -0
- redup-0.3.8/src/redup/reporters/code2llm_reporter.py +209 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/PKG-INFO +7 -7
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/SOURCES.txt +3 -1
- redup-0.3.8/tests/test_ts_extractor.py +225 -0
- {redup-0.3.6 → redup-0.3.8}/LICENSE +0 -0
- {redup-0.3.6 → redup-0.3.8}/setup.cfg +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/__main__.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/__init__.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/config.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/differ.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/hasher.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/matcher.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/models.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/parallel_scanner.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/pipeline.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/planner.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/__init__.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/json_reporter.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/markdown_reporter.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/toon_reporter.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/requires.txt +0 -0
- {redup-0.3.6 → redup-0.3.8}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_e2e.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_hasher.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_matcher.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_models.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_pipeline.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_planner.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_reporters.py +0 -0
- {redup-0.3.6 → redup-0.3.8}/tests/test_scanner.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.8
|
|
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,
|
|
@@ -106,6 +106,46 @@ def _extract_blocks_sliding(lines: list[str], min_lines: int) -> list[tuple[int,
|
|
|
106
106
|
return blocks
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _extract_class_blocks_python(source: str, filepath: str) -> list[CodeBlock]:
|
|
110
|
+
"""Extract class-level blocks from Python source using ast.
|
|
111
|
+
|
|
112
|
+
This enables detection of duplicate classes across files.
|
|
113
|
+
"""
|
|
114
|
+
import ast
|
|
115
|
+
|
|
116
|
+
blocks: list[CodeBlock] = []
|
|
117
|
+
try:
|
|
118
|
+
tree = ast.parse(source)
|
|
119
|
+
except SyntaxError:
|
|
120
|
+
return blocks
|
|
121
|
+
|
|
122
|
+
for node in ast.walk(tree):
|
|
123
|
+
if isinstance(node, ast.ClassDef):
|
|
124
|
+
start = node.lineno
|
|
125
|
+
end = node.end_lineno or start
|
|
126
|
+
lines = source.splitlines()
|
|
127
|
+
text = "\n".join(lines[start - 1 : end])
|
|
128
|
+
|
|
129
|
+
# Get method names for context
|
|
130
|
+
method_names = [
|
|
131
|
+
n.name for n in ast.walk(node)
|
|
132
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
blocks.append(
|
|
136
|
+
CodeBlock(
|
|
137
|
+
file=filepath,
|
|
138
|
+
line_start=start,
|
|
139
|
+
line_end=end,
|
|
140
|
+
text=text,
|
|
141
|
+
function_name=None, # It's a class, not a function
|
|
142
|
+
class_name=node.name,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return blocks
|
|
147
|
+
|
|
148
|
+
|
|
109
149
|
def _extract_function_blocks_python(source: str, filepath: str) -> list[CodeBlock]:
|
|
110
150
|
"""Extract function/method-level blocks from Python source using ast."""
|
|
111
151
|
import ast
|
|
@@ -171,6 +211,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
171
211
|
# Extract function-level blocks for Python and other supported languages
|
|
172
212
|
if filepath.suffix == ".py":
|
|
173
213
|
func_blocks = _extract_function_blocks_python(source, rel_path)
|
|
214
|
+
class_blocks = _extract_class_blocks_python(source, rel_path)
|
|
174
215
|
else:
|
|
175
216
|
# Try tree-sitter extraction for other languages
|
|
176
217
|
try:
|
|
@@ -181,6 +222,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
181
222
|
func_blocks = []
|
|
182
223
|
except ImportError:
|
|
183
224
|
func_blocks = []
|
|
225
|
+
class_blocks = []
|
|
184
226
|
|
|
185
227
|
# Also extract sliding-window blocks for line-level matching
|
|
186
228
|
sliding = _extract_blocks_sliding(lines, config.min_block_lines)
|
|
@@ -189,7 +231,7 @@ def scan_project(config: ScanConfig | None = None) -> tuple[list[ScannedFile], S
|
|
|
189
231
|
for s, e, t in sliding
|
|
190
232
|
]
|
|
191
233
|
|
|
192
|
-
all_blocks = func_blocks + line_blocks
|
|
234
|
+
all_blocks = func_blocks + class_blocks + line_blocks
|
|
193
235
|
|
|
194
236
|
sf = ScannedFile(path=rel_path, lines=lines, blocks=all_blocks)
|
|
195
237
|
scanned.append(sf)
|
|
@@ -13,7 +13,8 @@ except ImportError:
|
|
|
13
13
|
from redup.core.scanner import CodeBlock
|
|
14
14
|
from redup.core.utils.function_extractor import (
|
|
15
15
|
GO_EXTRACTOR, RUST_EXTRACTOR, JAVA_EXTRACTOR,
|
|
16
|
-
SCALA_EXTRACTOR, KOTLIN_EXTRACTOR, SWIFT_EXTRACTOR, OBJC_EXTRACTOR
|
|
16
|
+
SCALA_EXTRACTOR, KOTLIN_EXTRACTOR, SWIFT_EXTRACTOR, OBJC_EXTRACTOR,
|
|
17
|
+
LUA_EXTRACTOR
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -513,35 +514,7 @@ def _extract_blocks_sql(node: Any, source_lines: list[str], file_path: str) -> l
|
|
|
513
514
|
|
|
514
515
|
def _extract_functions_lua(node: Any, source_lines: list[str], file_path: str) -> list[CodeBlock]:
|
|
515
516
|
"""Extract functions from Lua using tree-sitter."""
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def traverse(node: Any, depth: int = 0) -> None:
|
|
519
|
-
if depth > 50:
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
node_type = node.type
|
|
523
|
-
|
|
524
|
-
# Function declarations (local function, function)
|
|
525
|
-
if node_type in ("function_declaration", "local_function"):
|
|
526
|
-
start_line = node.start_point[0] + 1
|
|
527
|
-
end_line = node.end_point[0] + 1
|
|
528
|
-
|
|
529
|
-
name_node = node.child_by_field_name("name")
|
|
530
|
-
function_name = name_node.text.decode() if name_node else "anonymous"
|
|
531
|
-
|
|
532
|
-
blocks.append(CodeBlock(
|
|
533
|
-
file=file_path,
|
|
534
|
-
line_start=start_line,
|
|
535
|
-
line_end=end_line,
|
|
536
|
-
text="\n".join(source_lines[start_line-1:end_line]),
|
|
537
|
-
function_name=function_name,
|
|
538
|
-
class_name=None,
|
|
539
|
-
))
|
|
540
|
-
|
|
541
|
-
for child in node.children:
|
|
542
|
-
traverse(child, depth + 1)
|
|
543
|
-
|
|
544
|
-
return blocks
|
|
517
|
+
return LUA_EXTRACTOR.extract_functions(node, source_lines, file_path)
|
|
545
518
|
|
|
546
519
|
|
|
547
520
|
def _extract_functions_c_sharp(node: Any, source_lines: list[str], file_path: str) -> list[CodeBlock]:
|
|
@@ -141,3 +141,8 @@ OBJC_EXTRACTOR = FunctionExtractor(
|
|
|
141
141
|
function_node_types=["function_definition"],
|
|
142
142
|
method_node_types=["method_declaration"]
|
|
143
143
|
)
|
|
144
|
+
|
|
145
|
+
LUA_EXTRACTOR = FunctionExtractor(
|
|
146
|
+
function_node_types=["function_declaration", "local_function"],
|
|
147
|
+
method_node_types=[]
|
|
148
|
+
)
|
|
@@ -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.8
|
|
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
|
)
|
|
@@ -28,6 +28,7 @@ src/redup/core/utils/duplicate_finders.py
|
|
|
28
28
|
src/redup/core/utils/function_extractor.py
|
|
29
29
|
src/redup/core/utils/hash_utils.py
|
|
30
30
|
src/redup/reporters/__init__.py
|
|
31
|
+
src/redup/reporters/code2llm_reporter.py
|
|
31
32
|
src/redup/reporters/enhanced_reporter.py
|
|
32
33
|
src/redup/reporters/json_reporter.py
|
|
33
34
|
src/redup/reporters/markdown_reporter.py
|
|
@@ -40,4 +41,5 @@ tests/test_models.py
|
|
|
40
41
|
tests/test_pipeline.py
|
|
41
42
|
tests/test_planner.py
|
|
42
43
|
tests/test_reporters.py
|
|
43
|
-
tests/test_scanner.py
|
|
44
|
+
tests/test_scanner.py
|
|
45
|
+
tests/test_ts_extractor.py
|
|
@@ -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
|