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,172 @@
|
|
|
1
|
+
"""Bandit security 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 BanditLinter(BaseLinter):
|
|
11
|
+
"""Bandit security linter."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: dict | None = None):
|
|
14
|
+
"""Initialize Bandit linter."""
|
|
15
|
+
super().__init__(config)
|
|
16
|
+
self.name = "bandit"
|
|
17
|
+
self.severity_map = {
|
|
18
|
+
"HIGH": Severity.HIGH,
|
|
19
|
+
"MEDIUM": Severity.MEDIUM,
|
|
20
|
+
"LOW": Severity.LOW,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def is_available(self) -> bool:
|
|
24
|
+
"""Check if bandit is available."""
|
|
25
|
+
try:
|
|
26
|
+
subprocess.run(
|
|
27
|
+
["bandit", "--version"],
|
|
28
|
+
capture_output=True,
|
|
29
|
+
timeout=5,
|
|
30
|
+
check=False,
|
|
31
|
+
)
|
|
32
|
+
return True
|
|
33
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
37
|
+
"""Run bandit on code."""
|
|
38
|
+
import time
|
|
39
|
+
|
|
40
|
+
start_time = time.time()
|
|
41
|
+
|
|
42
|
+
if not self.is_available():
|
|
43
|
+
return LinterResult(
|
|
44
|
+
linter=self.name,
|
|
45
|
+
success=False,
|
|
46
|
+
issues=[],
|
|
47
|
+
errors=["Bandit not found in PATH. Install with: pip install bandit"],
|
|
48
|
+
execution_time=0.0,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
path_obj = Path(path)
|
|
52
|
+
if not path_obj.exists():
|
|
53
|
+
return LinterResult(
|
|
54
|
+
linter=self.name,
|
|
55
|
+
success=False,
|
|
56
|
+
issues=[],
|
|
57
|
+
errors=[f"Path not found: {path}"],
|
|
58
|
+
execution_time=0.0,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Build command
|
|
62
|
+
cmd = ["bandit", "-f", "json", "-q", "-r"]
|
|
63
|
+
|
|
64
|
+
# Add specific files if provided
|
|
65
|
+
if files:
|
|
66
|
+
cmd.extend(files)
|
|
67
|
+
else:
|
|
68
|
+
cmd.append(str(path_obj))
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
cmd,
|
|
73
|
+
capture_output=True,
|
|
74
|
+
text=True,
|
|
75
|
+
timeout=60,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
execution_time = time.time() - start_time
|
|
80
|
+
return self.parse_output(
|
|
81
|
+
result.stdout,
|
|
82
|
+
result.stderr,
|
|
83
|
+
result.returncode,
|
|
84
|
+
execution_time,
|
|
85
|
+
)
|
|
86
|
+
except subprocess.TimeoutExpired:
|
|
87
|
+
return LinterResult(
|
|
88
|
+
linter=self.name,
|
|
89
|
+
success=False,
|
|
90
|
+
issues=[],
|
|
91
|
+
errors=["Bandit execution timed out"],
|
|
92
|
+
execution_time=60.0,
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return LinterResult(
|
|
96
|
+
linter=self.name,
|
|
97
|
+
success=False,
|
|
98
|
+
issues=[],
|
|
99
|
+
errors=[f"Bandit error: {e!s}"],
|
|
100
|
+
execution_time=time.time() - start_time,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def parse_output(
|
|
104
|
+
self,
|
|
105
|
+
output: str,
|
|
106
|
+
stderr: str,
|
|
107
|
+
returncode: int,
|
|
108
|
+
execution_time: float,
|
|
109
|
+
) -> LinterResult:
|
|
110
|
+
"""Parse bandit JSON output."""
|
|
111
|
+
issues = []
|
|
112
|
+
errors = []
|
|
113
|
+
|
|
114
|
+
if stderr:
|
|
115
|
+
errors.append(stderr)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(output)
|
|
119
|
+
|
|
120
|
+
# Bandit returns results in "results" key
|
|
121
|
+
for item in data.get("results", []):
|
|
122
|
+
severity = self.severity_map.get(
|
|
123
|
+
item.get("issue_severity", "LOW"),
|
|
124
|
+
Severity.LOW,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Bandit severity mapping
|
|
128
|
+
if item.get("issue_confidence", "LOW") == "HIGH" and severity == Severity.LOW:
|
|
129
|
+
severity = Severity.MEDIUM
|
|
130
|
+
|
|
131
|
+
issue = LinterIssue(
|
|
132
|
+
linter=self.name,
|
|
133
|
+
severity=severity,
|
|
134
|
+
message=item.get("issue_text", "Unknown issue"),
|
|
135
|
+
file=item.get("filename", ""),
|
|
136
|
+
line=item.get("line_number"),
|
|
137
|
+
code=item.get("code"),
|
|
138
|
+
rule_id=item.get("test_id"),
|
|
139
|
+
confidence=self._parse_confidence(item.get("issue_confidence")),
|
|
140
|
+
)
|
|
141
|
+
issues.append(issue)
|
|
142
|
+
|
|
143
|
+
return LinterResult(
|
|
144
|
+
linter=self.name,
|
|
145
|
+
success=returncode == 0 or len(issues) == 0,
|
|
146
|
+
issues=issues,
|
|
147
|
+
errors=errors,
|
|
148
|
+
execution_time=execution_time,
|
|
149
|
+
raw_output=output,
|
|
150
|
+
)
|
|
151
|
+
except json.JSONDecodeError:
|
|
152
|
+
errors.append("Failed to parse bandit JSON output")
|
|
153
|
+
return LinterResult(
|
|
154
|
+
linter=self.name,
|
|
155
|
+
success=False,
|
|
156
|
+
issues=issues,
|
|
157
|
+
errors=errors,
|
|
158
|
+
execution_time=execution_time,
|
|
159
|
+
raw_output=output,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _parse_confidence(self, confidence: str | None) -> float | None:
|
|
163
|
+
"""Parse confidence string to float."""
|
|
164
|
+
if not confidence:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
confidence_map = {
|
|
168
|
+
"HIGH": 0.9,
|
|
169
|
+
"MEDIUM": 0.6,
|
|
170
|
+
"LOW": 0.3,
|
|
171
|
+
}
|
|
172
|
+
return confidence_map.get(confidence.upper(), 0.5)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Base classes for linter integration."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Severity(str, Enum):
|
|
9
|
+
"""Issue severity levels."""
|
|
10
|
+
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
HIGH = "high"
|
|
13
|
+
MEDIUM = "medium"
|
|
14
|
+
LOW = "low"
|
|
15
|
+
INFO = "info"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class LinterIssue:
|
|
20
|
+
"""Represents a single linter issue."""
|
|
21
|
+
|
|
22
|
+
linter: str
|
|
23
|
+
severity: Severity
|
|
24
|
+
message: str
|
|
25
|
+
file: str
|
|
26
|
+
line: int | None = None
|
|
27
|
+
column: int | None = None
|
|
28
|
+
code: str | None = None
|
|
29
|
+
rule_id: str | None = None
|
|
30
|
+
confidence: float | None = None
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
"""Convert to dictionary."""
|
|
34
|
+
return {
|
|
35
|
+
"linter": self.linter,
|
|
36
|
+
"severity": self.severity.value,
|
|
37
|
+
"message": self.message,
|
|
38
|
+
"file": self.file,
|
|
39
|
+
"line": self.line,
|
|
40
|
+
"column": self.column,
|
|
41
|
+
"code": self.code,
|
|
42
|
+
"rule_id": self.rule_id,
|
|
43
|
+
"confidence": self.confidence,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class LinterResult:
|
|
49
|
+
"""Result from running a linter."""
|
|
50
|
+
|
|
51
|
+
linter: str
|
|
52
|
+
success: bool
|
|
53
|
+
issues: list[LinterIssue]
|
|
54
|
+
errors: list[str]
|
|
55
|
+
execution_time: float
|
|
56
|
+
raw_output: str | None = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def issue_count(self) -> int:
|
|
60
|
+
"""Get total issue count."""
|
|
61
|
+
return len(self.issues)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def critical_count(self) -> int:
|
|
65
|
+
"""Get critical issue count."""
|
|
66
|
+
return sum(1 for issue in self.issues if issue.severity == Severity.CRITICAL)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def high_count(self) -> int:
|
|
70
|
+
"""Get high severity issue count."""
|
|
71
|
+
return sum(1 for issue in self.issues if issue.severity == Severity.HIGH)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Convert to dictionary."""
|
|
75
|
+
return {
|
|
76
|
+
"linter": self.linter,
|
|
77
|
+
"success": self.success,
|
|
78
|
+
"issue_count": self.issue_count,
|
|
79
|
+
"critical_count": self.critical_count,
|
|
80
|
+
"high_count": self.high_count,
|
|
81
|
+
"issues": [issue.to_dict() for issue in self.issues],
|
|
82
|
+
"errors": self.errors,
|
|
83
|
+
"execution_time": self.execution_time,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BaseLinter:
|
|
88
|
+
"""Base class for linter implementations."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
91
|
+
"""Initialize linter."""
|
|
92
|
+
self.config = config or {}
|
|
93
|
+
self.name = self.__class__.__name__.replace("Linter", "").lower()
|
|
94
|
+
|
|
95
|
+
def is_available(self) -> bool:
|
|
96
|
+
"""Check if linter is available in PATH."""
|
|
97
|
+
raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
100
|
+
"""Run linter on code."""
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
def parse_output(self, output: str, stderr: str, returncode: int) -> LinterResult:
|
|
104
|
+
"""Parse linter output."""
|
|
105
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""coverage.py test coverage linter integration."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CoverageLinter(BaseLinter):
|
|
10
|
+
"""coverage.py test coverage analyzer."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: dict | None = None):
|
|
13
|
+
"""Initialize coverage linter."""
|
|
14
|
+
super().__init__(config)
|
|
15
|
+
self.name = "coverage"
|
|
16
|
+
self.min_coverage = self.config.get("min_coverage", 80.0)
|
|
17
|
+
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
"""Check if coverage is available."""
|
|
20
|
+
try:
|
|
21
|
+
subprocess.run(
|
|
22
|
+
["coverage", "--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 coverage analysis."""
|
|
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=["coverage not found in PATH. Install with: pip install coverage"],
|
|
43
|
+
execution_time=0.0,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
path_obj = Path(path)
|
|
47
|
+
if not path_obj.exists():
|
|
48
|
+
return LinterResult(
|
|
49
|
+
linter=self.name,
|
|
50
|
+
success=False,
|
|
51
|
+
issues=[],
|
|
52
|
+
errors=[f"Path not found: {path}"],
|
|
53
|
+
execution_time=0.0,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Coverage needs to be run after tests
|
|
57
|
+
# We'll check for existing coverage data or report
|
|
58
|
+
coverage_data_file = path_obj / ".coverage"
|
|
59
|
+
coverage_xml = path_obj / "coverage.xml"
|
|
60
|
+
path_obj / "htmlcov"
|
|
61
|
+
|
|
62
|
+
# Check if coverage data exists
|
|
63
|
+
if not coverage_data_file.exists() and not coverage_xml.exists():
|
|
64
|
+
return LinterResult(
|
|
65
|
+
linter=self.name,
|
|
66
|
+
success=True,
|
|
67
|
+
issues=[],
|
|
68
|
+
errors=["No coverage data found. Run tests with coverage first: pytest --cov"],
|
|
69
|
+
execution_time=0.0,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Generate report
|
|
74
|
+
cmd = ["coverage", "report", "--format=total"]
|
|
75
|
+
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
cmd,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
cwd=str(path_obj) if path_obj.is_dir() else str(path_obj.parent),
|
|
81
|
+
timeout=30,
|
|
82
|
+
check=False,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
execution_time = time.time() - start_time
|
|
86
|
+
return self.parse_output(
|
|
87
|
+
result.stdout,
|
|
88
|
+
result.stderr,
|
|
89
|
+
result.returncode,
|
|
90
|
+
execution_time,
|
|
91
|
+
)
|
|
92
|
+
except subprocess.TimeoutExpired:
|
|
93
|
+
return LinterResult(
|
|
94
|
+
linter=self.name,
|
|
95
|
+
success=False,
|
|
96
|
+
issues=[],
|
|
97
|
+
errors=["Coverage execution timed out"],
|
|
98
|
+
execution_time=30.0,
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return LinterResult(
|
|
102
|
+
linter=self.name,
|
|
103
|
+
success=False,
|
|
104
|
+
issues=[],
|
|
105
|
+
errors=[f"Coverage error: {e!s}"],
|
|
106
|
+
execution_time=time.time() - start_time,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def parse_output(
|
|
110
|
+
self,
|
|
111
|
+
output: str,
|
|
112
|
+
stderr: str,
|
|
113
|
+
returncode: int,
|
|
114
|
+
execution_time: float,
|
|
115
|
+
) -> LinterResult:
|
|
116
|
+
"""Parse coverage output."""
|
|
117
|
+
import re
|
|
118
|
+
|
|
119
|
+
issues = []
|
|
120
|
+
errors = []
|
|
121
|
+
|
|
122
|
+
if stderr:
|
|
123
|
+
errors.append(stderr)
|
|
124
|
+
|
|
125
|
+
# Parse coverage percentage
|
|
126
|
+
# Coverage format: "TOTAL XXX XX%"
|
|
127
|
+
pattern = r"TOTAL\s+\d+\s+(\d+)%"
|
|
128
|
+
match = re.search(pattern, output)
|
|
129
|
+
|
|
130
|
+
if match:
|
|
131
|
+
coverage_percent = float(match.group(1))
|
|
132
|
+
|
|
133
|
+
if coverage_percent < self.min_coverage:
|
|
134
|
+
severity = Severity.HIGH if coverage_percent < 50 else Severity.MEDIUM
|
|
135
|
+
|
|
136
|
+
issue = LinterIssue(
|
|
137
|
+
linter=self.name,
|
|
138
|
+
severity=severity,
|
|
139
|
+
message=f"Test coverage is {coverage_percent:.1f}%, below minimum {self.min_coverage}%",
|
|
140
|
+
file="coverage",
|
|
141
|
+
rule_id="COVERAGE-LOW",
|
|
142
|
+
)
|
|
143
|
+
issues.append(issue)
|
|
144
|
+
else:
|
|
145
|
+
# Try to parse from XML if available
|
|
146
|
+
# Or just report that coverage was checked
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
return LinterResult(
|
|
150
|
+
linter=self.name,
|
|
151
|
+
success=returncode == 0 and len(issues) == 0,
|
|
152
|
+
issues=issues,
|
|
153
|
+
errors=errors,
|
|
154
|
+
execution_time=execution_time,
|
|
155
|
+
raw_output=output,
|
|
156
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Flake8 style guide linter integration."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Flake8Linter(BaseLinter):
|
|
10
|
+
"""Flake8 style guide linter."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: dict | None = None):
|
|
13
|
+
"""Initialize Flake8 linter."""
|
|
14
|
+
super().__init__(config)
|
|
15
|
+
self.name = "flake8"
|
|
16
|
+
self.severity_map = {
|
|
17
|
+
"E": Severity.HIGH, # Error
|
|
18
|
+
"W": Severity.MEDIUM, # Warning
|
|
19
|
+
"F": Severity.HIGH, # Pyflakes
|
|
20
|
+
"C": Severity.LOW, # McCabe complexity
|
|
21
|
+
"N": Severity.LOW, # Naming
|
|
22
|
+
"B": Severity.MEDIUM, # flake8-bugbear
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def is_available(self) -> bool:
|
|
26
|
+
"""Check if flake8 is available."""
|
|
27
|
+
try:
|
|
28
|
+
subprocess.run(
|
|
29
|
+
["flake8", "--version"],
|
|
30
|
+
capture_output=True,
|
|
31
|
+
timeout=5,
|
|
32
|
+
check=False,
|
|
33
|
+
)
|
|
34
|
+
return True
|
|
35
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
39
|
+
"""Run flake8 on code."""
|
|
40
|
+
import time
|
|
41
|
+
|
|
42
|
+
start_time = time.time()
|
|
43
|
+
|
|
44
|
+
if not self.is_available():
|
|
45
|
+
return LinterResult(
|
|
46
|
+
linter=self.name,
|
|
47
|
+
success=False,
|
|
48
|
+
issues=[],
|
|
49
|
+
errors=["Flake8 not found in PATH. Install with: pip install flake8"],
|
|
50
|
+
execution_time=0.0,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
path_obj = Path(path)
|
|
54
|
+
if not path_obj.exists():
|
|
55
|
+
return LinterResult(
|
|
56
|
+
linter=self.name,
|
|
57
|
+
success=False,
|
|
58
|
+
issues=[],
|
|
59
|
+
errors=[f"Path not found: {path}"],
|
|
60
|
+
execution_time=0.0,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Build command
|
|
64
|
+
cmd = ["flake8"]
|
|
65
|
+
|
|
66
|
+
# Add specific files if provided
|
|
67
|
+
if files:
|
|
68
|
+
cmd.extend(files)
|
|
69
|
+
elif path_obj.is_file() or path_obj.is_dir():
|
|
70
|
+
cmd.append(str(path_obj))
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
cmd,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
timeout=60,
|
|
78
|
+
check=False,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
execution_time = time.time() - start_time
|
|
82
|
+
return self.parse_output(
|
|
83
|
+
result.stdout,
|
|
84
|
+
result.stderr,
|
|
85
|
+
result.returncode,
|
|
86
|
+
execution_time,
|
|
87
|
+
)
|
|
88
|
+
except subprocess.TimeoutExpired:
|
|
89
|
+
return LinterResult(
|
|
90
|
+
linter=self.name,
|
|
91
|
+
success=False,
|
|
92
|
+
issues=[],
|
|
93
|
+
errors=["Flake8 execution timed out"],
|
|
94
|
+
execution_time=60.0,
|
|
95
|
+
)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
return LinterResult(
|
|
98
|
+
linter=self.name,
|
|
99
|
+
success=False,
|
|
100
|
+
issues=[],
|
|
101
|
+
errors=[f"Flake8 error: {e!s}"],
|
|
102
|
+
execution_time=time.time() - start_time,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def parse_output(
|
|
106
|
+
self,
|
|
107
|
+
output: str,
|
|
108
|
+
stderr: str,
|
|
109
|
+
returncode: int,
|
|
110
|
+
execution_time: float,
|
|
111
|
+
) -> LinterResult:
|
|
112
|
+
"""Parse flake8 output."""
|
|
113
|
+
import re
|
|
114
|
+
|
|
115
|
+
issues = []
|
|
116
|
+
errors = []
|
|
117
|
+
|
|
118
|
+
if stderr:
|
|
119
|
+
errors.append(stderr)
|
|
120
|
+
|
|
121
|
+
# Flake8 format: filename:line:column: code message
|
|
122
|
+
pattern = r"(.+?):(\d+):(\d+):\s*([EWFNCB]\d+)\s+(.+?)$"
|
|
123
|
+
|
|
124
|
+
for line in output.split("\n"):
|
|
125
|
+
line = line.strip()
|
|
126
|
+
if not line:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
match = re.match(pattern, line)
|
|
130
|
+
if match:
|
|
131
|
+
file_path, line_num, col_num, code, message = match.groups()
|
|
132
|
+
|
|
133
|
+
# Determine severity from error code
|
|
134
|
+
error_type = code[0] if code else "W"
|
|
135
|
+
severity = self.severity_map.get(error_type, Severity.MEDIUM)
|
|
136
|
+
|
|
137
|
+
issue = LinterIssue(
|
|
138
|
+
linter=self.name,
|
|
139
|
+
severity=severity,
|
|
140
|
+
message=message.strip(),
|
|
141
|
+
file=file_path,
|
|
142
|
+
line=int(line_num) if line_num.isdigit() else None,
|
|
143
|
+
column=int(col_num) if col_num.isdigit() else None,
|
|
144
|
+
code=code,
|
|
145
|
+
rule_id=code,
|
|
146
|
+
)
|
|
147
|
+
issues.append(issue)
|
|
148
|
+
|
|
149
|
+
return LinterResult(
|
|
150
|
+
linter=self.name,
|
|
151
|
+
success=returncode == 0 or len(issues) == 0,
|
|
152
|
+
issues=issues,
|
|
153
|
+
errors=errors,
|
|
154
|
+
execution_time=execution_time,
|
|
155
|
+
raw_output=output,
|
|
156
|
+
)
|