redup 0.3.1__tar.gz → 0.3.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.
Files changed (40) hide show
  1. {redup-0.3.1 → redup-0.3.2}/PKG-INFO +50 -2
  2. {redup-0.3.1 → redup-0.3.2}/README.md +49 -1
  3. {redup-0.3.1 → redup-0.3.2}/pyproject.toml +1 -1
  4. {redup-0.3.1 → redup-0.3.2}/src/redup/__init__.py +1 -1
  5. {redup-0.3.1 → redup-0.3.2}/src/redup/cli_app/main.py +62 -47
  6. {redup-0.3.1 → redup-0.3.2}/src/redup/core/config.py +3 -3
  7. {redup-0.3.1 → redup-0.3.2}/src/redup/core/ts_extractor.py +151 -11
  8. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/PKG-INFO +50 -2
  9. {redup-0.3.1 → redup-0.3.2}/LICENSE +0 -0
  10. {redup-0.3.1 → redup-0.3.2}/setup.cfg +0 -0
  11. {redup-0.3.1 → redup-0.3.2}/src/redup/__main__.py +0 -0
  12. {redup-0.3.1 → redup-0.3.2}/src/redup/cli_app/__init__.py +0 -0
  13. {redup-0.3.1 → redup-0.3.2}/src/redup/core/__init__.py +0 -0
  14. {redup-0.3.1 → redup-0.3.2}/src/redup/core/differ.py +0 -0
  15. {redup-0.3.1 → redup-0.3.2}/src/redup/core/hasher.py +0 -0
  16. {redup-0.3.1 → redup-0.3.2}/src/redup/core/lsh_matcher.py +0 -0
  17. {redup-0.3.1 → redup-0.3.2}/src/redup/core/matcher.py +0 -0
  18. {redup-0.3.1 → redup-0.3.2}/src/redup/core/models.py +0 -0
  19. {redup-0.3.1 → redup-0.3.2}/src/redup/core/parallel_scanner.py +0 -0
  20. {redup-0.3.1 → redup-0.3.2}/src/redup/core/pipeline.py +0 -0
  21. {redup-0.3.1 → redup-0.3.2}/src/redup/core/planner.py +0 -0
  22. {redup-0.3.1 → redup-0.3.2}/src/redup/core/scanner.py +0 -0
  23. {redup-0.3.1 → redup-0.3.2}/src/redup/reporters/__init__.py +0 -0
  24. {redup-0.3.1 → redup-0.3.2}/src/redup/reporters/json_reporter.py +0 -0
  25. {redup-0.3.1 → redup-0.3.2}/src/redup/reporters/markdown_reporter.py +0 -0
  26. {redup-0.3.1 → redup-0.3.2}/src/redup/reporters/toon_reporter.py +0 -0
  27. {redup-0.3.1 → redup-0.3.2}/src/redup/reporters/yaml_reporter.py +0 -0
  28. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/SOURCES.txt +0 -0
  29. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/dependency_links.txt +0 -0
  30. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/entry_points.txt +0 -0
  31. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/requires.txt +0 -0
  32. {redup-0.3.1 → redup-0.3.2}/src/redup.egg-info/top_level.txt +0 -0
  33. {redup-0.3.1 → redup-0.3.2}/tests/test_e2e.py +0 -0
  34. {redup-0.3.1 → redup-0.3.2}/tests/test_hasher.py +0 -0
  35. {redup-0.3.1 → redup-0.3.2}/tests/test_matcher.py +0 -0
  36. {redup-0.3.1 → redup-0.3.2}/tests/test_models.py +0 -0
  37. {redup-0.3.1 → redup-0.3.2}/tests/test_pipeline.py +0 -0
  38. {redup-0.3.1 → redup-0.3.2}/tests/test_planner.py +0 -0
  39. {redup-0.3.1 → redup-0.3.2}/tests/test_reporters.py +0 -0
  40. {redup-0.3.1 → redup-0.3.2}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -75,7 +75,7 @@ Dynamic: license-file
75
75
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
76
76
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
77
77
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
78
- [![Version](https://img.shields.io/badge/version-0.3.1-green.svg)](https://pypi.org/project/redup/)
78
+ [![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](https://pypi.org/project/redup/)
79
79
 
80
80
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
81
81
 
@@ -122,6 +122,23 @@ redup scan .
122
122
  # Scan with JSON output saved to file
123
123
  redup scan ./src --format json --output ./reports/
124
124
 
125
+ # Parallel scanning for large projects
126
+ redup scan . --parallel --max-workers 4
127
+
128
+ # Multi-language scanning
129
+ redup scan . --ext ".py,.js,.ts,.go"
130
+
131
+ # CI gate with thresholds
132
+ redup check . --max-groups 10 --max-lines 100
133
+
134
+ # Compare two scans
135
+ redup diff before.json after.json
136
+
137
+ # Initialize configuration
138
+ redup config --init
139
+ ```
140
+
141
+ ```bash
125
142
  # Scan with all formats
126
143
  redup scan . --format all --output ./redup_output/
127
144
 
@@ -135,6 +152,37 @@ redup scan . --min-lines 5 --min-sim 0.9
135
152
  redup info
136
153
  ```
137
154
 
155
+ ### Configuration
156
+
157
+ Create a `redup.toml` file:
158
+
159
+ ```toml
160
+ [scan]
161
+ extensions = ".py,.js,.ts,.go,.rs,.java"
162
+ min_lines = 3
163
+ min_similarity = 0.85
164
+ include_tests = false
165
+
166
+ [lsh]
167
+ enabled = true
168
+ min_lines = 50
169
+ threshold = 0.8
170
+
171
+ [check]
172
+ max_groups = 10
173
+ max_lines = 100
174
+
175
+ [output]
176
+ format = "toon"
177
+ output = "redup_output"
178
+
179
+ [reporting]
180
+ include_snippets = true
181
+ generate_suggestions = true
182
+ ```
183
+
184
+ Or use `[tool.redup]` in `pyproject.toml`. Environment variables with `REDUP_` prefix override file settings.
185
+
138
186
  ### Python API
139
187
 
140
188
  ```python
@@ -5,7 +5,7 @@
5
5
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
8
- [![Version](https://img.shields.io/badge/version-0.3.1-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](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
 
@@ -52,6 +52,23 @@ redup scan .
52
52
  # Scan with JSON output saved to file
53
53
  redup scan ./src --format json --output ./reports/
54
54
 
55
+ # Parallel scanning for large projects
56
+ redup scan . --parallel --max-workers 4
57
+
58
+ # Multi-language scanning
59
+ redup scan . --ext ".py,.js,.ts,.go"
60
+
61
+ # CI gate with thresholds
62
+ redup check . --max-groups 10 --max-lines 100
63
+
64
+ # Compare two scans
65
+ redup diff before.json after.json
66
+
67
+ # Initialize configuration
68
+ redup config --init
69
+ ```
70
+
71
+ ```bash
55
72
  # Scan with all formats
56
73
  redup scan . --format all --output ./redup_output/
57
74
 
@@ -65,6 +82,37 @@ redup scan . --min-lines 5 --min-sim 0.9
65
82
  redup info
66
83
  ```
67
84
 
85
+ ### Configuration
86
+
87
+ Create a `redup.toml` file:
88
+
89
+ ```toml
90
+ [scan]
91
+ extensions = ".py,.js,.ts,.go,.rs,.java"
92
+ min_lines = 3
93
+ min_similarity = 0.85
94
+ include_tests = false
95
+
96
+ [lsh]
97
+ enabled = true
98
+ min_lines = 50
99
+ threshold = 0.8
100
+
101
+ [check]
102
+ max_groups = 10
103
+ max_lines = 100
104
+
105
+ [output]
106
+ format = "toon"
107
+ output = "redup_output"
108
+
109
+ [reporting]
110
+ include_snippets = true
111
+ generate_suggestions = true
112
+ ```
113
+
114
+ Or use `[tool.redup]` in `pyproject.toml`. Environment variables with `REDUP_` prefix override file settings.
115
+
68
116
  ### Python API
69
117
 
70
118
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.1"
5
+ __version__ = "0.3.2"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -28,7 +28,7 @@ app = typer.Typer(
28
28
  )
29
29
 
30
30
 
31
- OutputFormat = Literal["json", "yaml", "toon", "all"]
31
+ OutputFormat = Literal["json", "yaml", "toon", "markdown", "all"]
32
32
 
33
33
 
34
34
  DEFAULT_PATH = Path(".")
@@ -92,14 +92,12 @@ def _build_config_with_file_support(
92
92
 
93
93
  def _print_scan_header(
94
94
  path: Path,
95
- ext_list: list[str],
96
- min_lines: int,
97
- min_similarity: float,
95
+ config: ScanConfig,
98
96
  ) -> None:
99
97
  """Print scan configuration header."""
100
98
  typer.echo(f"reDUP v{redup.__version__} — scanning {path.resolve()}")
101
- typer.echo(f" extensions: {', '.join(ext_list)}")
102
- typer.echo(f" min_lines: {min_lines}, min_similarity: {min_similarity}")
99
+ typer.echo(f" extensions: {', '.join(config.extensions)}")
100
+ typer.echo(f" min_lines: {config.min_block_lines}, min_similarity: {config.min_similarity}")
103
101
  typer.echo("")
104
102
 
105
103
 
@@ -172,25 +170,25 @@ def scan(
172
170
  "--output", "-o",
173
171
  help="Output directory or file path. Defaults to stdout.",
174
172
  ),
175
- extensions: str = typer.Option(
176
- ".py",
173
+ extensions: str | None = typer.Option(
174
+ None,
177
175
  "--ext", "-e",
178
- help="Comma-separated file extensions to scan.",
176
+ help="Comma-separated file extensions to scan. Overrides config.",
179
177
  ),
180
- min_lines: int = typer.Option(
181
- 3,
178
+ min_lines: int | None = typer.Option(
179
+ None,
182
180
  "--min-lines",
183
- help="Minimum block size (lines) to consider as duplicate.",
181
+ help="Minimum block size (lines). Overrides config.",
184
182
  ),
185
- min_similarity: float = typer.Option(
186
- 0.85,
183
+ min_similarity: float | None = typer.Option(
184
+ None,
187
185
  "--min-sim",
188
- help="Minimum similarity score (0.0-1.0) for fuzzy matches.",
186
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
189
187
  ),
190
- include_tests: bool = typer.Option(
191
- False,
188
+ include_tests: bool | None = typer.Option(
189
+ None,
192
190
  "--include-tests",
193
- help="Include test files in analysis.",
191
+ help="Include test files. Overrides config.",
194
192
  ),
195
193
  functions_only: bool = typer.Option(
196
194
  False,
@@ -209,9 +207,9 @@ def scan(
209
207
  ),
210
208
  ) -> None:
211
209
  """Scan a project for code duplicates and generate a refactoring map."""
212
- config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
210
+ config = _build_config_with_file_support(path, extensions, min_lines, min_similarity, include_tests)
213
211
 
214
- _print_scan_header(path, config.extensions, min_lines, min_similarity)
212
+ _print_scan_header(path, config)
215
213
 
216
214
  # Choose analysis method
217
215
  if parallel:
@@ -270,61 +268,78 @@ def check(
270
268
  dir_okay=True,
271
269
  file_okay=False,
272
270
  ),
273
- max_groups: int = typer.Option(
274
- 10,
271
+ max_groups: int | None = typer.Option(
272
+ None,
275
273
  "--max-groups",
276
- help="Maximum allowed duplicate groups (default: 10).",
274
+ help="Maximum allowed duplicate groups. Overrides config.",
277
275
  ),
278
- max_saved_lines: int = typer.Option(
279
- 100,
276
+ max_saved_lines: int | None = typer.Option(
277
+ None,
280
278
  "--max-lines",
281
- help="Maximum allowed recoverable lines (default: 100).",
279
+ help="Maximum allowed recoverable lines. Overrides config.",
282
280
  ),
283
- extensions: str = typer.Option(
284
- ".py",
281
+ extensions: str | None = typer.Option(
282
+ None,
285
283
  "--ext", "-e",
286
- help="Comma-separated file extensions to scan.",
284
+ help="Comma-separated file extensions to scan. Overrides config.",
287
285
  ),
288
- min_lines: int = typer.Option(
289
- 3,
286
+ min_lines: int | None = typer.Option(
287
+ None,
290
288
  "--min-lines",
291
- help="Minimum block size (lines) to consider as duplicate.",
289
+ help="Minimum block size (lines). Overrides config.",
292
290
  ),
293
- min_similarity: float = typer.Option(
294
- 0.85,
291
+ min_similarity: float | None = typer.Option(
292
+ None,
295
293
  "--min-sim",
296
- help="Minimum similarity score (0.0-1.0) for fuzzy matches.",
294
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
297
295
  ),
298
- include_tests: bool = typer.Option(
299
- False,
296
+ include_tests: bool | None = typer.Option(
297
+ None,
300
298
  "--include-tests",
301
- help="Include test files in analysis.",
299
+ help="Include test files. Overrides config.",
302
300
  ),
303
301
  ) -> None:
304
302
  """Check project for duplicates and exit with non-zero code if thresholds exceeded."""
305
- config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
303
+ file_config = load_config()
304
+
305
+ # Override with CLI arguments
306
+ if extensions is not None:
307
+ file_config["extensions"] = extensions
308
+ if min_lines is not None:
309
+ file_config["min_lines"] = min_lines
310
+ if min_similarity is not None:
311
+ file_config["min_similarity"] = min_similarity
312
+ if include_tests is not None:
313
+ file_config["include_tests"] = include_tests
314
+
315
+ config = config_to_scan_config(file_config, path)
316
+
317
+ # Get thresholds from config or CLI
318
+ check_config = file_config.get("check", {})
319
+ threshold_groups = max_groups if max_groups is not None else check_config.get("max_groups", 10)
320
+ threshold_lines = max_saved_lines if max_saved_lines is not None else check_config.get("max_lines", 100)
306
321
 
307
322
  dup_map = analyze(config=config, function_level_only=True)
308
323
 
309
324
  # Check thresholds
310
- groups_exceeded = dup_map.total_groups > max_groups
311
- lines_exceeded = dup_map.total_saved_lines > max_saved_lines
325
+ groups_exceeded = dup_map.total_groups > threshold_groups
326
+ lines_exceeded = dup_map.total_saved_lines > threshold_lines
312
327
 
313
328
  if groups_exceeded or lines_exceeded:
314
329
  typer.echo(f"❌ reDUP check FAILED - thresholds exceeded:", err=True)
315
- typer.echo(f" Duplicate groups: {dup_map.total_groups} (max: {max_groups})", err=True)
316
- typer.echo(f" Recoverable lines: {dup_map.total_saved_lines} (max: {max_saved_lines})", err=True)
330
+ typer.echo(f" Duplicate groups: {dup_map.total_groups} (max: {threshold_groups})", err=True)
331
+ typer.echo(f" Recoverable lines: {dup_map.total_saved_lines} (max: {threshold_lines})", err=True)
317
332
 
318
333
  if groups_exceeded:
319
- typer.echo(f" ❌ Too many duplicate groups ({dup_map.total_groups} > {max_groups})", err=True)
334
+ typer.echo(f" ❌ Too many duplicate groups ({dup_map.total_groups} > {threshold_groups})", err=True)
320
335
  if lines_exceeded:
321
- typer.echo(f" ❌ Too many duplicate lines ({dup_map.total_saved_lines} > {max_saved_lines})", err=True)
336
+ typer.echo(f" ❌ Too many duplicate lines ({dup_map.total_saved_lines} > {threshold_lines})", err=True)
322
337
 
323
338
  raise typer.Exit(1)
324
339
  else:
325
340
  typer.echo(f"✅ reDUP check PASSED")
326
- typer.echo(f" Duplicate groups: {dup_map.total_groups}/{max_groups}")
327
- typer.echo(f" Recoverable lines: {dup_map.total_saved_lines}/{max_saved_lines}")
341
+ typer.echo(f" Duplicate groups: {dup_map.total_groups}/{threshold_groups}")
342
+ typer.echo(f" Recoverable lines: {dup_map.total_saved_lines}/{threshold_lines}")
328
343
  # Don't exit - just return normally (exit code 0)
329
344
  return
330
345
 
@@ -103,9 +103,9 @@ def config_to_scan_config(config: dict[str, Any], path: Path) -> ScanConfig:
103
103
  return ScanConfig(
104
104
  root=path,
105
105
  extensions=ext_list,
106
- min_block_lines=scan_config.get("min_lines", 3),
107
- min_similarity=scan_config.get("min_similarity", 0.85),
108
- include_tests=scan_config.get("include_tests", False),
106
+ min_block_lines=config.get("min_lines", scan_config.get("min_lines", 3)),
107
+ min_similarity=config.get("min_similarity", scan_config.get("min_similarity", 0.85)),
108
+ include_tests=config.get("include_tests", scan_config.get("include_tests", False)),
109
109
  lsh_enabled=lsh_config.get("enabled", True),
110
110
  lsh_min_lines=lsh_config.get("min_lines", 50),
111
111
  lsh_threshold=lsh_config.get("threshold", 0.8),
@@ -15,12 +15,30 @@ from redup.core.scanner import CodeBlock
15
15
 
16
16
  # Language mappings for tree-sitter
17
17
  _LANGUAGE_MAPPING = {
18
+ # Web/JavaScript
18
19
  ".js": "javascript",
19
- ".jsx": "javascript",
20
+ ".jsx": "javascript",
20
21
  ".ts": "typescript",
21
22
  ".tsx": "typescript",
23
+ ".mjs": "javascript",
24
+ ".cjs": "javascript",
25
+ ".mts": "typescript",
26
+ # HTML/XML
27
+ ".html": "html",
28
+ ".htm": "html",
29
+ ".xhtml": "html",
30
+ ".xml": "xml",
31
+ ".svg": "xml",
32
+ # CSS
33
+ ".css": "css",
34
+ ".scss": "css",
35
+ ".sass": "css",
36
+ ".less": "css",
37
+ # Go
22
38
  ".go": "go",
39
+ # Rust
23
40
  ".rs": "rust",
41
+ # Java/C-family
24
42
  ".java": "java",
25
43
  ".c": "c",
26
44
  ".cpp": "cpp",
@@ -28,7 +46,57 @@ _LANGUAGE_MAPPING = {
28
46
  ".cxx": "cpp",
29
47
  ".h": "c",
30
48
  ".hpp": "cpp",
31
- ".py": "python", # Fallback to AST
49
+ ".cs": "c_sharp",
50
+ ".scala": "scala",
51
+ ".kt": "kotlin",
52
+ # Swift/Objective-C
53
+ ".swift": "swift",
54
+ ".m": "objc",
55
+ ".mm": "objc",
56
+ # Python (fallback to AST)
57
+ ".py": "python",
58
+ ".pyw": "python",
59
+ # Ruby
60
+ ".rb": "ruby",
61
+ ".rbw": "ruby",
62
+ ".rake": "ruby",
63
+ ".gemspec": "ruby",
64
+ # PHP
65
+ ".php": "php",
66
+ ".phtml": "php",
67
+ ".php3": "php",
68
+ ".php4": "php",
69
+ ".php5": "php",
70
+ ".phps": "php",
71
+ # SQL
72
+ ".sql": "sql",
73
+ ".ddl": "sql",
74
+ ".dml": "sql",
75
+ # Data formats
76
+ ".json": "json",
77
+ ".json5": "json",
78
+ ".jsonl": "json",
79
+ ".yaml": "yaml",
80
+ ".yml": "yaml",
81
+ ".toml": "toml",
82
+ # Markdown
83
+ ".md": "markdown",
84
+ ".markdown": "markdown",
85
+ ".mdown": "markdown",
86
+ ".mkd": "markdown",
87
+ ".mkdown": "markdown",
88
+ # Shell
89
+ ".sh": "bash",
90
+ ".bash": "bash",
91
+ ".zsh": "bash",
92
+ ".fish": "bash",
93
+ # Templates
94
+ ".erb": "embedded_template",
95
+ ".ejs": "embedded_template",
96
+ ".hbs": "embedded_template",
97
+ ".handlebars": "embedded_template",
98
+ # Regex
99
+ ".regex": "regex",
32
100
  }
33
101
 
34
102
 
@@ -40,25 +108,79 @@ def _get_tree_sitter_language(language_name: str) -> Any:
40
108
  try:
41
109
  if language_name == "javascript":
42
110
  import tree_sitter_javascript
43
- return tree_sitter_javascript.language()
111
+ return tree_sitter.Language(tree_sitter_javascript.language())
44
112
  elif language_name == "typescript":
45
113
  import tree_sitter_typescript
46
- return tree_sitter_typescript.language_typescript()
114
+ return tree_sitter.Language(tree_sitter_typescript.language_typescript())
115
+ elif language_name == "html":
116
+ import tree_sitter_html
117
+ return tree_sitter.Language(tree_sitter_html.language())
118
+ elif language_name == "xml":
119
+ import tree_sitter_xml
120
+ return tree_sitter.Language(tree_sitter_xml.language())
121
+ elif language_name == "css":
122
+ import tree_sitter_css
123
+ return tree_sitter.Language(tree_sitter_css.language())
47
124
  elif language_name == "go":
48
125
  import tree_sitter_go
49
- return tree_sitter_go.language()
126
+ return tree_sitter.Language(tree_sitter_go.language())
50
127
  elif language_name == "rust":
51
128
  import tree_sitter_rust
52
- return tree_sitter_rust.language()
129
+ return tree_sitter.Language(tree_sitter_rust.language())
53
130
  elif language_name == "java":
54
131
  import tree_sitter_java
55
- return tree_sitter_java.language()
132
+ return tree_sitter.Language(tree_sitter_java.language())
56
133
  elif language_name == "c":
57
134
  import tree_sitter_c
58
- return tree_sitter_c.language()
135
+ return tree_sitter.Language(tree_sitter_c.language())
59
136
  elif language_name == "cpp":
60
137
  import tree_sitter_cpp
61
- return tree_sitter_cpp.language()
138
+ return tree_sitter.Language(tree_sitter_cpp.language())
139
+ elif language_name == "c_sharp":
140
+ import tree_sitter_c_sharp
141
+ return tree_sitter.Language(tree_sitter_c_sharp.language())
142
+ elif language_name == "scala":
143
+ import tree_sitter_scala
144
+ return tree_sitter.Language(tree_sitter_scala.language())
145
+ elif language_name == "kotlin":
146
+ import tree_sitter_kotlin
147
+ return tree_sitter.Language(tree_sitter_kotlin.language())
148
+ elif language_name == "swift":
149
+ import tree_sitter_swift
150
+ return tree_sitter.Language(tree_sitter_swift.language())
151
+ elif language_name == "objc":
152
+ import tree_sitter_objc
153
+ return tree_sitter.Language(tree_sitter_objc.language())
154
+ elif language_name == "ruby":
155
+ import tree_sitter_ruby
156
+ return tree_sitter.Language(tree_sitter_ruby.language())
157
+ elif language_name == "php":
158
+ import tree_sitter_php
159
+ return tree_sitter.Language(tree_sitter_php.language())
160
+ elif language_name == "sql":
161
+ import tree_sitter_sql
162
+ return tree_sitter.Language(tree_sitter_sql.language())
163
+ elif language_name == "json":
164
+ import tree_sitter_json
165
+ return tree_sitter.Language(tree_sitter_json.language())
166
+ elif language_name == "yaml":
167
+ import tree_sitter_yaml
168
+ return tree_sitter.Language(tree_sitter_yaml.language())
169
+ elif language_name == "toml":
170
+ import tree_sitter_toml
171
+ return tree_sitter.Language(tree_sitter_toml.language())
172
+ elif language_name == "markdown":
173
+ import tree_sitter_markdown
174
+ return tree_sitter.Language(tree_sitter_markdown.language())
175
+ elif language_name == "bash":
176
+ import tree_sitter_bash
177
+ return tree_sitter.Language(tree_sitter_bash.language())
178
+ elif language_name == "embedded_template":
179
+ import tree_sitter_embedded_template
180
+ return tree_sitter.Language(tree_sitter_embedded_template.language())
181
+ elif language_name == "regex":
182
+ import tree_sitter_regex
183
+ return tree_sitter.Language(tree_sitter_regex.language())
62
184
  else:
63
185
  return None
64
186
  except ImportError:
@@ -302,8 +424,7 @@ def extract_functions_treesitter(source: str, file_path: str) -> list[CodeBlock]
302
424
  return []
303
425
 
304
426
  # Parse the source code
305
- parser = tree_sitter.Parser()
306
- parser.set_language(language)
427
+ parser = tree_sitter.Parser(language)
307
428
 
308
429
  try:
309
430
  tree = parser.parse(bytes(source, "utf8"))
@@ -321,10 +442,29 @@ def extract_functions_treesitter(source: str, file_path: str) -> list[CodeBlock]
321
442
  return _extract_functions_rust(tree.root_node, source_lines, file_path)
322
443
  elif language_name == "java":
323
444
  return _extract_functions_java(tree.root_node, source_lines, file_path)
445
+ elif language_name == "c_sharp":
446
+ return _extract_functions_c_sharp(tree.root_node, source_lines, file_path)
447
+ elif language_name == "scala":
448
+ return _extract_functions_scala(tree.root_node, source_lines, file_path)
449
+ elif language_name == "kotlin":
450
+ return _extract_functions_kotlin(tree.root_node, source_lines, file_path)
451
+ elif language_name == "swift":
452
+ return _extract_functions_swift(tree.root_node, source_lines, file_path)
453
+ elif language_name == "objc":
454
+ return _extract_functions_objc(tree.root_node, source_lines, file_path)
455
+ elif language_name == "ruby":
456
+ return _extract_functions_ruby(tree.root_node, source_lines, file_path)
457
+ elif language_name == "php":
458
+ return _extract_functions_php(tree.root_node, source_lines, file_path)
459
+ elif language_name == "bash":
460
+ return _extract_functions_bash(tree.root_node, source_lines, file_path)
324
461
  elif language_name in ("c", "cpp"):
325
462
  # C/C++ function extraction would be more complex
326
463
  # For now, return empty list
327
464
  return []
465
+ elif language_name in ("html", "xml", "css", "json", "yaml", "toml", "markdown", "embedded_template", "regex", "sql"):
466
+ # Data formats and markup languages don't have functions in the traditional sense
467
+ return []
328
468
 
329
469
  return []
330
470
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.3.1
3
+ Version: 0.3.2
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
@@ -75,7 +75,7 @@ Dynamic: license-file
75
75
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
76
76
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
77
77
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
78
- [![Version](https://img.shields.io/badge/version-0.3.1-green.svg)](https://pypi.org/project/redup/)
78
+ [![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](https://pypi.org/project/redup/)
79
79
 
80
80
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
81
81
 
@@ -122,6 +122,23 @@ redup scan .
122
122
  # Scan with JSON output saved to file
123
123
  redup scan ./src --format json --output ./reports/
124
124
 
125
+ # Parallel scanning for large projects
126
+ redup scan . --parallel --max-workers 4
127
+
128
+ # Multi-language scanning
129
+ redup scan . --ext ".py,.js,.ts,.go"
130
+
131
+ # CI gate with thresholds
132
+ redup check . --max-groups 10 --max-lines 100
133
+
134
+ # Compare two scans
135
+ redup diff before.json after.json
136
+
137
+ # Initialize configuration
138
+ redup config --init
139
+ ```
140
+
141
+ ```bash
125
142
  # Scan with all formats
126
143
  redup scan . --format all --output ./redup_output/
127
144
 
@@ -135,6 +152,37 @@ redup scan . --min-lines 5 --min-sim 0.9
135
152
  redup info
136
153
  ```
137
154
 
155
+ ### Configuration
156
+
157
+ Create a `redup.toml` file:
158
+
159
+ ```toml
160
+ [scan]
161
+ extensions = ".py,.js,.ts,.go,.rs,.java"
162
+ min_lines = 3
163
+ min_similarity = 0.85
164
+ include_tests = false
165
+
166
+ [lsh]
167
+ enabled = true
168
+ min_lines = 50
169
+ threshold = 0.8
170
+
171
+ [check]
172
+ max_groups = 10
173
+ max_lines = 100
174
+
175
+ [output]
176
+ format = "toon"
177
+ output = "redup_output"
178
+
179
+ [reporting]
180
+ include_snippets = true
181
+ generate_suggestions = true
182
+ ```
183
+
184
+ Or use `[tool.redup]` in `pyproject.toml`. Environment variables with `REDUP_` prefix override file settings.
185
+
138
186
  ### Python API
139
187
 
140
188
  ```python
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