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.
Files changed (91) hide show
  1. codeoptix/__init__.py +8 -0
  2. codeoptix/acp/__init__.py +33 -0
  3. codeoptix/acp/agent.py +209 -0
  4. codeoptix/acp/bridge.py +402 -0
  5. codeoptix/acp/client_adapter.py +312 -0
  6. codeoptix/acp/code_extractor.py +125 -0
  7. codeoptix/acp/orchestrator.py +349 -0
  8. codeoptix/acp/registry.py +294 -0
  9. codeoptix/adapters/__init__.py +18 -0
  10. codeoptix/adapters/base.py +50 -0
  11. codeoptix/adapters/basic.py +195 -0
  12. codeoptix/adapters/claude_code.py +218 -0
  13. codeoptix/adapters/codex.py +327 -0
  14. codeoptix/adapters/factory.py +56 -0
  15. codeoptix/adapters/gemini_cli.py +370 -0
  16. codeoptix/artifacts/__init__.py +5 -0
  17. codeoptix/artifacts/manager.py +193 -0
  18. codeoptix/behaviors/__init__.py +45 -0
  19. codeoptix/behaviors/base.py +81 -0
  20. codeoptix/behaviors/insecure_code.py +129 -0
  21. codeoptix/behaviors/plan_drift.py +192 -0
  22. codeoptix/behaviors/vacuous_tests.py +198 -0
  23. codeoptix/cli.py +1472 -0
  24. codeoptix/evaluation/__init__.py +23 -0
  25. codeoptix/evaluation/bloom_integration.py +271 -0
  26. codeoptix/evaluation/engine.py +274 -0
  27. codeoptix/evaluation/evaluators.py +308 -0
  28. codeoptix/evaluation/scenario_generator.py +222 -0
  29. codeoptix/evolution/__init__.py +7 -0
  30. codeoptix/evolution/engine.py +206 -0
  31. codeoptix/evolution/gepa_integration.py +149 -0
  32. codeoptix/evolution/proposer.py +185 -0
  33. codeoptix/linters/__init__.py +13 -0
  34. codeoptix/linters/bandit_linter.py +172 -0
  35. codeoptix/linters/base.py +105 -0
  36. codeoptix/linters/coverage_linter.py +156 -0
  37. codeoptix/linters/flake8_linter.py +156 -0
  38. codeoptix/linters/html_accessibility_linter.py +374 -0
  39. codeoptix/linters/language_detector.py +150 -0
  40. codeoptix/linters/mypy_linter.py +184 -0
  41. codeoptix/linters/pip_audit_linter.py +152 -0
  42. codeoptix/linters/pylint_linter.py +198 -0
  43. codeoptix/linters/ruff_linter.py +206 -0
  44. codeoptix/linters/runner.py +186 -0
  45. codeoptix/linters/safety_linter.py +184 -0
  46. codeoptix/reflection/__init__.py +6 -0
  47. codeoptix/reflection/engine.py +70 -0
  48. codeoptix/reflection/generator.py +209 -0
  49. codeoptix/utils/__init__.py +1 -0
  50. codeoptix/utils/config.py +91 -0
  51. codeoptix/utils/llm.py +332 -0
  52. codeoptix/utils/retry.py +133 -0
  53. codeoptix/vendor/__init__.py +2 -0
  54. codeoptix/vendor/bloom/README.md +26 -0
  55. codeoptix/vendor/bloom/__init__.py +11 -0
  56. codeoptix/vendor/bloom/globals.py +39 -0
  57. codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
  58. codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
  59. codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
  60. codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
  61. codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
  62. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
  63. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
  64. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
  65. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
  66. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
  67. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
  68. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
  69. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
  70. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
  71. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
  72. codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
  73. codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
  74. codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
  75. codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
  76. codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
  77. codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
  78. codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
  79. codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
  80. codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
  81. codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
  82. codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
  83. codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
  84. codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
  85. codeoptix/vendor/bloom/transcript_utils.py +440 -0
  86. codeoptix/vendor/bloom/utils.py +700 -0
  87. codeoptix-0.1.0.dist-info/METADATA +304 -0
  88. codeoptix-0.1.0.dist-info/RECORD +91 -0
  89. codeoptix-0.1.0.dist-info/WHEEL +4 -0
  90. codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
  91. 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
+ )