traceclip 0.1.0__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.
@@ -0,0 +1,34 @@
1
+ name: Detector Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Run Detector Unit Tests (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.8", "3.10", "3.12"]
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ cache: 'pip'
26
+
27
+ - name: Install package and dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -e .
31
+
32
+ - name: Run Detector Tests
33
+ run: |
34
+ python -m unittest tests/test_detector.py
@@ -0,0 +1,34 @@
1
+ name: Performance Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Run Performance Stress Tests (Python ${{ matrix.python-version }})
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ python-version: ["3.8", "3.10", "3.12"]
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ cache: 'pip'
26
+
27
+ - name: Install package and dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -e .
31
+
32
+ - name: Run Performance Tests
33
+ run: |
34
+ python -m unittest tests/test_performance.py
@@ -0,0 +1,50 @@
1
+ name: TraceClip CI/CD
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ test:
11
+ name: Test on ${{ matrix.os }} with Python ${{ matrix.python-version }}
12
+ runs-on: ${{ matrix.os }}
13
+ strategy:
14
+ fail-fast: false # If Windows fails, let Ubuntu finish running so you can see all errors
15
+ matrix:
16
+ os: [ubuntu-latest, windows-latest, macos-latest]
17
+ python-version: ["3.10", "3.11", "3.12"]
18
+
19
+ steps:
20
+ - name: Checkout Code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ cache: 'pip' # Caches dependencies to speed up the workflow
28
+
29
+ - name: Install Dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install pytest pytest-cov
33
+ pip install -e .
34
+
35
+ # CRITICAL: Headless Linux servers don't have a UI or clipboard by default.
36
+ # We install xclip and a virtual display (xvfb) so pyperclip doesn't instantly crash.
37
+ - name: Setup Headless Display for Linux Clipboard
38
+ if: runner.os == 'Linux'
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y xclip xvfb
42
+
43
+ - name: Run Pytest (Linux)
44
+ if: runner.os == 'Linux'
45
+ # We use xvfb-run to simulate a screen for the clipboard
46
+ run: xvfb-run pytest --cov=traceclip -v
47
+
48
+ - name: Run Pytest (Windows & macOS)
49
+ if: runner.os != 'Linux'
50
+ run: pytest --cov=traceclip -v
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prakhar Bhardwaj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: traceclip
3
+ Version: 0.1.0
4
+ Summary: A fast, local linting wrapper specifically designed for AI-generated code.
5
+ Project-URL: Homepage, https://github.com/Prakhar3801b/Traceclip
6
+ Project-URL: Repository, https://github.com/Prakhar3801b/Traceclip
7
+ Project-URL: Bug Tracker, https://github.com/Prakhar3801b/Traceclip/issues
8
+ Author-email: Prakhar Bhardwaj <bhardwajprakhar02@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,chatgpt,code-quality,copilot,linter,sanitizer,vibecheck
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.8
23
+ Requires-Dist: rich>=12.0.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Traceclip (vibecheck) 🧼
27
+
28
+ **Traceclip** is a fast, local code sanitizer and linter designed specifically to prune and inspect AI-generated code.
29
+
30
+ Rather than enforcing pedantic style formats like traditional linters, `vibecheck` hunts down common AI developer artifacts and "hallucinations":
31
+ - 🚫 **Unused Imports**: Bloat imported by AI but never referenced.
32
+ - 🪦 **Dead Functions**: Functions generated by the AI that are defined but never called or referenced.
33
+ - 🔑 **Placeholder Variables & Secrets**: Hardcoded templates like `API_KEY = "your_key_here"` or `URL = "https://example.com/api"`.
34
+ - 🕳️ **Stub Implementations**: Empty functions with only `pass`, `...`, or `raise NotImplementedError`.
35
+
36
+ It grades your script with a **Vibe Score** (out of 100). If your score drops too low, it fails!
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ Inside the project directory, run:
43
+ ```bash
44
+ pip install -e .
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Check a script:
50
+ ```bash
51
+ vibecheck my_script.py
52
+ ```
53
+
54
+ Check multiple files or directories:
55
+ ```bash
56
+ vibecheck src/ tests/
57
+ ```
58
+
59
+ Automatically sanitize and fix issues:
60
+ ```bash
61
+ vibecheck my_script.py --fix
62
+ ```
63
+
64
+ Enforce a minimum vibe score (ideal for CI/CD):
65
+ ```bash
66
+ vibecheck my_script.py --min-score 80
67
+ ```
@@ -0,0 +1,42 @@
1
+ # Traceclip (vibecheck) 🧼
2
+
3
+ **Traceclip** is a fast, local code sanitizer and linter designed specifically to prune and inspect AI-generated code.
4
+
5
+ Rather than enforcing pedantic style formats like traditional linters, `vibecheck` hunts down common AI developer artifacts and "hallucinations":
6
+ - 🚫 **Unused Imports**: Bloat imported by AI but never referenced.
7
+ - 🪦 **Dead Functions**: Functions generated by the AI that are defined but never called or referenced.
8
+ - 🔑 **Placeholder Variables & Secrets**: Hardcoded templates like `API_KEY = "your_key_here"` or `URL = "https://example.com/api"`.
9
+ - 🕳️ **Stub Implementations**: Empty functions with only `pass`, `...`, or `raise NotImplementedError`.
10
+
11
+ It grades your script with a **Vibe Score** (out of 100). If your score drops too low, it fails!
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ Inside the project directory, run:
18
+ ```bash
19
+ pip install -e .
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Check a script:
25
+ ```bash
26
+ vibecheck my_script.py
27
+ ```
28
+
29
+ Check multiple files or directories:
30
+ ```bash
31
+ vibecheck src/ tests/
32
+ ```
33
+
34
+ Automatically sanitize and fix issues:
35
+ ```bash
36
+ vibecheck my_script.py --fix
37
+ ```
38
+
39
+ Enforce a minimum vibe score (ideal for CI/CD):
40
+ ```bash
41
+ vibecheck my_script.py --min-score 80
42
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "traceclip"
7
+ version = "0.1.0"
8
+ description = "A fast, local linting wrapper specifically designed for AI-generated code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Prakhar Bhardwaj", email = "bhardwajprakhar02@gmail.com" }
14
+ ]
15
+ keywords = ["linter", "ai", "code-quality", "sanitizer", "vibecheck", "copilot", "chatgpt"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ "Intended Audience :: Developers",
27
+ ]
28
+ dependencies = [
29
+ "rich>=12.0.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/Prakhar3801b/Traceclip"
34
+ Repository = "https://github.com/Prakhar3801b/Traceclip"
35
+ "Bug Tracker" = "https://github.com/Prakhar3801b/Traceclip/issues"
36
+
37
+ [project.scripts]
38
+ vibecheck = "traceclip.cli:main"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/traceclip"]
@@ -0,0 +1,3 @@
1
+ """Traceclip: The AI-Generated Code Sanitizer."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,170 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+
12
+ from traceclip.detector import detect_issues
13
+ from traceclip.fixer import fix_file
14
+
15
+ console = Console()
16
+
17
+ def find_py_files(paths: List[str]) -> List[Path]:
18
+ py_files = []
19
+ ignored_dirs = {".git", "venv", ".venv", "env", "__pycache__", ".pytest_cache", "build", "dist", ".egg-info"}
20
+
21
+ for path_str in paths:
22
+ path = Path(path_str)
23
+ if path.is_file():
24
+ if path.suffix == ".py":
25
+ py_files.append(path)
26
+ elif path.is_dir():
27
+ for root, dirs, files in os.walk(path):
28
+ # Prune ignored directories in-place to avoid traversing them
29
+ dirs[:] = [d for d in dirs if d not in ignored_dirs and not d.startswith(".")]
30
+ for file in files:
31
+ if file.endswith(".py"):
32
+ py_files.append(Path(root) / file)
33
+ return sorted(list(set(py_files)))
34
+
35
+ def format_score(score: int) -> Text:
36
+ if score >= 90:
37
+ return Text(f"{score}/100 (IMMACULATE VIBES)", style="bold green")
38
+ elif score >= 70:
39
+ return Text(f"{score}/100 (PASSABLE VIBES)", style="bold yellow")
40
+ else:
41
+ return Text(f"{score}/100 (BAD VIBES)", style="bold red")
42
+
43
+ def main():
44
+ parser = argparse.ArgumentParser(
45
+ description="Traceclip (vibecheck): The AI-Generated Code Sanitizer."
46
+ )
47
+ parser.add_argument(
48
+ "paths",
49
+ nargs="*",
50
+ default=["."],
51
+ help="Files or directories to scan (default: current directory)."
52
+ )
53
+ parser.add_argument(
54
+ "--fix",
55
+ "-f",
56
+ action="store_true",
57
+ help="Automatically sanitize and fix issues in-place."
58
+ )
59
+ parser.add_argument(
60
+ "--min-score",
61
+ "-m",
62
+ type=int,
63
+ default=0,
64
+ help="Minimum allowed Vibe Score. Returns exit code 1 if any file scores lower."
65
+ )
66
+
67
+ args = parser.parse_args()
68
+ py_files = find_py_files(args.paths)
69
+
70
+ if not py_files:
71
+ console.print("[bold red]Error:[/] No Python files found to scan.", style="red")
72
+ sys.exit(1)
73
+
74
+ console.print(Panel(
75
+ "[bold cyan]Traceclip Vibecheck[/] - Linting wrapper for AI-generated code\n"
76
+ "[dim]Hunts down unused imports, dead functions, stub methods, and placeholder secrets.[/]",
77
+ border_style="cyan"
78
+ ))
79
+
80
+ all_passed = True
81
+ total_issues = 0
82
+ total_files = len(py_files)
83
+ scores = []
84
+
85
+ for file_path in py_files:
86
+ try:
87
+ with open(file_path, "r", encoding="utf-8") as f:
88
+ content = f.read()
89
+ except Exception as e:
90
+ console.print(f"[bold red]Error reading file {file_path}:[/] {e}")
91
+ continue
92
+
93
+ # Detect issues
94
+ issues = detect_issues(content, str(file_path))
95
+
96
+ # Calculate Vibe Score
97
+ deductions = sum(issue.weight for issue in issues)
98
+ score = max(0, 100 - deductions)
99
+ scores.append(score)
100
+ total_issues += len(issues)
101
+
102
+ # Print report
103
+ relative_path = os.path.relpath(file_path, os.getcwd())
104
+ console.print(f"\n[bold underline]{relative_path}[/]")
105
+
106
+ if not issues:
107
+ console.print(" [bold green]✓[/] Vibe check passed! Code is clean and tidy. (Score: 100/100)")
108
+ continue
109
+
110
+ # Display issues in a table
111
+ table = Table(show_header=True, header_style="bold magenta", box=None, padding=(0, 2))
112
+ table.add_column("Line:Col", justify="right", style="dim")
113
+ table.add_column("Type", style="bold")
114
+ table.add_column("Message")
115
+ table.add_column("Context", style="italic dim")
116
+
117
+ for issue in issues:
118
+ sev_style = "bold red" if issue.severity == "error" else "yellow"
119
+ issue_type_formatted = f"[{sev_style}]{issue.issue_type.replace('_', ' ').title()}[/]"
120
+ table.add_row(
121
+ f"{issue.line}:{issue.col}",
122
+ issue_type_formatted,
123
+ issue.message,
124
+ issue.context
125
+ )
126
+
127
+ console.print(table)
128
+ console.print(f" [bold]Vibe Score:[/] ", end="")
129
+ console.print(format_score(score))
130
+
131
+ # Enforce min-score check
132
+ if score < args.min_score:
133
+ all_passed = False
134
+
135
+ # Run Auto-Fix if requested
136
+ if args.fix and len([i for i in issues if i.issue_type not in ("syntax_error", "wildcard_import")]) > 0:
137
+ console.print(f" [cyan]Applying autofixes to {relative_path}...[/]")
138
+ fixed_content = fix_file(content, str(file_path))
139
+ try:
140
+ with open(file_path, "w", encoding="utf-8") as f:
141
+ f.write(fixed_content)
142
+ console.print(f" [bold green]✓[/] Sanitized and saved!")
143
+ # Recalculate score for summary print
144
+ fixed_issues = detect_issues(fixed_content, str(file_path))
145
+ fixed_deductions = sum(issue.weight for issue in fixed_issues)
146
+ fixed_score = max(0, 100 - fixed_deductions)
147
+ scores[-1] = fixed_score
148
+ console.print(f" [bold]New Vibe Score:[/] ", end="")
149
+ console.print(format_score(fixed_score))
150
+ except Exception as e:
151
+ console.print(f" [bold red]Failed to write fixes:[/] {e}")
152
+
153
+ # Summary Panel
154
+ avg_score = int(sum(scores) / len(scores)) if scores else 100
155
+ summary_style = "bold green" if avg_score >= 90 else "bold yellow" if avg_score >= 70 else "bold red"
156
+
157
+ console.print("\n" + "="*50)
158
+ console.print(f"[bold cyan]Summary Report:[/]")
159
+ console.print(f" Files scanned: {total_files}")
160
+ console.print(f" Total issues found: {total_issues}")
161
+ console.print(f" Average Vibe Score: ", end="")
162
+ console.print(format_score(avg_score))
163
+ console.print("="*50)
164
+
165
+ if not all_passed:
166
+ console.print("\n[bold red]✖ vibecheck failed:[/] One or more files scored below the minimum threshold of --min-score.", style="red")
167
+ sys.exit(1)
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,358 @@
1
+ import ast
2
+ import re
3
+ from dataclasses import dataclass
4
+ from typing import List, Set, Dict, Any, Tuple
5
+
6
+ # Severity weights for scoring
7
+ WEIGHT_UNUSED_IMPORT = 5
8
+ WEIGHT_WILDCARD_IMPORT = 5
9
+ WEIGHT_DEAD_FUNCTION = 10
10
+ WEIGHT_STUB_FUNCTION = 15
11
+ WEIGHT_PLACEHOLDER_VAR = 20
12
+
13
+ @dataclass
14
+ class Issue:
15
+ file_path: str
16
+ line: int
17
+ col: int
18
+ issue_type: str
19
+ severity: str
20
+ message: str
21
+ weight: int
22
+ end_line: int = 0
23
+ context: str = ""
24
+
25
+ class CodeAnalyzer(ast.NodeVisitor):
26
+ def __init__(self, file_path: str, source_code: str):
27
+ self.file_path = file_path
28
+ self.source_code = source_code
29
+ self.source_lines = source_code.splitlines()
30
+ self.issues: List[Issue] = []
31
+
32
+ # Tracks imports: bound_name -> (line, col, import_node, alias_node)
33
+ self.imports: Dict[str, Tuple[int, int, ast.AST, ast.alias]] = {}
34
+ # Tracks occurrences of names used in Load context
35
+ self.name_loads: Set[str] = set()
36
+ # Tracks occurrences of attribute access
37
+ self.attribute_loads: Set[str] = set()
38
+
39
+ # Tracks defined functions: name -> {node, line, end_line, is_method}
40
+ self.functions: Dict[str, Dict[str, Any]] = {}
41
+ # Tracks function names referenced (loads or attributes)
42
+ self.referenced_names: Set[str] = set()
43
+ # Reference line numbers for each name: name -> list of lines where it was referenced
44
+ self.name_reference_lines: Dict[str, List[int]] = {}
45
+ # Tracks exported names in __all__
46
+ self.exported_names: Set[str] = set()
47
+
48
+ def add_name_reference(self, name: str, line: int):
49
+ self.referenced_names.add(name)
50
+ if name not in self.name_reference_lines:
51
+ self.name_reference_lines[name] = []
52
+ self.name_reference_lines[name].append(line)
53
+
54
+ def visit_Import(self, node: ast.Import):
55
+ for alias in node.names:
56
+ # import foo.bar as baz -> bound name is baz
57
+ # import foo.bar -> bound name is foo
58
+ bound_name = alias.asname if alias.asname else alias.name.split('.')[0]
59
+ self.imports[bound_name] = (node.lineno, node.col_offset, node, alias)
60
+ self.generic_visit(node)
61
+
62
+ def visit_ImportFrom(self, node: ast.ImportFrom):
63
+ for alias in node.names:
64
+ if alias.name == '*':
65
+ # Wildcard import detected
66
+ end_line = getattr(node, "end_lineno", node.lineno)
67
+ self.issues.append(Issue(
68
+ file_path=self.file_path,
69
+ line=node.lineno,
70
+ col=node.col_offset,
71
+ issue_type="wildcard_import",
72
+ severity="warning",
73
+ message=f"Wildcard import 'from {node.module} import *' pollutes namespace.",
74
+ weight=WEIGHT_WILDCARD_IMPORT,
75
+ end_line=end_line,
76
+ context=self.get_line_context(node.lineno)
77
+ ))
78
+ continue
79
+ # from foo import bar as baz -> bound name is baz
80
+ # from foo import bar -> bound name is bar
81
+ bound_name = alias.asname if alias.asname else alias.name
82
+ self.imports[bound_name] = (node.lineno, node.col_offset, node, alias)
83
+ self.generic_visit(node)
84
+
85
+ def visit_Name(self, node: ast.Name):
86
+ if isinstance(node.ctx, ast.Load):
87
+ self.name_loads.add(node.id)
88
+ self.add_name_reference(node.id, node.lineno)
89
+ self.generic_visit(node)
90
+
91
+ def visit_Attribute(self, node: ast.Attribute):
92
+ # E.g. obj.method_name -> attribute access
93
+ self.attribute_loads.add(node.attr)
94
+ self.add_name_reference(node.attr, node.lineno)
95
+ self.generic_visit(node)
96
+
97
+ def visit_FunctionDef(self, node: ast.FunctionDef):
98
+ self.process_function(node)
99
+
100
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
101
+ self.process_function(node)
102
+
103
+ def process_function(self, node: Any):
104
+ # Skip dunder methods
105
+ if node.name.startswith("__") and node.name.endswith("__"):
106
+ self.generic_visit(node)
107
+ return
108
+
109
+ # Skip common entrypoints
110
+ if node.name == "main":
111
+ self.generic_visit(node)
112
+ return
113
+
114
+ # Record function details
115
+ end_line = getattr(node, "end_lineno", node.lineno)
116
+ self.functions[node.name] = {
117
+ "node": node,
118
+ "line": node.lineno,
119
+ "end_line": end_line,
120
+ }
121
+
122
+ # Check for stub implementations
123
+ self.check_stub_function(node)
124
+
125
+ self.generic_visit(node)
126
+
127
+ def check_stub_function(self, node: Any):
128
+ # A stub is a function with body containing only pass, Ellipsis, raise NotImplementedError
129
+ body = node.body
130
+ is_stub = False
131
+ reason = ""
132
+
133
+ if len(body) == 1:
134
+ stmt = body[0]
135
+ if isinstance(stmt, ast.Pass):
136
+ is_stub = True
137
+ reason = "contains only 'pass'"
138
+ elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and stmt.value.value is Ellipsis:
139
+ is_stub = True
140
+ reason = "contains only '...'"
141
+ elif isinstance(stmt, ast.Raise):
142
+ if isinstance(stmt.exc, ast.Name) and stmt.exc.id == "NotImplementedError":
143
+ is_stub = True
144
+ reason = "raises 'NotImplementedError'"
145
+ elif isinstance(stmt.exc, ast.Call) and isinstance(stmt.exc.func, ast.Name) and stmt.exc.func.id == "NotImplementedError":
146
+ is_stub = True
147
+ reason = "raises 'NotImplementedError'"
148
+ elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str):
149
+ # A function containing only a docstring
150
+ is_stub = True
151
+ reason = "contains only a docstring"
152
+
153
+ if is_stub:
154
+ self.issues.append(Issue(
155
+ file_path=self.file_path,
156
+ line=node.lineno,
157
+ col=node.col_offset,
158
+ issue_type="stub_function",
159
+ severity="warning",
160
+ message=f"Function '{node.name}' appears to be an empty stub ({reason}).",
161
+ weight=WEIGHT_STUB_FUNCTION,
162
+ end_line=getattr(node, "end_lineno", node.lineno),
163
+ context=self.get_line_context(node.lineno)
164
+ ))
165
+
166
+ def visit_Assign(self, node: ast.Assign):
167
+ self.check_placeholder_assignment(node)
168
+ self.check_all_export(node)
169
+ self.generic_visit(node)
170
+
171
+ def check_all_export(self, node: ast.Assign):
172
+ for t in node.targets:
173
+ if isinstance(t, ast.Name) and t.id == "__all__":
174
+ if isinstance(node.value, (ast.List, ast.Tuple, ast.Set)):
175
+ for elt in node.value.elts:
176
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
177
+ self.exported_names.add(elt.value)
178
+ elif isinstance(elt, ast.Str):
179
+ self.exported_names.add(elt.s)
180
+
181
+ def visit_AnnAssign(self, node: ast.AnnAssign):
182
+ self.check_placeholder_assignment(node)
183
+ self.generic_visit(node)
184
+
185
+ def check_placeholder_assignment(self, node: Any):
186
+ # Extract string value if it's a constant
187
+ if not node.value or not isinstance(node.value, ast.Constant) or not isinstance(node.value.value, str):
188
+ return
189
+
190
+ val_str = node.value.value
191
+ targets = []
192
+ if isinstance(node, ast.Assign):
193
+ for t in node.targets:
194
+ if isinstance(t, ast.Name):
195
+ targets.append(t.id)
196
+ elif isinstance(t, ast.Attribute):
197
+ targets.append(t.attr)
198
+ elif isinstance(node, ast.AnnAssign):
199
+ if isinstance(node.target, ast.Name):
200
+ targets.append(node.target.id)
201
+ elif isinstance(node.target, ast.Attribute):
202
+ targets.append(node.target.attr)
203
+
204
+ placeholder_regexes = [
205
+ r"(?i)your_.*_here",
206
+ r"(?i)insert_.*",
207
+ r"(?i)enter_.*",
208
+ r"(?i)placeholder",
209
+ r"(?i)change[_-]?me",
210
+ r"(?i)todo",
211
+ r"(?i)<.*>",
212
+ r"(?i)\[.*\]",
213
+ r"(?i)your-.*",
214
+ r"^your_api_key$",
215
+ r"^your_token$",
216
+ r"^password123$",
217
+ ]
218
+
219
+ for target in targets:
220
+ is_placeholder = False
221
+ # Check value against regexes
222
+ for r in placeholder_regexes:
223
+ if re.search(r, val_str):
224
+ is_placeholder = True
225
+ break
226
+
227
+ # Check key name + typical suspicious placeholder values
228
+ suspicious_keys = {"key", "token", "secret", "pass", "pwd", "credential", "url"}
229
+ any_suspicious_key = any(sk in target.lower() for sk in suspicious_keys)
230
+
231
+ if any_suspicious_key:
232
+ # If key is key/token/secret and value is very typical dummy text
233
+ dummy_texts = {"your", "insert", "placeholder", "enter", "todo", "example", "here", "change", "secret"}
234
+ if any(dt in val_str.lower() for dt in dummy_texts) or len(val_str) < 5 or val_str.strip() == "":
235
+ is_placeholder = True
236
+
237
+ if is_placeholder:
238
+ self.issues.append(Issue(
239
+ file_path=self.file_path,
240
+ line=node.lineno,
241
+ col=node.col_offset,
242
+ issue_type="placeholder_var",
243
+ severity="error",
244
+ message=f"Placeholder value '{val_str}' assigned to variable/attribute '{target}'.",
245
+ weight=WEIGHT_PLACEHOLDER_VAR,
246
+ end_line=getattr(node, "end_lineno", node.lineno),
247
+ context=self.get_line_context(node.lineno)
248
+ ))
249
+
250
+ def get_line_context(self, lineno: int) -> str:
251
+ if 1 <= lineno <= len(self.source_lines):
252
+ return self.source_lines[lineno - 1].strip()
253
+ return ""
254
+
255
+ def analyze(self):
256
+ # Run visitor to collect all imports, functions, and references
257
+ self.visit(ast.parse(self.source_code))
258
+
259
+ # 1. Unused Imports check
260
+ # An import is unused if its bound name is never in name_loads (except inside imports themselves)
261
+ # Wait, if we import 'foo', but the only reference is the import statement itself.
262
+ # Since ast.alias / ast.Import nodes are visited, we should make sure that the name loads count references.
263
+ # Let's check which bound names have 0 references in the loads.
264
+ for bound_name, (line, col, node, alias) in self.imports.items():
265
+ if bound_name not in self.name_loads:
266
+ if bound_name not in self.exported_names:
267
+ name_desc = alias.name
268
+ if alias.asname:
269
+ name_desc = f"{alias.name} as {alias.asname}"
270
+ self.issues.append(Issue(
271
+ file_path=self.file_path,
272
+ line=line,
273
+ col=col,
274
+ issue_type="unused_import",
275
+ severity="warning",
276
+ message=f"Unused import '{name_desc}'.",
277
+ weight=WEIGHT_UNUSED_IMPORT,
278
+ end_line=getattr(node, "end_lineno", line),
279
+ context=self.get_line_context(line)
280
+ ))
281
+
282
+ # 2. Dead Functions check
283
+ for func_name, info in self.functions.items():
284
+ ref_lines = self.name_reference_lines.get(func_name, [])
285
+
286
+ # An external reference is any reference outside the function's own body range
287
+ external_refs = [r_line for r_line in ref_lines if not (info["line"] <= r_line <= info["end_line"])]
288
+
289
+ if len(external_refs) == 0:
290
+ self.issues.append(Issue(
291
+ file_path=self.file_path,
292
+ line=info["line"],
293
+ col=info["node"].col_offset,
294
+ issue_type="dead_function",
295
+ severity="warning",
296
+ message=f"Function '{func_name}' is defined but never called or referenced externally.",
297
+ weight=WEIGHT_DEAD_FUNCTION,
298
+ end_line=info["end_line"],
299
+ context=self.get_line_context(info["line"])
300
+ ))
301
+
302
+
303
+ def scan_comments_for_placeholders(source_code: str, file_path: str) -> List[Issue]:
304
+ issues = []
305
+ lines = source_code.splitlines()
306
+ ai_comment_patterns = [
307
+ (r"(?i)#\s*todo:\s*(implement|replace|fill)", "AI placeholder comment: TODO implementation detail"),
308
+ (r"(?i)#\s*generated\s*by\s*(copilot|ai|chatgpt)", "AI leftover comment: 'Generated by AI'"),
309
+ (r"(?i)#\s*insert\s*.*\s*here", "AI placeholder comment: insert indicator"),
310
+ (r"(?i)#\s*change\s*me", "AI placeholder comment: 'change me'"),
311
+ ]
312
+ for idx, line in enumerate(lines, 1):
313
+ # We only want to match actual comment lines or parts of lines that are comments
314
+ comment_match = re.search(r"#.*", line)
315
+ if comment_match:
316
+ comment_text = comment_match.group(0)
317
+ for pattern, message in ai_comment_patterns:
318
+ if re.search(pattern, comment_text):
319
+ issues.append(Issue(
320
+ file_path=file_path,
321
+ line=idx,
322
+ col=comment_match.start(),
323
+ issue_type="placeholder_var", # Treat comment placeholders similarly to variable placeholders
324
+ severity="warning",
325
+ message=message,
326
+ weight=5, # lighter weight for comments
327
+ context=line.strip()
328
+ ))
329
+ return issues
330
+
331
+
332
+ def detect_issues(source_code: str, file_path: str) -> List[Issue]:
333
+ # AST analysis
334
+ try:
335
+ analyzer = CodeAnalyzer(file_path, source_code)
336
+ analyzer.analyze()
337
+ issues = analyzer.issues
338
+ except SyntaxError as e:
339
+ # If there's a syntax error, we report it as a critical failure
340
+ issues = [Issue(
341
+ file_path=file_path,
342
+ line=e.lineno or 1,
343
+ col=e.offset or 1,
344
+ issue_type="syntax_error",
345
+ severity="error",
346
+ message=f"Syntax Error: {e.msg}",
347
+ weight=50,
348
+ context=e.text.strip() if e.text else ""
349
+ )]
350
+ return issues
351
+
352
+ # Comments analysis
353
+ comment_issues = scan_comments_for_placeholders(source_code, file_path)
354
+ issues.extend(comment_issues)
355
+
356
+ # Sort issues by line number
357
+ issues.sort(key=lambda x: x.line)
358
+ return issues
@@ -0,0 +1,137 @@
1
+ import ast
2
+ import re
3
+ from typing import List
4
+ from traceclip.detector import CodeAnalyzer, Issue, scan_comments_for_placeholders
5
+
6
+ def fix_file(source_code: str, file_path: str) -> str:
7
+ # 1. Detect issues using the same engine
8
+ try:
9
+ analyzer = CodeAnalyzer(file_path, source_code)
10
+ analyzer.analyze()
11
+ issues = analyzer.issues
12
+ # Exclude syntax errors and wildcard imports from being fixed automatically
13
+ issues = [i for i in issues if i.issue_type not in ("syntax_error", "wildcard_import")]
14
+ except Exception:
15
+ # If AST parsing fails, return source_code unmodified
16
+ return source_code
17
+
18
+ if not issues:
19
+ return source_code
20
+
21
+ # 2. Sort issues in descending order of line number to process bottom-up
22
+ # This prevents shifting lines of unprocessed issues above the current edit.
23
+ issues.sort(key=lambda x: (x.line, x.col), reverse=True)
24
+
25
+ lines = source_code.splitlines()
26
+ modified_lines = set()
27
+ needs_os_import = False
28
+
29
+ for issue in issues:
30
+ # Avoid double-modifying lines (e.g. if a function is both a stub and dead)
31
+ line_range = range(issue.line, issue.end_line + 1)
32
+ if any(l in modified_lines for l in line_range):
33
+ continue
34
+
35
+ # Mark range as modified
36
+ for l in line_range:
37
+ modified_lines.add(l)
38
+
39
+ if issue.issue_type == "unused_import":
40
+ # Find the import node in the analyzer
41
+ # Let's search the imports dict for the one matching this line
42
+ matching_import_info = None
43
+ bound_name_to_remove = None
44
+ for name, info in analyzer.imports.items():
45
+ import_line, import_col, node, alias = info
46
+ if import_line == issue.line:
47
+ # Match by line number and column
48
+ matching_import_info = info
49
+ bound_name_to_remove = name
50
+ break
51
+
52
+ if matching_import_info:
53
+ _, _, node, alias_node = matching_import_info
54
+ start_idx = node.lineno - 1
55
+ end_idx = getattr(node, "end_lineno", node.lineno) - 1
56
+
57
+ # Check if this import statement has multiple imported names
58
+ if len(node.names) > 1:
59
+ # Reconstruct separate imports for all names that are NOT unused
60
+ used_aliases = []
61
+ for a in node.names:
62
+ # Determine bound name for this alias
63
+ b_name = a.asname if a.asname else (a.name if isinstance(node, ast.ImportFrom) else a.name.split('.')[0])
64
+ # A name is used if it's not the one we are removing
65
+ if b_name != bound_name_to_remove and b_name in analyzer.name_loads:
66
+ used_aliases.append(a)
67
+
68
+ # Comment out the old import statement
69
+ for idx in range(start_idx, end_idx + 1):
70
+ lines[idx] = f"# vibecheck-removed: {lines[idx]}"
71
+
72
+ if used_aliases:
73
+ # Build the new import line
74
+ if isinstance(node, ast.Import):
75
+ new_import_parts = [f"{a.name} as {a.asname}" if a.asname else a.name for a in used_aliases]
76
+ new_import = "import " + ", ".join(new_import_parts)
77
+ else:
78
+ new_import_parts = [f"{a.name} as {a.asname}" if a.asname else a.name for a in used_aliases]
79
+ new_import = f"from {node.module} import " + ", ".join(new_import_parts)
80
+
81
+ # Append the new import statement right below the commented one
82
+ lines[end_idx] = lines[end_idx] + "\n" + new_import
83
+ else:
84
+ # Single name import: comment out the whole thing
85
+ for idx in range(start_idx, end_idx + 1):
86
+ lines[idx] = f"# vibecheck-removed: {lines[idx]}"
87
+
88
+ elif issue.issue_type in ("dead_function", "stub_function"):
89
+ start_idx = issue.line - 1
90
+ end_idx = issue.end_line - 1
91
+ prefix = "# vibecheck-removed: " if issue.issue_type == "dead_function" else "# vibecheck-stub-removed: "
92
+ for idx in range(start_idx, end_idx + 1):
93
+ lines[idx] = f"{prefix}{lines[idx]}"
94
+
95
+ elif issue.issue_type == "placeholder_var":
96
+ # For placeholder variables (API_KEY = "your_key"), replace with os.getenv
97
+ # Check if this is a comment placeholder or variable placeholder
98
+ # If it's a comment, we don't change the code (just keep it as is, or we can remove it. Let's just comment it out further or leave it)
99
+ if "comment:" in issue.message.lower():
100
+ # Comment placeholder, we can prefix the comment line with a warning or just leave it
101
+ continue
102
+
103
+ # Variable placeholder: e.g. API_KEY = "your_key"
104
+ start_idx = issue.line - 1
105
+ end_idx = issue.end_line - 1
106
+
107
+ # Since AST is valid, we can parse this assignment line
108
+ # Let's extract the target and value
109
+ line_str = lines[start_idx]
110
+ if "=" in line_str:
111
+ parts = line_str.split("=", 1)
112
+ left_side = parts[0].rstrip()
113
+ # Find the target name
114
+ # Let's guess the target variable name: we can get it from the issue message or parse it
115
+ # E.g. "Placeholder value 'your_key' assigned to variable/attribute 'API_KEY'."
116
+ # We can extract the target name from the message or left_side
117
+ target_name = left_side.strip().split()[-1] # Simple guess
118
+ # Clean up characters like colon for typed annotations
119
+ if ":" in target_name:
120
+ target_name = target_name.split(":")[0].strip()
121
+
122
+ # Extract the original string literal value
123
+ # We know the issue.message contains "Placeholder value 'val' assigned to..."
124
+ # Let's extract 'val' from the message using regex
125
+ val_match = re.search(r"Placeholder value '(.*)' assigned to", issue.message)
126
+ val_str = val_match.group(1) if val_match else "your_key_here"
127
+
128
+ # Replace with os.getenv("TARGET_NAME", "default_val")
129
+ new_assignment = f"{left_side} = os.getenv({target_name!r}, {val_str!r})"
130
+ lines[start_idx] = f"# vibecheck-placeholder-fixed:\n# {line_str}\n{new_assignment}"
131
+ needs_os_import = True
132
+
133
+ # 3. Add import os if needed and not already imported and kept
134
+ if needs_os_import and ("os" not in analyzer.imports or "os" not in analyzer.name_loads):
135
+ lines.insert(0, "import os")
136
+
137
+ return "\n".join(lines) + "\n"
@@ -0,0 +1 @@
1
+ # Traceclip test package
@@ -0,0 +1,129 @@
1
+ import unittest
2
+ from traceclip.detector import detect_issues
3
+ from traceclip.fixer import fix_file
4
+
5
+ class TestTraceclip(unittest.TestCase):
6
+ def test_unused_imports(self):
7
+ code = """import os
8
+ import sys
9
+ from collections import defaultdict, Counter
10
+ import math as m
11
+
12
+ d = defaultdict(int)
13
+ """
14
+ issues = detect_issues(code, "test.py")
15
+ types = [i.issue_type for i in issues]
16
+ messages = [i.message for i in issues]
17
+
18
+ self.assertIn("unused_import", types)
19
+ # os, sys, Counter, m are unused
20
+ unused_names = [m for m in messages if "Unused import" in m]
21
+ self.assertEqual(len(unused_names), 4)
22
+ self.assertTrue(any("os" in m for m in unused_names))
23
+ self.assertTrue(any("sys" in m for m in unused_names))
24
+ self.assertTrue(any("Counter" in m for m in unused_names))
25
+ self.assertTrue(any("math as m" in m for m in unused_names))
26
+
27
+ def test_wildcard_import(self):
28
+ code = "from math import *"
29
+ issues = detect_issues(code, "test.py")
30
+ types = [i.issue_type for i in issues]
31
+ self.assertIn("wildcard_import", types)
32
+
33
+ def test_dead_functions(self):
34
+ code = """def dead_func(x):
35
+ return x * 2
36
+
37
+ def recursive_dead_func(n):
38
+ if n <= 1:
39
+ return 1
40
+ return n * recursive_dead_func(n-1)
41
+
42
+ def active_func():
43
+ return 42
44
+
45
+ print(active_func())
46
+ """
47
+ issues = detect_issues(code, "test.py")
48
+ dead_funcs = [i.message for i in issues if i.issue_type == "dead_function"]
49
+ self.assertEqual(len(dead_funcs), 2)
50
+ self.assertTrue(any("dead_func" in m for m in dead_funcs))
51
+ self.assertTrue(any("recursive_dead_func" in m for m in dead_funcs))
52
+ self.assertFalse(any("active_func" in m for m in dead_funcs))
53
+
54
+ def test_placeholder_variables(self):
55
+ code = """API_KEY = "your_api_key_here"
56
+ PASSWORD = "insert_password_here"
57
+ token = "enter_token_here"
58
+ my_val = "normal value"
59
+ """
60
+ issues = detect_issues(code, "test.py")
61
+ placeholders = [i.message for i in issues if i.issue_type == "placeholder_var"]
62
+ self.assertEqual(len(placeholders), 3)
63
+ self.assertTrue(any("API_KEY" in m for m in placeholders))
64
+ self.assertTrue(any("PASSWORD" in m for m in placeholders))
65
+ self.assertTrue(any("token" in m for m in placeholders))
66
+ self.assertFalse(any("my_val" in m for m in placeholders))
67
+
68
+ def test_stub_functions(self):
69
+ code = """def stub1():
70
+ pass
71
+
72
+ def stub2():
73
+ ...
74
+
75
+ def stub3():
76
+ raise NotImplementedError
77
+
78
+ def active():
79
+ print("hello")
80
+ """
81
+ issues = detect_issues(code, "test.py")
82
+ stubs = [i.message for i in issues if i.issue_type == "stub_function"]
83
+ self.assertEqual(len(stubs), 3)
84
+ self.assertTrue(any("stub1" in m for m in stubs))
85
+ self.assertTrue(any("stub2" in m for m in stubs))
86
+ self.assertTrue(any("stub3" in m for m in stubs))
87
+ self.assertFalse(any("active" in m for m in stubs))
88
+
89
+ def test_comment_scanning(self):
90
+ code = """# generated by copilot
91
+ # TODO: implement this
92
+ # normal comment
93
+ """
94
+ issues = detect_issues(code, "test.py")
95
+ comments = [i.message for i in issues if i.issue_type == "placeholder_var" and "comment:" in i.message.lower()]
96
+ self.assertEqual(len(comments), 2)
97
+
98
+ def test_fix_unused_imports(self):
99
+ code = """import sys
100
+ from collections import defaultdict, Counter
101
+
102
+ d = defaultdict(int)
103
+ """
104
+ fixed = fix_file(code, "test.py")
105
+ self.assertIn("# vibecheck-removed: import sys", fixed)
106
+ self.assertIn("# vibecheck-removed: from collections import defaultdict, Counter", fixed)
107
+ self.assertIn("from collections import defaultdict", fixed)
108
+ self.assertNotIn("Counter", fixed.splitlines()[-1]) # Counter is removed in the recreated import
109
+
110
+ def test_fix_dead_functions(self):
111
+ code = """def dead_func():
112
+ return 1
113
+
114
+ print("hello")
115
+ """
116
+ fixed = fix_file(code, "test.py")
117
+ self.assertIn("# vibecheck-removed: def dead_func():", fixed)
118
+ self.assertIn("# vibecheck-removed: return 1", fixed)
119
+
120
+ def test_fix_placeholder_variables(self):
121
+ code = """API_KEY = "your_api_key_here"
122
+ """
123
+ # API_KEY is a placeholder, should be replaced by os.getenv, and import os added
124
+ fixed = fix_file(code, "test.py")
125
+ self.assertIn("import os", fixed)
126
+ self.assertIn("API_KEY = os.getenv('API_KEY', 'your_api_key_here')", fixed)
127
+
128
+ if __name__ == "__main__":
129
+ unittest.main()
@@ -0,0 +1,38 @@
1
+ # Generated by ChatGPT
2
+
3
+ import sys
4
+ import os
5
+ from collections import defaultdict, Counter
6
+ from math import *
7
+
8
+ API_KEY = "your_api_key_here"
9
+ PORT = 8080
10
+
11
+ def dead_utility_func(x):
12
+ # This is a dead function defined by AI but never called
13
+ return x * 2
14
+
15
+ def active_func(data):
16
+ # This is used and references defaultdict
17
+ d = defaultdict(int)
18
+ for item in data:
19
+ d[item] += 1
20
+ return d
21
+
22
+ def recursive_dead_func(n):
23
+ if n <= 1:
24
+ return 1
25
+ # Only references itself (recursive)
26
+ return n * recursive_dead_func(n - 1)
27
+
28
+ def empty_stub_func():
29
+ # TODO: implement this later
30
+ pass
31
+
32
+ def raise_stub_func():
33
+ raise NotImplementedError
34
+
35
+ if __name__ == "__main__":
36
+ test_data = [1, 2, 2, 3]
37
+ result = active_func(test_data)
38
+ print(f"Result: {result}")
@@ -0,0 +1,84 @@
1
+ import unittest
2
+ import time
3
+ from traceclip.detector import detect_issues
4
+ from traceclip.fixer import fix_file
5
+
6
+ class TestPerformance(unittest.TestCase):
7
+ def test_large_file_performance(self):
8
+ lines = []
9
+
10
+ # 1. Add 100 unused imports
11
+ for i in range(100):
12
+ lines.append(f"import unused_module_{i}")
13
+
14
+ # Add used imports
15
+ lines.append("import os")
16
+ lines.append("from collections import defaultdict")
17
+
18
+ # 2. Add 200 placeholder variables (100 API keys, 100 DB URLs)
19
+ for i in range(100):
20
+ lines.append(f"API_KEY_{i} = 'your_api_key_here_{i}'")
21
+ lines.append(f"DB_URL_{i} = 'insert_db_url_here'")
22
+
23
+ # 3. Add 100 stub functions (which will also be dead)
24
+ for i in range(100):
25
+ lines.append(f"def stub_function_{i}():")
26
+ lines.append(" pass")
27
+
28
+ # 4. Add 250 active functions (chained calls)
29
+ for i in range(250):
30
+ lines.append(f"def active_function_{i}(x):")
31
+ if i == 0:
32
+ lines.append(" return x + 1")
33
+ else:
34
+ lines.append(f" return active_function_{i-1}(x) + 1")
35
+
36
+ # 5. Add 250 dead functions
37
+ for i in range(250):
38
+ lines.append(f"def dead_function_{i}(x):")
39
+ lines.append(" return x * 2")
40
+
41
+ # 6. Entrypoint using active functions
42
+ lines.append("if __name__ == '__main__':")
43
+ lines.append(" d = defaultdict(int)")
44
+ lines.append(" print(active_function_249(10))")
45
+
46
+ large_code = "\n".join(lines)
47
+ line_count = len(lines)
48
+
49
+ # Measure detection speed
50
+ start_detect = time.perf_counter()
51
+ issues = detect_issues(large_code, "large_file.py")
52
+ detect_duration = time.perf_counter() - start_detect
53
+
54
+ # Measure fixing speed
55
+ start_fix = time.perf_counter()
56
+ fixed_code = fix_file(large_code, "large_file.py")
57
+ fix_duration = time.perf_counter() - start_fix
58
+
59
+ # Print stats
60
+ print(f"\n[Stress Test] Generated codebase: {line_count} lines.")
61
+ print(f" - Detected {len(issues)} issues in {detect_duration:.4f} seconds.")
62
+ print(f" - Fixed codebase in {fix_duration:.4f} seconds.")
63
+
64
+ # Performance assertions (sub-second threshold)
65
+ self.assertLess(detect_duration, 1.0, f"Detection took too long: {detect_duration:.4f}s")
66
+ self.assertLess(fix_duration, 1.5, f"Fixing took too long: {fix_duration:.4f}s")
67
+
68
+ # Correctness checks: we expect:
69
+ # - 100 unused imports
70
+ # - 200 placeholder variables
71
+ # - 100 stub functions
72
+ # - 250 dead functions
73
+ # - 100 stub functions (which are also dead functions)
74
+ # Total issues should be at least 750
75
+ self.assertGreaterEqual(len(issues), 750)
76
+
77
+ # Syntax check: verify the fixed codebase compiles successfully as valid Python
78
+ try:
79
+ compile(fixed_code, "large_file.py", "exec")
80
+ except SyntaxError as e:
81
+ self.fail(f"Autofixer produced syntactically invalid Python code: {e}")
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()