codeoptix 0.1.0__py3-none-any.whl
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.
- codeoptix/__init__.py +8 -0
- codeoptix/acp/__init__.py +33 -0
- codeoptix/acp/agent.py +209 -0
- codeoptix/acp/bridge.py +402 -0
- codeoptix/acp/client_adapter.py +312 -0
- codeoptix/acp/code_extractor.py +125 -0
- codeoptix/acp/orchestrator.py +349 -0
- codeoptix/acp/registry.py +294 -0
- codeoptix/adapters/__init__.py +18 -0
- codeoptix/adapters/base.py +50 -0
- codeoptix/adapters/basic.py +195 -0
- codeoptix/adapters/claude_code.py +218 -0
- codeoptix/adapters/codex.py +327 -0
- codeoptix/adapters/factory.py +56 -0
- codeoptix/adapters/gemini_cli.py +370 -0
- codeoptix/artifacts/__init__.py +5 -0
- codeoptix/artifacts/manager.py +193 -0
- codeoptix/behaviors/__init__.py +45 -0
- codeoptix/behaviors/base.py +81 -0
- codeoptix/behaviors/insecure_code.py +129 -0
- codeoptix/behaviors/plan_drift.py +192 -0
- codeoptix/behaviors/vacuous_tests.py +198 -0
- codeoptix/cli.py +1472 -0
- codeoptix/evaluation/__init__.py +23 -0
- codeoptix/evaluation/bloom_integration.py +271 -0
- codeoptix/evaluation/engine.py +274 -0
- codeoptix/evaluation/evaluators.py +308 -0
- codeoptix/evaluation/scenario_generator.py +222 -0
- codeoptix/evolution/__init__.py +7 -0
- codeoptix/evolution/engine.py +206 -0
- codeoptix/evolution/gepa_integration.py +149 -0
- codeoptix/evolution/proposer.py +185 -0
- codeoptix/linters/__init__.py +13 -0
- codeoptix/linters/bandit_linter.py +172 -0
- codeoptix/linters/base.py +105 -0
- codeoptix/linters/coverage_linter.py +156 -0
- codeoptix/linters/flake8_linter.py +156 -0
- codeoptix/linters/html_accessibility_linter.py +374 -0
- codeoptix/linters/language_detector.py +150 -0
- codeoptix/linters/mypy_linter.py +184 -0
- codeoptix/linters/pip_audit_linter.py +152 -0
- codeoptix/linters/pylint_linter.py +198 -0
- codeoptix/linters/ruff_linter.py +206 -0
- codeoptix/linters/runner.py +186 -0
- codeoptix/linters/safety_linter.py +184 -0
- codeoptix/reflection/__init__.py +6 -0
- codeoptix/reflection/engine.py +70 -0
- codeoptix/reflection/generator.py +209 -0
- codeoptix/utils/__init__.py +1 -0
- codeoptix/utils/config.py +91 -0
- codeoptix/utils/llm.py +332 -0
- codeoptix/utils/retry.py +133 -0
- codeoptix/vendor/__init__.py +2 -0
- codeoptix/vendor/bloom/README.md +26 -0
- codeoptix/vendor/bloom/__init__.py +11 -0
- codeoptix/vendor/bloom/globals.py +39 -0
- codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
- codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
- codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
- codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
- codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
- codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
- codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
- codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
- codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
- codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
- codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
- codeoptix/vendor/bloom/transcript_utils.py +440 -0
- codeoptix/vendor/bloom/utils.py +700 -0
- codeoptix-0.1.0.dist-info/METADATA +304 -0
- codeoptix-0.1.0.dist-info/RECORD +91 -0
- codeoptix-0.1.0.dist-info/WHEEL +4 -0
- codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""pip-audit package vulnerability linter integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PipAuditLinter(BaseLinter):
|
|
11
|
+
"""pip-audit package vulnerability scanner."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: dict | None = None):
|
|
14
|
+
"""Initialize pip-audit linter."""
|
|
15
|
+
super().__init__(config)
|
|
16
|
+
self.name = "pip-audit"
|
|
17
|
+
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
"""Check if pip-audit is available."""
|
|
20
|
+
try:
|
|
21
|
+
subprocess.run(
|
|
22
|
+
["pip-audit", "--version"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
timeout=5,
|
|
25
|
+
check=False,
|
|
26
|
+
)
|
|
27
|
+
return True
|
|
28
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
32
|
+
"""Run pip-audit on packages."""
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
start_time = time.time()
|
|
36
|
+
|
|
37
|
+
if not self.is_available():
|
|
38
|
+
return LinterResult(
|
|
39
|
+
linter=self.name,
|
|
40
|
+
success=False,
|
|
41
|
+
issues=[],
|
|
42
|
+
errors=["pip-audit not found in PATH. Install with: pip install pip-audit"],
|
|
43
|
+
execution_time=0.0,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Build command
|
|
48
|
+
cmd = ["pip-audit", "--format=json"]
|
|
49
|
+
|
|
50
|
+
# Check for requirements file
|
|
51
|
+
path_obj = Path(path)
|
|
52
|
+
if path_obj.is_file() and path_obj.name in ["requirements.txt", "requirements-dev.txt"]:
|
|
53
|
+
cmd.extend(["--requirement", str(path_obj)])
|
|
54
|
+
elif path_obj.is_dir():
|
|
55
|
+
req_file = path_obj / "requirements.txt"
|
|
56
|
+
if req_file.exists():
|
|
57
|
+
cmd.extend(["--requirement", str(req_file)])
|
|
58
|
+
|
|
59
|
+
result = subprocess.run(
|
|
60
|
+
cmd,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
timeout=60,
|
|
64
|
+
check=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
execution_time = time.time() - start_time
|
|
68
|
+
return self.parse_output(
|
|
69
|
+
result.stdout,
|
|
70
|
+
result.stderr,
|
|
71
|
+
result.returncode,
|
|
72
|
+
execution_time,
|
|
73
|
+
)
|
|
74
|
+
except subprocess.TimeoutExpired:
|
|
75
|
+
return LinterResult(
|
|
76
|
+
linter=self.name,
|
|
77
|
+
success=False,
|
|
78
|
+
issues=[],
|
|
79
|
+
errors=["pip-audit execution timed out"],
|
|
80
|
+
execution_time=60.0,
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
return LinterResult(
|
|
84
|
+
linter=self.name,
|
|
85
|
+
success=False,
|
|
86
|
+
issues=[],
|
|
87
|
+
errors=[f"pip-audit error: {e!s}"],
|
|
88
|
+
execution_time=time.time() - start_time,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def parse_output(
|
|
92
|
+
self,
|
|
93
|
+
output: str,
|
|
94
|
+
stderr: str,
|
|
95
|
+
returncode: int,
|
|
96
|
+
execution_time: float,
|
|
97
|
+
) -> LinterResult:
|
|
98
|
+
"""Parse pip-audit JSON output."""
|
|
99
|
+
issues = []
|
|
100
|
+
errors = []
|
|
101
|
+
|
|
102
|
+
if stderr:
|
|
103
|
+
errors.append(stderr)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
data = json.loads(output) if output.strip() else {}
|
|
107
|
+
|
|
108
|
+
# pip-audit JSON format: {"vulnerabilities": [...]}
|
|
109
|
+
vulnerabilities = data.get("vulnerabilities", [])
|
|
110
|
+
|
|
111
|
+
for vuln in vulnerabilities:
|
|
112
|
+
name = vuln.get("name", "unknown")
|
|
113
|
+
installed_version = vuln.get("installed_version", "")
|
|
114
|
+
vuln_id = vuln.get("id", "")
|
|
115
|
+
fix_versions = vuln.get("fix_versions", [])
|
|
116
|
+
|
|
117
|
+
# Determine severity
|
|
118
|
+
severity = Severity.HIGH
|
|
119
|
+
if "critical" in str(vuln).lower():
|
|
120
|
+
severity = Severity.CRITICAL
|
|
121
|
+
|
|
122
|
+
message = f"{name} {installed_version}: {vuln_id}"
|
|
123
|
+
if fix_versions:
|
|
124
|
+
message += f" [Fix: {', '.join(fix_versions)}]"
|
|
125
|
+
|
|
126
|
+
issue = LinterIssue(
|
|
127
|
+
linter=self.name,
|
|
128
|
+
severity=severity,
|
|
129
|
+
message=message,
|
|
130
|
+
file="requirements.txt",
|
|
131
|
+
rule_id=vuln_id,
|
|
132
|
+
)
|
|
133
|
+
issues.append(issue)
|
|
134
|
+
|
|
135
|
+
return LinterResult(
|
|
136
|
+
linter=self.name,
|
|
137
|
+
success=returncode == 0 or len(issues) == 0,
|
|
138
|
+
issues=issues,
|
|
139
|
+
errors=errors,
|
|
140
|
+
execution_time=execution_time,
|
|
141
|
+
raw_output=output,
|
|
142
|
+
)
|
|
143
|
+
except json.JSONDecodeError:
|
|
144
|
+
errors.append("Failed to parse pip-audit JSON output")
|
|
145
|
+
return LinterResult(
|
|
146
|
+
linter=self.name,
|
|
147
|
+
success=False,
|
|
148
|
+
issues=issues,
|
|
149
|
+
errors=errors,
|
|
150
|
+
execution_time=execution_time,
|
|
151
|
+
raw_output=output,
|
|
152
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Pylint code quality linter integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PylintLinter(BaseLinter):
|
|
12
|
+
"""Pylint code quality linter."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: dict | None = None):
|
|
15
|
+
"""Initialize Pylint linter."""
|
|
16
|
+
super().__init__(config)
|
|
17
|
+
self.name = "pylint"
|
|
18
|
+
self.severity_map = {
|
|
19
|
+
"E": Severity.HIGH, # Error
|
|
20
|
+
"W": Severity.MEDIUM, # Warning
|
|
21
|
+
"C": Severity.LOW, # Convention
|
|
22
|
+
"R": Severity.LOW, # Refactor
|
|
23
|
+
"F": Severity.CRITICAL, # Fatal
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
"""Check if pylint is available."""
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run(
|
|
30
|
+
["pylint", "--version"],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
check=False,
|
|
34
|
+
)
|
|
35
|
+
return True
|
|
36
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
40
|
+
"""Run pylint on code."""
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
start_time = time.time()
|
|
44
|
+
|
|
45
|
+
if not self.is_available():
|
|
46
|
+
return LinterResult(
|
|
47
|
+
linter=self.name,
|
|
48
|
+
success=False,
|
|
49
|
+
issues=[],
|
|
50
|
+
errors=["Pylint not found in PATH. Install with: pip install pylint"],
|
|
51
|
+
execution_time=0.0,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
path_obj = Path(path)
|
|
55
|
+
if not path_obj.exists():
|
|
56
|
+
return LinterResult(
|
|
57
|
+
linter=self.name,
|
|
58
|
+
success=False,
|
|
59
|
+
issues=[],
|
|
60
|
+
errors=[f"Path not found: {path}"],
|
|
61
|
+
execution_time=0.0,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Build command
|
|
65
|
+
cmd = [
|
|
66
|
+
"pylint",
|
|
67
|
+
"--output-format=json",
|
|
68
|
+
"--disable=all",
|
|
69
|
+
"--enable=E,F,W,C,R", # Enable all message types
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Add specific files if provided
|
|
73
|
+
if files:
|
|
74
|
+
cmd.extend(files)
|
|
75
|
+
# Find Python files
|
|
76
|
+
elif (path_obj.is_file() and path_obj.suffix == ".py") or path_obj.is_dir():
|
|
77
|
+
cmd.append(str(path_obj))
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
cmd,
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=120,
|
|
85
|
+
check=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
execution_time = time.time() - start_time
|
|
89
|
+
return self.parse_output(
|
|
90
|
+
result.stdout,
|
|
91
|
+
result.stderr,
|
|
92
|
+
result.returncode,
|
|
93
|
+
execution_time,
|
|
94
|
+
)
|
|
95
|
+
except subprocess.TimeoutExpired:
|
|
96
|
+
return LinterResult(
|
|
97
|
+
linter=self.name,
|
|
98
|
+
success=False,
|
|
99
|
+
issues=[],
|
|
100
|
+
errors=["Pylint execution timed out"],
|
|
101
|
+
execution_time=120.0,
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return LinterResult(
|
|
105
|
+
linter=self.name,
|
|
106
|
+
success=False,
|
|
107
|
+
issues=[],
|
|
108
|
+
errors=[f"Pylint error: {e!s}"],
|
|
109
|
+
execution_time=time.time() - start_time,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def parse_output(
|
|
113
|
+
self,
|
|
114
|
+
output: str,
|
|
115
|
+
stderr: str,
|
|
116
|
+
returncode: int,
|
|
117
|
+
execution_time: float,
|
|
118
|
+
) -> LinterResult:
|
|
119
|
+
"""Parse pylint JSON output."""
|
|
120
|
+
issues = []
|
|
121
|
+
errors = []
|
|
122
|
+
|
|
123
|
+
if stderr:
|
|
124
|
+
errors.append(stderr)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
data = json.loads(output)
|
|
128
|
+
|
|
129
|
+
for item in data:
|
|
130
|
+
message_type = item.get("type", "W")
|
|
131
|
+
severity = self.severity_map.get(message_type, Severity.MEDIUM)
|
|
132
|
+
|
|
133
|
+
issue = LinterIssue(
|
|
134
|
+
linter=self.name,
|
|
135
|
+
severity=severity,
|
|
136
|
+
message=item.get("message", "Unknown issue"),
|
|
137
|
+
file=item.get("path", ""),
|
|
138
|
+
line=item.get("line"),
|
|
139
|
+
column=item.get("column"),
|
|
140
|
+
code=item.get("symbol"),
|
|
141
|
+
rule_id=item.get("message-id"),
|
|
142
|
+
)
|
|
143
|
+
issues.append(issue)
|
|
144
|
+
|
|
145
|
+
return LinterResult(
|
|
146
|
+
linter=self.name,
|
|
147
|
+
success=returncode == 0 or len(issues) == 0,
|
|
148
|
+
issues=issues,
|
|
149
|
+
errors=errors,
|
|
150
|
+
execution_time=execution_time,
|
|
151
|
+
raw_output=output,
|
|
152
|
+
)
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
# Try parsing text output as fallback
|
|
155
|
+
return self._parse_text_output(output, stderr, returncode, execution_time)
|
|
156
|
+
|
|
157
|
+
def _parse_text_output(
|
|
158
|
+
self,
|
|
159
|
+
output: str,
|
|
160
|
+
stderr: str,
|
|
161
|
+
returncode: int,
|
|
162
|
+
execution_time: float,
|
|
163
|
+
) -> LinterResult:
|
|
164
|
+
"""Parse pylint text output as fallback."""
|
|
165
|
+
issues = []
|
|
166
|
+
errors = []
|
|
167
|
+
|
|
168
|
+
if stderr:
|
|
169
|
+
errors.append(stderr)
|
|
170
|
+
|
|
171
|
+
# Pylint text format: filename:line:column: message-type: message (message-id)
|
|
172
|
+
pattern = r"(.+?):(\d+):(\d+):\s*([EWCRF]):\s*(.+?)\s*\((.+?)\)"
|
|
173
|
+
|
|
174
|
+
for line in output.split("\n"):
|
|
175
|
+
match = re.match(pattern, line)
|
|
176
|
+
if match:
|
|
177
|
+
file_path, line_num, col_num, msg_type, message, rule_id = match.groups()
|
|
178
|
+
severity = self.severity_map.get(msg_type, Severity.MEDIUM)
|
|
179
|
+
|
|
180
|
+
issue = LinterIssue(
|
|
181
|
+
linter=self.name,
|
|
182
|
+
severity=severity,
|
|
183
|
+
message=message.strip(),
|
|
184
|
+
file=file_path,
|
|
185
|
+
line=int(line_num) if line_num.isdigit() else None,
|
|
186
|
+
column=int(col_num) if col_num.isdigit() else None,
|
|
187
|
+
rule_id=rule_id,
|
|
188
|
+
)
|
|
189
|
+
issues.append(issue)
|
|
190
|
+
|
|
191
|
+
return LinterResult(
|
|
192
|
+
linter=self.name,
|
|
193
|
+
success=returncode == 0 or len(issues) == 0,
|
|
194
|
+
issues=issues,
|
|
195
|
+
errors=errors,
|
|
196
|
+
execution_time=execution_time,
|
|
197
|
+
raw_output=output,
|
|
198
|
+
)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Ruff linter integration - extremely fast Python linter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuffLinter(BaseLinter):
|
|
11
|
+
"""Ruff linter - extremely fast Python linter written in Rust."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: dict | None = None):
|
|
14
|
+
"""Initialize Ruff linter."""
|
|
15
|
+
super().__init__(config)
|
|
16
|
+
self.name = "ruff"
|
|
17
|
+
self.severity_map = {
|
|
18
|
+
"E": Severity.HIGH, # Error
|
|
19
|
+
"W": Severity.MEDIUM, # Warning
|
|
20
|
+
"F": Severity.HIGH, # Pyflakes
|
|
21
|
+
"I": Severity.LOW, # isort
|
|
22
|
+
"N": Severity.LOW, # pep8-naming
|
|
23
|
+
"UP": Severity.LOW, # pyupgrade
|
|
24
|
+
"B": Severity.MEDIUM, # flake8-bugbear
|
|
25
|
+
"C4": Severity.LOW, # flake8-comprehensions
|
|
26
|
+
"SIM": Severity.LOW, # flake8-simplify
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def is_available(self) -> bool:
|
|
30
|
+
"""Check if ruff is available."""
|
|
31
|
+
try:
|
|
32
|
+
subprocess.run(
|
|
33
|
+
["ruff", "--version"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
timeout=5,
|
|
36
|
+
check=False,
|
|
37
|
+
)
|
|
38
|
+
return True
|
|
39
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def _find_config_file(self, path: str) -> Path | None:
|
|
43
|
+
"""Find ruff configuration file (ruff.toml, pyproject.toml, .ruff.toml)."""
|
|
44
|
+
path_obj = Path(path)
|
|
45
|
+
|
|
46
|
+
# Check common config locations
|
|
47
|
+
config_files = [
|
|
48
|
+
"ruff.toml",
|
|
49
|
+
".ruff.toml",
|
|
50
|
+
"pyproject.toml",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Start from path and walk up
|
|
54
|
+
current = path_obj if path_obj.is_file() else path_obj.parent
|
|
55
|
+
while current != current.parent:
|
|
56
|
+
for config_file in config_files:
|
|
57
|
+
config_path = current / config_file
|
|
58
|
+
if config_path.exists():
|
|
59
|
+
return config_path
|
|
60
|
+
current = current.parent
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
65
|
+
"""Run ruff on code."""
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
|
|
70
|
+
if not self.is_available():
|
|
71
|
+
return LinterResult(
|
|
72
|
+
linter=self.name,
|
|
73
|
+
success=False,
|
|
74
|
+
issues=[],
|
|
75
|
+
errors=[
|
|
76
|
+
"Ruff not found in PATH. Install with: pip install ruff or uv tool install ruff"
|
|
77
|
+
],
|
|
78
|
+
execution_time=0.0,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
path_obj = Path(path)
|
|
82
|
+
if not path_obj.exists():
|
|
83
|
+
return LinterResult(
|
|
84
|
+
linter=self.name,
|
|
85
|
+
success=False,
|
|
86
|
+
issues=[],
|
|
87
|
+
errors=[f"Path not found: {path}"],
|
|
88
|
+
execution_time=0.0,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Find config file (use existing project config)
|
|
92
|
+
config_file = self._find_config_file(path)
|
|
93
|
+
|
|
94
|
+
# Build command
|
|
95
|
+
cmd = ["ruff", "check", "--output-format=json"]
|
|
96
|
+
|
|
97
|
+
# Use existing config if found
|
|
98
|
+
if config_file:
|
|
99
|
+
# Ruff automatically uses pyproject.toml or ruff.toml in project root
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# Add specific files if provided
|
|
103
|
+
if files:
|
|
104
|
+
cmd.extend(files)
|
|
105
|
+
elif (path_obj.is_file() and path_obj.suffix == ".py") or path_obj.is_dir():
|
|
106
|
+
cmd.append(str(path_obj))
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
cmd,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=60,
|
|
114
|
+
check=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
execution_time = time.time() - start_time
|
|
118
|
+
return self.parse_output(
|
|
119
|
+
result.stdout,
|
|
120
|
+
result.stderr,
|
|
121
|
+
result.returncode,
|
|
122
|
+
execution_time,
|
|
123
|
+
)
|
|
124
|
+
except subprocess.TimeoutExpired:
|
|
125
|
+
return LinterResult(
|
|
126
|
+
linter=self.name,
|
|
127
|
+
success=False,
|
|
128
|
+
issues=[],
|
|
129
|
+
errors=["Ruff execution timed out"],
|
|
130
|
+
execution_time=60.0,
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return LinterResult(
|
|
134
|
+
linter=self.name,
|
|
135
|
+
success=False,
|
|
136
|
+
issues=[],
|
|
137
|
+
errors=[f"Ruff error: {e!s}"],
|
|
138
|
+
execution_time=time.time() - start_time,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def parse_output(
|
|
142
|
+
self,
|
|
143
|
+
output: str,
|
|
144
|
+
stderr: str,
|
|
145
|
+
returncode: int,
|
|
146
|
+
execution_time: float,
|
|
147
|
+
) -> LinterResult:
|
|
148
|
+
"""Parse ruff JSON output."""
|
|
149
|
+
issues = []
|
|
150
|
+
errors = []
|
|
151
|
+
|
|
152
|
+
if stderr:
|
|
153
|
+
errors.append(stderr)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Ruff JSON output is a list of diagnostic objects
|
|
157
|
+
data = json.loads(output) if output.strip() else []
|
|
158
|
+
|
|
159
|
+
for item in data:
|
|
160
|
+
code = item.get("code", "")
|
|
161
|
+
severity_str = item.get("severity", "warning")
|
|
162
|
+
|
|
163
|
+
# Map ruff severity to our severity
|
|
164
|
+
if severity_str == "error":
|
|
165
|
+
severity = Severity.HIGH
|
|
166
|
+
elif severity_str == "warning":
|
|
167
|
+
severity = Severity.MEDIUM
|
|
168
|
+
else:
|
|
169
|
+
severity = Severity.LOW
|
|
170
|
+
|
|
171
|
+
# Determine severity from code prefix if available
|
|
172
|
+
if code:
|
|
173
|
+
code_prefix = code.split(".")[0] if "." in code else code
|
|
174
|
+
if code_prefix in self.severity_map:
|
|
175
|
+
severity = self.severity_map[code_prefix]
|
|
176
|
+
|
|
177
|
+
issue = LinterIssue(
|
|
178
|
+
linter=self.name,
|
|
179
|
+
severity=severity,
|
|
180
|
+
message=item.get("message", "Unknown issue"),
|
|
181
|
+
file=item.get("filename", ""),
|
|
182
|
+
line=item.get("location", {}).get("row"),
|
|
183
|
+
column=item.get("location", {}).get("column"),
|
|
184
|
+
code=code,
|
|
185
|
+
rule_id=code,
|
|
186
|
+
)
|
|
187
|
+
issues.append(issue)
|
|
188
|
+
|
|
189
|
+
return LinterResult(
|
|
190
|
+
linter=self.name,
|
|
191
|
+
success=returncode == 0 or len(issues) == 0,
|
|
192
|
+
issues=issues,
|
|
193
|
+
errors=errors,
|
|
194
|
+
execution_time=execution_time,
|
|
195
|
+
raw_output=output,
|
|
196
|
+
)
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
errors.append("Failed to parse ruff JSON output")
|
|
199
|
+
return LinterResult(
|
|
200
|
+
linter=self.name,
|
|
201
|
+
success=False,
|
|
202
|
+
issues=issues,
|
|
203
|
+
errors=errors,
|
|
204
|
+
execution_time=execution_time,
|
|
205
|
+
raw_output=output,
|
|
206
|
+
)
|