redup 0.2.2__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {redup-0.2.2 → redup-0.3.1}/PKG-INFO +29 -7
  2. {redup-0.2.2 → redup-0.3.1}/README.md +9 -5
  3. {redup-0.2.2 → redup-0.3.1}/pyproject.toml +20 -2
  4. {redup-0.2.2 → redup-0.3.1}/src/redup/__init__.py +1 -1
  5. {redup-0.2.2 → redup-0.3.1}/src/redup/cli_app/main.py +105 -18
  6. redup-0.3.1/src/redup/core/config.py +154 -0
  7. {redup-0.2.2 → redup-0.3.1}/src/redup/core/hasher.py +4 -4
  8. redup-0.3.1/src/redup/core/lsh_matcher.py +217 -0
  9. {redup-0.2.2 → redup-0.3.1}/src/redup/core/models.py +5 -0
  10. redup-0.3.1/src/redup/core/parallel_scanner.py +185 -0
  11. {redup-0.2.2 → redup-0.3.1}/src/redup/core/pipeline.py +136 -1
  12. {redup-0.2.2 → redup-0.3.1}/src/redup/reporters/json_reporter.py +20 -13
  13. redup-0.3.1/src/redup/reporters/markdown_reporter.py +90 -0
  14. {redup-0.2.2 → redup-0.3.1}/src/redup.egg-info/PKG-INFO +29 -7
  15. {redup-0.2.2 → redup-0.3.1}/src/redup.egg-info/SOURCES.txt +4 -0
  16. redup-0.3.1/src/redup.egg-info/requires.txt +49 -0
  17. redup-0.2.2/src/redup.egg-info/requires.txt +0 -31
  18. {redup-0.2.2 → redup-0.3.1}/LICENSE +0 -0
  19. {redup-0.2.2 → redup-0.3.1}/setup.cfg +0 -0
  20. {redup-0.2.2 → redup-0.3.1}/src/redup/__main__.py +0 -0
  21. {redup-0.2.2 → redup-0.3.1}/src/redup/cli_app/__init__.py +0 -0
  22. {redup-0.2.2 → redup-0.3.1}/src/redup/core/__init__.py +0 -0
  23. {redup-0.2.2 → redup-0.3.1}/src/redup/core/differ.py +0 -0
  24. {redup-0.2.2 → redup-0.3.1}/src/redup/core/matcher.py +0 -0
  25. {redup-0.2.2 → redup-0.3.1}/src/redup/core/planner.py +0 -0
  26. {redup-0.2.2 → redup-0.3.1}/src/redup/core/scanner.py +0 -0
  27. {redup-0.2.2 → redup-0.3.1}/src/redup/core/ts_extractor.py +0 -0
  28. {redup-0.2.2 → redup-0.3.1}/src/redup/reporters/__init__.py +0 -0
  29. {redup-0.2.2 → redup-0.3.1}/src/redup/reporters/toon_reporter.py +0 -0
  30. {redup-0.2.2 → redup-0.3.1}/src/redup/reporters/yaml_reporter.py +0 -0
  31. {redup-0.2.2 → redup-0.3.1}/src/redup.egg-info/dependency_links.txt +0 -0
  32. {redup-0.2.2 → redup-0.3.1}/src/redup.egg-info/entry_points.txt +0 -0
  33. {redup-0.2.2 → redup-0.3.1}/src/redup.egg-info/top_level.txt +0 -0
  34. {redup-0.2.2 → redup-0.3.1}/tests/test_e2e.py +0 -0
  35. {redup-0.2.2 → redup-0.3.1}/tests/test_hasher.py +0 -0
  36. {redup-0.2.2 → redup-0.3.1}/tests/test_matcher.py +0 -0
  37. {redup-0.2.2 → redup-0.3.1}/tests/test_models.py +0 -0
  38. {redup-0.2.2 → redup-0.3.1}/tests/test_pipeline.py +0 -0
  39. {redup-0.2.2 → redup-0.3.1}/tests/test_planner.py +0 -0
  40. {redup-0.2.2 → redup-0.3.1}/tests/test_reporters.py +0 -0
  41. {redup-0.2.2 → redup-0.3.1}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Code duplication analyzer and refactoring planner for LLMs
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -23,7 +23,7 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pyyaml>=6.0
26
- Requires-Dist: typer>=0.9.0
26
+ Requires-Dist: typer>=0.12.0
27
27
  Requires-Dist: rich>=13.0
28
28
  Requires-Dist: pydantic>=2.0
29
29
  Provides-Extra: all
@@ -41,6 +41,24 @@ Requires-Dist: tree-sitter-rust>=0.20; extra == "ast"
41
41
  Requires-Dist: tree-sitter-java>=0.20; extra == "ast"
42
42
  Requires-Dist: tree-sitter-c>=0.20; extra == "ast"
43
43
  Requires-Dist: tree-sitter-cpp>=0.20; extra == "ast"
44
+ Requires-Dist: tree-sitter-html>=0.20; extra == "ast"
45
+ Requires-Dist: tree-sitter-css>=0.20; extra == "ast"
46
+ Requires-Dist: tree-sitter-sql>=0.20; extra == "ast"
47
+ Requires-Dist: tree-sitter-json>=0.20; extra == "ast"
48
+ Requires-Dist: tree-sitter-yaml>=0.20; extra == "ast"
49
+ Requires-Dist: tree-sitter-toml>=0.20; extra == "ast"
50
+ Requires-Dist: tree-sitter-xml>=0.20; extra == "ast"
51
+ Requires-Dist: tree-sitter-markdown>=0.20; extra == "ast"
52
+ Requires-Dist: tree-sitter-bash>=0.20; extra == "ast"
53
+ Requires-Dist: tree-sitter-ruby>=0.20; extra == "ast"
54
+ Requires-Dist: tree-sitter-php>=0.20; extra == "ast"
55
+ Requires-Dist: tree-sitter-embedded-template>=0.20; extra == "ast"
56
+ Requires-Dist: tree-sitter-regex>=0.20; extra == "ast"
57
+ Requires-Dist: tree-sitter-scala>=0.20; extra == "ast"
58
+ Requires-Dist: tree-sitter-kotlin>=0.20; extra == "ast"
59
+ Requires-Dist: tree-sitter-swift>=0.20; extra == "ast"
60
+ Requires-Dist: tree-sitter-objc>=0.20; extra == "ast"
61
+ Requires-Dist: tree-sitter-c-sharp>=0.20; extra == "ast"
44
62
  Provides-Extra: lsh
45
63
  Requires-Dist: datasketch>=1.6; extra == "lsh"
46
64
  Provides-Extra: dev
@@ -57,7 +75,7 @@ Dynamic: license-file
57
75
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
58
76
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
59
77
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
60
- [![Version](https://img.shields.io/badge/version-0.2.2-green.svg)](https://pypi.org/project/redup/)
78
+ [![Version](https://img.shields.io/badge/version-0.3.1-green.svg)](https://pypi.org/project/redup/)
61
79
 
62
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.
63
81
 
@@ -65,14 +83,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
65
83
 
66
84
  - **Exact duplicate detection** via SHA-256 block hashing
67
85
  - **Structural clone detection** — same AST shape, different variable names
86
+ - **LSH near-duplicate detection** for large code blocks (>50 lines)
87
+ - **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
88
+ - **Parallel scanning** for large projects (2x+ performance improvement)
68
89
  - **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
69
- - **Function-level analysis** using Python AST extraction
90
+ - **Function-level analysis** using Python AST and tree-sitter extraction
70
91
  - **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
71
92
  - **Refactoring planner** — generates concrete extract/inline suggestions
72
- - **Three output formats**: JSON (tooling), YAML (humans), TOON (LLMs)
73
- - **CLI** with `typer` + `rich` for interactive use
93
+ - **Multiple output formats**: JSON, YAML, TOON, Markdown
94
+ - **Configuration system** TOML files and environment variables
95
+ - **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
96
+ - **CI integration** with configurable quality gates
74
97
  - **Clean output** — no syntax warnings from external libraries
75
- - **Optimized performance** — reduced complexity and improved maintainability
76
98
 
77
99
  ## Installation
78
100
 
@@ -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.2.2-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.1-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
 
@@ -13,14 +13,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
13
13
 
14
14
  - **Exact duplicate detection** via SHA-256 block hashing
15
15
  - **Structural clone detection** — same AST shape, different variable names
16
+ - **LSH near-duplicate detection** for large code blocks (>50 lines)
17
+ - **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
18
+ - **Parallel scanning** for large projects (2x+ performance improvement)
16
19
  - **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
17
- - **Function-level analysis** using Python AST extraction
20
+ - **Function-level analysis** using Python AST and tree-sitter extraction
18
21
  - **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
19
22
  - **Refactoring planner** — generates concrete extract/inline suggestions
20
- - **Three output formats**: JSON (tooling), YAML (humans), TOON (LLMs)
21
- - **CLI** with `typer` + `rich` for interactive use
23
+ - **Multiple output formats**: JSON, YAML, TOON, Markdown
24
+ - **Configuration system** TOML files and environment variables
25
+ - **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
26
+ - **CI integration** with configurable quality gates
22
27
  - **Clean output** — no syntax warnings from external libraries
23
- - **Optimized performance** — reduced complexity and improved maintainability
24
28
 
25
29
  ## Installation
26
30
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.2.2"
7
+ version = "0.3.1"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "pyyaml>=6.0",
30
- "typer>=0.9.0",
30
+ "typer>=0.12.0",
31
31
  "rich>=13.0",
32
32
  "pydantic>=2.0",
33
33
  ]
@@ -48,6 +48,24 @@ ast = [
48
48
  "tree-sitter-java>=0.20",
49
49
  "tree-sitter-c>=0.20",
50
50
  "tree-sitter-cpp>=0.20",
51
+ "tree-sitter-html>=0.20",
52
+ "tree-sitter-css>=0.20",
53
+ "tree-sitter-sql>=0.20",
54
+ "tree-sitter-json>=0.20",
55
+ "tree-sitter-yaml>=0.20",
56
+ "tree-sitter-toml>=0.20",
57
+ "tree-sitter-xml>=0.20",
58
+ "tree-sitter-markdown>=0.20",
59
+ "tree-sitter-bash>=0.20",
60
+ "tree-sitter-ruby>=0.20",
61
+ "tree-sitter-php>=0.20",
62
+ "tree-sitter-embedded-template>=0.20",
63
+ "tree-sitter-regex>=0.20",
64
+ "tree-sitter-scala>=0.20",
65
+ "tree-sitter-kotlin>=0.20",
66
+ "tree-sitter-swift>=0.20",
67
+ "tree-sitter-objc>=0.20",
68
+ "tree-sitter-c-sharp>=0.20",
51
69
  ]
52
70
  lsh = ["datasketch>=1.6"]
53
71
  dev = [
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.3.1"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import warnings
6
6
  from collections.abc import Callable
7
- from enum import Enum
7
+ from typing import Literal
8
8
  from pathlib import Path
9
9
 
10
10
  import typer
@@ -13,9 +13,11 @@ import typer
13
13
  warnings.filterwarnings("ignore", category=SyntaxWarning)
14
14
 
15
15
  import redup # noqa: E402
16
+ from redup.core.config import config_to_scan_config, create_sample_redup_toml, load_config # noqa: E402
16
17
  from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
17
- from redup.core.pipeline import analyze # noqa: E402
18
+ from redup.core.pipeline import analyze, analyze_parallel # noqa: E402
18
19
  from redup.reporters.json_reporter import to_json # noqa: E402
20
+ from redup.reporters.markdown_reporter import to_markdown # noqa: E402
19
21
  from redup.reporters.toon_reporter import to_toon # noqa: E402
20
22
  from redup.reporters.yaml_reporter import to_yaml # noqa: E402
21
23
 
@@ -26,15 +28,11 @@ app = typer.Typer(
26
28
  )
27
29
 
28
30
 
29
- class OutputFormat(str, Enum):
30
- json = "json"
31
- yaml = "yaml"
32
- toon = "toon"
33
- all = "all"
31
+ OutputFormat = Literal["json", "yaml", "toon", "all"]
34
32
 
35
33
 
36
34
  DEFAULT_PATH = Path(".")
37
- DEFAULT_FORMAT = OutputFormat.toon
35
+ DEFAULT_FORMAT: OutputFormat = "toon"
38
36
  DEFAULT_OUTPUT: Path | None = None
39
37
 
40
38
 
@@ -68,6 +66,30 @@ def _build_config(
68
66
  )
69
67
 
70
68
 
69
+ def _build_config_with_file_support(
70
+ path: Path,
71
+ extensions: str | None,
72
+ min_lines: int | None,
73
+ min_similarity: float | None,
74
+ include_tests: bool | None
75
+ ) -> ScanConfig:
76
+ """Build scan configuration supporting config files and CLI overrides."""
77
+ # Load configuration from files
78
+ file_config = load_config()
79
+
80
+ # Use CLI arguments to override file config (if provided)
81
+ if extensions is not None:
82
+ file_config["extensions"] = extensions
83
+ if min_lines is not None:
84
+ file_config["min_lines"] = min_lines
85
+ if min_similarity is not None:
86
+ file_config["min_similarity"] = min_similarity
87
+ if include_tests is not None:
88
+ file_config["include_tests"] = include_tests
89
+
90
+ return config_to_scan_config(file_config, path)
91
+
92
+
71
93
  def _print_scan_header(
72
94
  path: Path,
73
95
  ext_list: list[str],
@@ -94,14 +116,14 @@ def _print_scan_summary(dup_map: DuplicationMap) -> None:
94
116
 
95
117
  def _write_results(
96
118
  dup_map: DuplicationMap,
97
- format: OutputFormat,
119
+ format: str,
98
120
  output: Path | None,
99
121
  path: Path,
100
122
  ) -> None:
101
123
  """Write scan results to output files."""
102
124
  output_dir = output
103
125
 
104
- if format == OutputFormat.all:
126
+ if format == "all":
105
127
  if output_dir is None:
106
128
  output_dir = path / "redup_output"
107
129
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -110,18 +132,25 @@ def _write_results(
110
132
  ("JSON", to_json, "json"),
111
133
  ("YAML", to_yaml, "yaml"),
112
134
  ("TOON", to_toon, "toon"),
135
+ ("Markdown", to_markdown, "md"),
113
136
  ]
114
137
  for _fmt, renderer, suffix in renderers:
115
138
  content = renderer(dup_map)
139
+ if suffix == "json":
140
+ include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
141
+ content = to_json(dup_map, include_snippets=include_snippets)
116
142
  target = output_dir / f"duplication.{suffix}"
117
143
  target.write_text(content, encoding="utf-8")
118
144
  typer.echo(f" → {target}")
119
- elif format == OutputFormat.json:
120
- _write_output(to_json(dup_map), output_dir, "json")
121
- elif format == OutputFormat.yaml:
145
+ elif format == "json":
146
+ include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
147
+ _write_output(to_json(dup_map, include_snippets=include_snippets), output_dir, "json")
148
+ elif format == "yaml":
122
149
  _write_output(to_yaml(dup_map), output_dir, "yaml")
123
- elif format == OutputFormat.toon:
150
+ elif format == "toon":
124
151
  _write_output(to_toon(dup_map), output_dir, "toon")
152
+ elif format == "markdown":
153
+ _write_output(to_markdown(dup_map), output_dir, "md")
125
154
 
126
155
 
127
156
  @app.command()
@@ -133,10 +162,10 @@ def scan(
133
162
  dir_okay=True,
134
163
  file_okay=False,
135
164
  ),
136
- format: OutputFormat = typer.Option(
137
- DEFAULT_FORMAT,
165
+ format: str = typer.Option(
166
+ "toon",
138
167
  "--format", "-f",
139
- help="Output format.",
168
+ help="Output format (json, yaml, toon, markdown, all).",
140
169
  ),
141
170
  output: Path | None = typer.Option(
142
171
  DEFAULT_OUTPUT,
@@ -168,13 +197,27 @@ def scan(
168
197
  "--functions-only",
169
198
  help="Only analyze function-level duplicates (faster).",
170
199
  ),
200
+ parallel: bool = typer.Option(
201
+ False,
202
+ "--parallel",
203
+ help="Use parallel scanning for large projects.",
204
+ ),
205
+ max_workers: int = typer.Option(
206
+ None,
207
+ "--max-workers",
208
+ help="Number of parallel workers (default: CPU count).",
209
+ ),
171
210
  ) -> None:
172
211
  """Scan a project for code duplicates and generate a refactoring map."""
173
212
  config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
174
213
 
175
214
  _print_scan_header(path, config.extensions, min_lines, min_similarity)
176
215
 
177
- dup_map = analyze(config=config, function_level_only=functions_only)
216
+ # Choose analysis method
217
+ if parallel:
218
+ dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
219
+ else:
220
+ dup_map = analyze(config=config, function_level_only=functions_only)
178
221
 
179
222
  _print_scan_summary(dup_map)
180
223
  _write_results(dup_map, format, output, path)
@@ -286,6 +329,50 @@ def check(
286
329
  return
287
330
 
288
331
 
332
+ @app.command()
333
+ def config(
334
+ init: bool = typer.Option(
335
+ False,
336
+ "--init",
337
+ help="Create a sample redup.toml configuration file.",
338
+ ),
339
+ show: bool = typer.Option(
340
+ False,
341
+ "--show",
342
+ help="Show current configuration (files + env vars).",
343
+ ),
344
+ ) -> None:
345
+ """Manage reDUP configuration."""
346
+ if init:
347
+ config_file = Path.cwd() / "redup.toml"
348
+ if config_file.exists():
349
+ typer.echo(f"❌ Configuration file already exists: {config_file}")
350
+ raise typer.Exit(1)
351
+
352
+ content = create_sample_redup_toml()
353
+ config_file.write_text(content, encoding="utf-8")
354
+ typer.echo(f"✅ Created sample configuration: {config_file}")
355
+ typer.echo("Edit the file to customize reDUP settings.")
356
+ return
357
+
358
+ if show:
359
+ config = load_config()
360
+ typer.echo("Current reDUP configuration:")
361
+ typer.echo("=" * 40)
362
+
363
+ if not config:
364
+ typer.echo("No configuration found (using defaults)")
365
+ return
366
+
367
+ for key, value in sorted(config.items()):
368
+ typer.echo(f" {key}: {value}")
369
+ return
370
+
371
+ # Default: show help
372
+ typer.echo("Use --init to create a sample configuration file.")
373
+ typer.echo("Use --show to display current configuration.")
374
+
375
+
289
376
  @app.command()
290
377
  def info() -> None:
291
378
  """Show reDUP version and configuration info."""
@@ -0,0 +1,154 @@
1
+ """Configuration management for reDUP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ try:
10
+ import tomllib
11
+ except ImportError:
12
+ try:
13
+ import tomli as tomllib
14
+ except ImportError:
15
+ tomllib = None
16
+
17
+ from redup.core.models import ScanConfig
18
+
19
+
20
+ def _load_toml_file(file_path: Path) -> dict[str, Any]:
21
+ """Load TOML file and return as dictionary."""
22
+ if tomllib is None:
23
+ return {}
24
+
25
+ if not file_path.exists():
26
+ return {}
27
+
28
+ try:
29
+ with open(file_path, "rb") as f:
30
+ return tomllib.load(f)
31
+ except Exception:
32
+ return {}
33
+
34
+
35
+ def _get_config_from_pyproject() -> dict[str, Any]:
36
+ """Get reDUP configuration from pyproject.toml [tool.redup] section."""
37
+ pyproject_path = Path.cwd() / "pyproject.toml"
38
+ data = _load_toml_file(pyproject_path)
39
+ return data.get("tool", {}).get("redup", {})
40
+
41
+
42
+ def _get_config_from_redup_toml() -> dict[str, Any]:
43
+ """Get reDUP configuration from redup.toml file."""
44
+ redup_toml_path = Path.cwd() / "redup.toml"
45
+ return _load_toml_file(redup_toml_path)
46
+
47
+
48
+ def load_config() -> dict[str, Any]:
49
+ """Load reDUP configuration from available sources.
50
+
51
+ Priority order:
52
+ 1. Environment variables (REDUP_*)
53
+ 2. redup.toml in current directory
54
+ 3. [tool.redup] in pyproject.toml
55
+ 4. Defaults
56
+ """
57
+ config = {}
58
+
59
+ # Load from redup.toml
60
+ config.update(_get_config_from_redup_toml())
61
+
62
+ # Load from pyproject.toml (overwrites redup.toml)
63
+ config.update(_get_config_from_pyproject())
64
+
65
+ # Override with environment variables
66
+ env_mappings = {
67
+ "REDUP_EXTENSIONS": ("extensions", str),
68
+ "REDUP_MIN_LINES": ("min_lines", int),
69
+ "REDUP_MIN_SIMILARITY": ("min_similarity", float),
70
+ "REDUP_INCLUDE_TESTS": ("include_tests", bool),
71
+ "REDUP_OUTPUT": ("output", str),
72
+ "REDUP_FORMAT": ("format", str),
73
+ "REDUP_MAX_GROUPS": ("max_groups", int),
74
+ "REDUP_MAX_LINES": ("max_lines", int),
75
+ }
76
+
77
+ for env_var, (key, type_func) in env_mappings.items():
78
+ value = os.getenv(env_var)
79
+ if value is not None:
80
+ try:
81
+ if type_func == bool:
82
+ config[key] = value.lower() in ("true", "1", "yes", "on")
83
+ else:
84
+ config[key] = type_func(value)
85
+ except (ValueError, TypeError):
86
+ pass # Ignore invalid environment variables
87
+
88
+ return config
89
+
90
+
91
+ def config_to_scan_config(config: dict[str, Any], path: Path) -> ScanConfig:
92
+ """Convert configuration dict to ScanConfig object."""
93
+ extensions = config.get("extensions", ".py")
94
+ if isinstance(extensions, str):
95
+ ext_list = [e.strip() if e.startswith(".") else f".{e.strip()}"
96
+ for e in extensions.split(",")]
97
+ else:
98
+ ext_list = extensions
99
+
100
+ scan_config = config.get("scan", {})
101
+ lsh_config = config.get("lsh", {})
102
+
103
+ return ScanConfig(
104
+ root=path,
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),
109
+ lsh_enabled=lsh_config.get("enabled", True),
110
+ lsh_min_lines=lsh_config.get("min_lines", 50),
111
+ lsh_threshold=lsh_config.get("threshold", 0.8),
112
+ )
113
+
114
+
115
+ def create_sample_redup_toml() -> str:
116
+ """Create a sample redup.toml configuration file content."""
117
+ return """# reDUP Configuration File
118
+ # See https://github.com/semcod/redup for documentation
119
+
120
+ [scan]
121
+ # File extensions to scan (comma-separated)
122
+ extensions = ".py,.js,.ts,.go,.rs,.java"
123
+ # Minimum block size in lines to consider as duplicate
124
+ min_lines = 3
125
+ # Minimum similarity score (0.0-1.0) for fuzzy matches
126
+ min_similarity = 0.85
127
+ # Include test files in analysis
128
+ include_tests = false
129
+
130
+ [lsh]
131
+ # Enable LSH near-duplicate detection for large blocks
132
+ enabled = true
133
+ # Minimum block size for LSH analysis
134
+ min_lines = 50
135
+ # Similarity threshold for LSH (0.0-1.0)
136
+ threshold = 0.8
137
+
138
+ [check]
139
+ # CI gate thresholds
140
+ max_groups = 10
141
+ max_lines = 100
142
+
143
+ [output]
144
+ # Default output format
145
+ format = "toon"
146
+ # Default output directory (relative to project root)
147
+ output = "redup_output"
148
+
149
+ [reporting]
150
+ # Include code snippets in JSON output
151
+ include_snippets = false
152
+ # Generate suggestions for refactoring
153
+ generate_suggestions = true
154
+ """
@@ -149,12 +149,12 @@ def _hash_text(text: str, normalizer: Callable[[str], str]) -> str:
149
149
  return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
150
150
 
151
151
 
152
- def hash_block(text: str) -> str:
152
+ def hash_block(text: str) -> str: # nodup: accepted thin API wrapper
153
153
  """SHA-256 hash of normalized text."""
154
154
  return _hash_text(text, _normalize_text)
155
155
 
156
156
 
157
- def hash_block_structural(text: str) -> str:
157
+ def hash_block_structural(text: str) -> str: # nodup: accepted thin API wrapper
158
158
  """SHA-256 hash of deeply normalized text (variable names replaced)."""
159
159
  return _hash_text(text, _normalize_ast_text)
160
160
 
@@ -213,12 +213,12 @@ def _find_duplicates(hash_dict: dict[str, list[HashedBlock]]) -> dict[str, list[
213
213
  }
214
214
 
215
215
 
216
- def find_exact_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
216
+ def find_exact_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]: # nodup: accepted thin API wrapper
217
217
  """Find groups of blocks with identical normalized text."""
218
218
  return _find_duplicates(index.exact)
219
219
 
220
220
 
221
- def find_structural_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]:
221
+ def find_structural_duplicates(index: HashIndex) -> dict[str, list[HashedBlock]]: # nodup: accepted thin API wrapper
222
222
  """Find groups of blocks with identical structure (names may differ)."""
223
223
  return _find_duplicates(index.structural)
224
224