autoai-codetrust 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.
- aicodetrustcore/__init__.py +8 -0
- aicodetrustcore/analyzers/__init__.py +29 -0
- aicodetrustcore/analyzers/consistency.py +255 -0
- aicodetrustcore/analyzers/correctness.py +330 -0
- aicodetrustcore/analyzers/dependency.py +249 -0
- aicodetrustcore/analyzers/hallucination.py +607 -0
- aicodetrustcore/analyzers/safety.py +253 -0
- aicodetrustcore/analyzers/test_coverage.py +222 -0
- aicodetrustcore/api.py +348 -0
- aicodetrustcore/cli.py +462 -0
- aicodetrustcore/server.py +476 -0
- aicodetrustcore/store.py +182 -0
- aicodetrustcore/types.py +158 -0
- autoai_codetrust-0.1.0.dist-info/METADATA +64 -0
- autoai_codetrust-0.1.0.dist-info/RECORD +17 -0
- autoai_codetrust-0.1.0.dist-info/WHEEL +4 -0
- autoai_codetrust-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""AICodeTrustScore -- Continuous trust scoring for AI-generated code.
|
|
2
|
+
|
|
3
|
+
Analyzes code produced by Cursor, Copilot, Claude, and other AI tools,
|
|
4
|
+
assigning a 0-100 trust score based on correctness, safety, test coverage,
|
|
5
|
+
dependency risk, consistency, and hallucination detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""AICodeTrustScore analyzers -- each module detects a specific class of trust issue."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .correctness import CorrectnessAnalyzer
|
|
6
|
+
from .safety import SafetyAnalyzer
|
|
7
|
+
from .test_coverage import TestCoverageAnalyzer
|
|
8
|
+
from .dependency import DependencyAnalyzer
|
|
9
|
+
from .consistency import ConsistencyAnalyzer
|
|
10
|
+
from .hallucination import HallucinationAnalyzer
|
|
11
|
+
|
|
12
|
+
ALL_ANALYZERS = [
|
|
13
|
+
CorrectnessAnalyzer,
|
|
14
|
+
SafetyAnalyzer,
|
|
15
|
+
TestCoverageAnalyzer,
|
|
16
|
+
DependencyAnalyzer,
|
|
17
|
+
ConsistencyAnalyzer,
|
|
18
|
+
HallucinationAnalyzer,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CorrectnessAnalyzer",
|
|
23
|
+
"SafetyAnalyzer",
|
|
24
|
+
"TestCoverageAnalyzer",
|
|
25
|
+
"DependencyAnalyzer",
|
|
26
|
+
"ConsistencyAnalyzer",
|
|
27
|
+
"HallucinationAnalyzer",
|
|
28
|
+
"ALL_ANALYZERS",
|
|
29
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Consistency analyzer -- checks if AI code matches existing project style.
|
|
2
|
+
|
|
3
|
+
Checks:
|
|
4
|
+
- Naming convention mismatch with existing code
|
|
5
|
+
- Different error handling patterns than the project uses
|
|
6
|
+
- Import style inconsistency
|
|
7
|
+
- Comment style mismatch
|
|
8
|
+
- Architecture layer violations
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..types import TrustIssue, IssueCategory, Severity
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_CODE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".go"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConsistencyAnalyzer:
|
|
23
|
+
"""Detects style and pattern inconsistencies in AI-generated code."""
|
|
24
|
+
|
|
25
|
+
name = "consistency"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def analyze_file(cls, file_path: Path, content: str) -> list[TrustIssue]:
|
|
29
|
+
"""Analyse a file for consistency issues."""
|
|
30
|
+
findings: list[TrustIssue] = []
|
|
31
|
+
|
|
32
|
+
if file_path.suffix not in _CODE_EXTENSIONS:
|
|
33
|
+
return findings
|
|
34
|
+
|
|
35
|
+
lines = content.splitlines()
|
|
36
|
+
rel_path = str(file_path)
|
|
37
|
+
is_python = file_path.suffix == ".py"
|
|
38
|
+
is_js_ts = file_path.suffix in {".ts", ".tsx", ".js", ".jsx"}
|
|
39
|
+
|
|
40
|
+
# Naming convention checks
|
|
41
|
+
if is_python:
|
|
42
|
+
findings.extend(cls._check_python_naming(rel_path, lines))
|
|
43
|
+
findings.extend(cls._check_python_import_style(rel_path, lines))
|
|
44
|
+
findings.extend(cls._check_python_string_style(rel_path, lines))
|
|
45
|
+
|
|
46
|
+
if is_js_ts:
|
|
47
|
+
findings.extend(cls._check_js_naming(rel_path, lines))
|
|
48
|
+
findings.extend(cls._check_js_import_style(rel_path, lines))
|
|
49
|
+
|
|
50
|
+
# Mixed tab/space indentation
|
|
51
|
+
findings.extend(cls._check_indentation(rel_path, lines))
|
|
52
|
+
|
|
53
|
+
return findings
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _check_python_naming(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
57
|
+
"""Check Python naming conventions (PEP 8)."""
|
|
58
|
+
findings: list[TrustIssue] = []
|
|
59
|
+
camel_case_var = re.compile(r'^\s*([a-z]+[A-Z]\w*)\s*=')
|
|
60
|
+
|
|
61
|
+
camel_count = 0
|
|
62
|
+
for i, line in enumerate(lines, 1):
|
|
63
|
+
m = camel_case_var.match(line)
|
|
64
|
+
if m:
|
|
65
|
+
var_name = m.group(1)
|
|
66
|
+
# Exclude common abbreviations
|
|
67
|
+
if not re.match(r'^(is|has|can|should|will)', var_name):
|
|
68
|
+
camel_count += 1
|
|
69
|
+
if camel_count <= 3: # Report first few only
|
|
70
|
+
findings.append(TrustIssue(
|
|
71
|
+
analyzer=cls.name,
|
|
72
|
+
category=IssueCategory.CONSISTENCY,
|
|
73
|
+
severity=Severity.LOW,
|
|
74
|
+
title=f"camelCase variable '{var_name}' in Python",
|
|
75
|
+
description="Python convention is snake_case for variables and functions (PEP 8).",
|
|
76
|
+
file_path=rel_path,
|
|
77
|
+
line_start=i,
|
|
78
|
+
suggestion=f"Rename to '{cls._to_snake_case(var_name)}'.",
|
|
79
|
+
code_snippet=line.strip()[:200],
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
# Classes not in PascalCase
|
|
83
|
+
class_pattern = re.compile(r'^\s*class\s+(\w+)')
|
|
84
|
+
for i, line in enumerate(lines, 1):
|
|
85
|
+
m = class_pattern.match(line)
|
|
86
|
+
if m:
|
|
87
|
+
name = m.group(1)
|
|
88
|
+
if "_" in name and not name.startswith("_"):
|
|
89
|
+
findings.append(TrustIssue(
|
|
90
|
+
analyzer=cls.name,
|
|
91
|
+
category=IssueCategory.CONSISTENCY,
|
|
92
|
+
severity=Severity.LOW,
|
|
93
|
+
title=f"Class '{name}' uses snake_case instead of PascalCase",
|
|
94
|
+
description="Python convention is PascalCase for class names (PEP 8).",
|
|
95
|
+
file_path=rel_path,
|
|
96
|
+
line_start=i,
|
|
97
|
+
suggestion=f"Rename to PascalCase.",
|
|
98
|
+
code_snippet=line.strip()[:200],
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
return findings
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _check_python_import_style(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
105
|
+
"""Check for inconsistent import styles."""
|
|
106
|
+
findings: list[TrustIssue] = []
|
|
107
|
+
|
|
108
|
+
has_wildcard = False
|
|
109
|
+
wildcard_line = 0
|
|
110
|
+
for i, line in enumerate(lines, 1):
|
|
111
|
+
stripped = line.strip()
|
|
112
|
+
if re.match(r'from\s+\w+\s+import\s+\*', stripped):
|
|
113
|
+
has_wildcard = True
|
|
114
|
+
wildcard_line = i
|
|
115
|
+
|
|
116
|
+
if has_wildcard:
|
|
117
|
+
findings.append(TrustIssue(
|
|
118
|
+
analyzer=cls.name,
|
|
119
|
+
category=IssueCategory.CONSISTENCY,
|
|
120
|
+
severity=Severity.MEDIUM,
|
|
121
|
+
title="Wildcard import (from x import *)",
|
|
122
|
+
description="Wildcard imports pollute the namespace and make it unclear what's available.",
|
|
123
|
+
file_path=rel_path,
|
|
124
|
+
line_start=wildcard_line,
|
|
125
|
+
suggestion="Import specific names: from module import Class1, func1",
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
return findings
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def _check_python_string_style(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
132
|
+
"""Check for mixed single/double quotes (inconsistency indicator)."""
|
|
133
|
+
findings: list[TrustIssue] = []
|
|
134
|
+
single_count = 0
|
|
135
|
+
double_count = 0
|
|
136
|
+
|
|
137
|
+
for line in lines:
|
|
138
|
+
stripped = line.strip()
|
|
139
|
+
if stripped.startswith("#"):
|
|
140
|
+
continue
|
|
141
|
+
# Count string literal starts (simple heuristic)
|
|
142
|
+
single_count += len(re.findall(r"(?<![\"\\])'(?!'')", stripped))
|
|
143
|
+
double_count += len(re.findall(r'(?<![\'\\])"(?!"")', stripped))
|
|
144
|
+
|
|
145
|
+
total = single_count + double_count
|
|
146
|
+
if total > 10: # Only check if there are enough strings
|
|
147
|
+
ratio = min(single_count, double_count) / max(single_count, double_count, 1)
|
|
148
|
+
if 0.3 < ratio < 0.7:
|
|
149
|
+
findings.append(TrustIssue(
|
|
150
|
+
analyzer=cls.name,
|
|
151
|
+
category=IssueCategory.CONSISTENCY,
|
|
152
|
+
severity=Severity.INFO,
|
|
153
|
+
title="Inconsistent string quote style",
|
|
154
|
+
description=f"Mixed use of single ({single_count}) and double ({double_count}) quotes. "
|
|
155
|
+
"Pick one style and use it consistently.",
|
|
156
|
+
file_path=rel_path,
|
|
157
|
+
line_start=1,
|
|
158
|
+
suggestion="Use a formatter like black (Python) or prettier (JS) to enforce consistent quotes.",
|
|
159
|
+
))
|
|
160
|
+
|
|
161
|
+
return findings
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def _check_js_naming(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
165
|
+
"""Check JS/TS naming conventions."""
|
|
166
|
+
findings: list[TrustIssue] = []
|
|
167
|
+
snake_case_var = re.compile(r'^\s*(?:const|let|var)\s+([a-z]+_[a-z_]+)\s*=')
|
|
168
|
+
|
|
169
|
+
snake_count = 0
|
|
170
|
+
for i, line in enumerate(lines, 1):
|
|
171
|
+
m = snake_case_var.match(line)
|
|
172
|
+
if m:
|
|
173
|
+
var_name = m.group(1)
|
|
174
|
+
snake_count += 1
|
|
175
|
+
if snake_count <= 3:
|
|
176
|
+
findings.append(TrustIssue(
|
|
177
|
+
analyzer=cls.name,
|
|
178
|
+
category=IssueCategory.CONSISTENCY,
|
|
179
|
+
severity=Severity.LOW,
|
|
180
|
+
title=f"snake_case variable '{var_name}' in JS/TS",
|
|
181
|
+
description="JavaScript/TypeScript convention is camelCase for variables.",
|
|
182
|
+
file_path=rel_path,
|
|
183
|
+
line_start=i,
|
|
184
|
+
suggestion=f"Rename to '{cls._to_camel_case(var_name)}'.",
|
|
185
|
+
code_snippet=line.strip()[:200],
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
return findings
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def _check_js_import_style(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
192
|
+
"""Check for mixed require/import in JS/TS."""
|
|
193
|
+
findings: list[TrustIssue] = []
|
|
194
|
+
has_import = False
|
|
195
|
+
has_require = False
|
|
196
|
+
|
|
197
|
+
for line in lines:
|
|
198
|
+
stripped = line.strip()
|
|
199
|
+
if re.match(r'import\s+', stripped):
|
|
200
|
+
has_import = True
|
|
201
|
+
if re.search(r'require\s*\(', stripped):
|
|
202
|
+
has_require = True
|
|
203
|
+
|
|
204
|
+
if has_import and has_require:
|
|
205
|
+
findings.append(TrustIssue(
|
|
206
|
+
analyzer=cls.name,
|
|
207
|
+
category=IssueCategory.CONSISTENCY,
|
|
208
|
+
severity=Severity.LOW,
|
|
209
|
+
title="Mixed import and require() statements",
|
|
210
|
+
description="File uses both ES module imports and CommonJS require() -- pick one style.",
|
|
211
|
+
file_path=rel_path,
|
|
212
|
+
line_start=1,
|
|
213
|
+
suggestion="Use ES modules (import/export) consistently in modern JS/TS projects.",
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return findings
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def _check_indentation(cls, rel_path: str, lines: list[str]) -> list[TrustIssue]:
|
|
220
|
+
"""Check for mixed tabs and spaces."""
|
|
221
|
+
findings: list[TrustIssue] = []
|
|
222
|
+
has_tabs = False
|
|
223
|
+
has_spaces = False
|
|
224
|
+
|
|
225
|
+
for line in lines:
|
|
226
|
+
if line.startswith("\t"):
|
|
227
|
+
has_tabs = True
|
|
228
|
+
elif line.startswith(" "):
|
|
229
|
+
has_spaces = True
|
|
230
|
+
|
|
231
|
+
if has_tabs and has_spaces:
|
|
232
|
+
findings.append(TrustIssue(
|
|
233
|
+
analyzer=cls.name,
|
|
234
|
+
category=IssueCategory.CONSISTENCY,
|
|
235
|
+
severity=Severity.MEDIUM,
|
|
236
|
+
title="Mixed tabs and spaces for indentation",
|
|
237
|
+
description="File uses both tabs and spaces for indentation -- this causes subtle bugs.",
|
|
238
|
+
file_path=rel_path,
|
|
239
|
+
line_start=1,
|
|
240
|
+
suggestion="Configure your editor to use consistent indentation (spaces recommended).",
|
|
241
|
+
))
|
|
242
|
+
|
|
243
|
+
return findings
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _to_snake_case(name: str) -> str:
|
|
247
|
+
"""Convert camelCase to snake_case."""
|
|
248
|
+
s = re.sub(r'([A-Z])', r'_\1', name).lower()
|
|
249
|
+
return s.lstrip("_")
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _to_camel_case(name: str) -> str:
|
|
253
|
+
"""Convert snake_case to camelCase."""
|
|
254
|
+
parts = name.split("_")
|
|
255
|
+
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Correctness analyzer -- detects common AI-generated code bugs.
|
|
2
|
+
|
|
3
|
+
Checks:
|
|
4
|
+
- Off-by-one patterns (loop bounds, array indexing)
|
|
5
|
+
- Null/undefined checks missing
|
|
6
|
+
- Type mismatches (comparing string to int)
|
|
7
|
+
- Unreachable conditions (always-true/false)
|
|
8
|
+
- Return type inconsistencies
|
|
9
|
+
- Error handling gaps (try without catch, catch without handling)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ..types import TrustIssue, IssueCategory, Severity
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_CODE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".go"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Off-by-one patterns
|
|
24
|
+
_OFF_BY_ONE_PATTERNS = [
|
|
25
|
+
# range(len(x)) when should be range(len(x) - 1) or vice versa
|
|
26
|
+
(re.compile(r'for\s+\w+\s+in\s+range\s*\(\s*len\s*\(\s*\w+\s*\)\s*\+\s*1\s*\)'),
|
|
27
|
+
"range(len(x) + 1) likely off-by-one -- will index past end of list"),
|
|
28
|
+
# arr[len(arr)] -- always out of bounds
|
|
29
|
+
(re.compile(r'\w+\[\s*len\s*\(\s*\w+\s*\)\s*\]'),
|
|
30
|
+
"Indexing at len(x) is always out of bounds -- last valid index is len(x)-1"),
|
|
31
|
+
# <= in range-based for loop (JS/TS/Go)
|
|
32
|
+
(re.compile(r'for\s*\(.*;\s*\w+\s*<=\s*\w+\.length\s*;'),
|
|
33
|
+
"Loop condition uses <= length instead of < length -- off-by-one error"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Missing null/undefined checks
|
|
37
|
+
_NULL_CHECK_PATTERNS = [
|
|
38
|
+
# Chained property access without optional chaining (JS/TS)
|
|
39
|
+
(re.compile(r'(?<![\?\.])\.\w+\.\w+\.\w+\.\w+'),
|
|
40
|
+
"Deep property access without null checks -- may throw on undefined intermediate"),
|
|
41
|
+
# .get() result used without None check (Python)
|
|
42
|
+
(re.compile(r'(\w+)\.get\([^)]+\)\.\w+'),
|
|
43
|
+
"Calling method on .get() result without None check -- .get() can return None"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Type mismatch patterns
|
|
47
|
+
_TYPE_MISMATCH_PATTERNS = [
|
|
48
|
+
# Comparing string to number without conversion
|
|
49
|
+
(re.compile(r'==\s*["\'][0-9]+["\']'),
|
|
50
|
+
"Comparing with string-encoded number -- may need int() or parseInt() conversion"),
|
|
51
|
+
# parseInt without radix
|
|
52
|
+
(re.compile(r'parseInt\s*\(\s*\w+\s*\)(?!\s*,)'),
|
|
53
|
+
"parseInt() without radix parameter -- always specify radix (e.g., parseInt(x, 10))"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Unreachable code patterns
|
|
57
|
+
_UNREACHABLE_PATTERNS = [
|
|
58
|
+
# if True / if False
|
|
59
|
+
(re.compile(r'if\s+True\s*:'),
|
|
60
|
+
"Condition is always True -- dead code branch or debugging leftover"),
|
|
61
|
+
(re.compile(r'if\s+False\s*:'),
|
|
62
|
+
"Condition is always False -- code block is unreachable"),
|
|
63
|
+
# while True without break (simple heuristic)
|
|
64
|
+
(re.compile(r'if\s+\w+\s*(?:==|!=)\s*\w+\s*and\s+\w+\s*(?:==|!=)\s*\w+.*:\s*$'),
|
|
65
|
+
None), # placeholder, checked by _check_tautology
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Error handling gaps
|
|
69
|
+
_ERROR_HANDLING_PATTERNS_PY = [
|
|
70
|
+
# bare except
|
|
71
|
+
(re.compile(r'except\s*:'),
|
|
72
|
+
"Bare except catches all exceptions including SystemExit and KeyboardInterrupt"),
|
|
73
|
+
# except Exception: pass
|
|
74
|
+
(re.compile(r'except\s+\w+.*:\s*\n\s*pass\s*$', re.MULTILINE),
|
|
75
|
+
"Exception caught but silently ignored with pass -- errors will be hidden"),
|
|
76
|
+
# try without except (Python)
|
|
77
|
+
(re.compile(r'try\s*:\s*\n(?:(?!\s*except|\s*finally).)*\n\s*finally', re.MULTILINE),
|
|
78
|
+
None), # try/finally is valid, skip
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
_ERROR_HANDLING_PATTERNS_JS = [
|
|
82
|
+
# empty catch block
|
|
83
|
+
(re.compile(r'catch\s*\([^)]*\)\s*\{\s*\}'),
|
|
84
|
+
"Empty catch block -- errors are silently swallowed"),
|
|
85
|
+
# catch with only console.log
|
|
86
|
+
(re.compile(r'catch\s*\([^)]*\)\s*\{\s*console\.log'),
|
|
87
|
+
"Catch block only logs the error but does not handle or re-throw it"),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Return type inconsistency
|
|
91
|
+
_RETURN_PATTERNS = [
|
|
92
|
+
# Function with both return value and bare return
|
|
93
|
+
(re.compile(r'def\s+\w+[^:]*:.*?\n(?:.*\n)*?\s+return\s+\S+.*\n(?:.*\n)*?\s+return\s*$', re.MULTILINE),
|
|
94
|
+
"Function has both value-returning and bare return statements -- inconsistent return type"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CorrectnessAnalyzer:
|
|
99
|
+
"""Detects correctness issues common in AI-generated code."""
|
|
100
|
+
|
|
101
|
+
name = "correctness"
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def analyze_file(cls, file_path: Path, content: str) -> list[TrustIssue]:
|
|
105
|
+
"""Analyse a single file for correctness issues."""
|
|
106
|
+
findings: list[TrustIssue] = []
|
|
107
|
+
|
|
108
|
+
if file_path.suffix not in _CODE_EXTENSIONS:
|
|
109
|
+
return findings
|
|
110
|
+
|
|
111
|
+
lines = content.splitlines()
|
|
112
|
+
rel_path = str(file_path)
|
|
113
|
+
is_python = file_path.suffix == ".py"
|
|
114
|
+
is_js_ts = file_path.suffix in {".ts", ".tsx", ".js", ".jsx"}
|
|
115
|
+
|
|
116
|
+
for i, line in enumerate(lines, 1):
|
|
117
|
+
stripped = line.strip()
|
|
118
|
+
|
|
119
|
+
# Skip comments
|
|
120
|
+
if stripped.startswith("#") or stripped.startswith("//") or stripped.startswith("*"):
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Off-by-one patterns
|
|
124
|
+
for pattern, desc in _OFF_BY_ONE_PATTERNS:
|
|
125
|
+
if pattern.search(line):
|
|
126
|
+
findings.append(TrustIssue(
|
|
127
|
+
analyzer=cls.name,
|
|
128
|
+
category=IssueCategory.CORRECTNESS,
|
|
129
|
+
severity=Severity.HIGH,
|
|
130
|
+
title="Potential off-by-one error",
|
|
131
|
+
description=desc,
|
|
132
|
+
file_path=rel_path,
|
|
133
|
+
line_start=i,
|
|
134
|
+
line_end=i,
|
|
135
|
+
suggestion="Check array/list bounds carefully. Last valid index is length-1.",
|
|
136
|
+
code_snippet=stripped[:200],
|
|
137
|
+
))
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
# Null check patterns
|
|
141
|
+
for pattern, desc in _NULL_CHECK_PATTERNS:
|
|
142
|
+
if pattern.search(line):
|
|
143
|
+
findings.append(TrustIssue(
|
|
144
|
+
analyzer=cls.name,
|
|
145
|
+
category=IssueCategory.CORRECTNESS,
|
|
146
|
+
severity=Severity.MEDIUM,
|
|
147
|
+
title="Missing null/undefined check",
|
|
148
|
+
description=desc,
|
|
149
|
+
file_path=rel_path,
|
|
150
|
+
line_start=i,
|
|
151
|
+
line_end=i,
|
|
152
|
+
suggestion="Add null checks or use optional chaining (?.) before accessing nested properties.",
|
|
153
|
+
code_snippet=stripped[:200],
|
|
154
|
+
))
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Type mismatch patterns
|
|
158
|
+
for pattern, desc in _TYPE_MISMATCH_PATTERNS:
|
|
159
|
+
if pattern.search(line):
|
|
160
|
+
findings.append(TrustIssue(
|
|
161
|
+
analyzer=cls.name,
|
|
162
|
+
category=IssueCategory.CORRECTNESS,
|
|
163
|
+
severity=Severity.MEDIUM,
|
|
164
|
+
title="Potential type mismatch",
|
|
165
|
+
description=desc,
|
|
166
|
+
file_path=rel_path,
|
|
167
|
+
line_start=i,
|
|
168
|
+
line_end=i,
|
|
169
|
+
suggestion="Ensure types match before comparison. Use explicit conversion.",
|
|
170
|
+
code_snippet=stripped[:200],
|
|
171
|
+
))
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
# Unreachable code (if True / if False)
|
|
175
|
+
if re.search(r'if\s+True\s*:', stripped):
|
|
176
|
+
findings.append(TrustIssue(
|
|
177
|
+
analyzer=cls.name,
|
|
178
|
+
category=IssueCategory.CORRECTNESS,
|
|
179
|
+
severity=Severity.LOW,
|
|
180
|
+
title="Always-true condition",
|
|
181
|
+
description="Condition is always True -- dead code branch or debugging leftover",
|
|
182
|
+
file_path=rel_path,
|
|
183
|
+
line_start=i,
|
|
184
|
+
line_end=i,
|
|
185
|
+
suggestion="Remove the always-true condition or replace with the actual logic.",
|
|
186
|
+
code_snippet=stripped[:200],
|
|
187
|
+
))
|
|
188
|
+
elif re.search(r'if\s+False\s*:', stripped):
|
|
189
|
+
findings.append(TrustIssue(
|
|
190
|
+
analyzer=cls.name,
|
|
191
|
+
category=IssueCategory.CORRECTNESS,
|
|
192
|
+
severity=Severity.MEDIUM,
|
|
193
|
+
title="Unreachable code block",
|
|
194
|
+
description="Condition is always False -- code block is unreachable",
|
|
195
|
+
file_path=rel_path,
|
|
196
|
+
line_start=i,
|
|
197
|
+
line_end=i,
|
|
198
|
+
suggestion="Remove the dead code block.",
|
|
199
|
+
code_snippet=stripped[:200],
|
|
200
|
+
))
|
|
201
|
+
|
|
202
|
+
# Error handling -- Python
|
|
203
|
+
if is_python:
|
|
204
|
+
if re.match(r'except\s*:', stripped):
|
|
205
|
+
findings.append(TrustIssue(
|
|
206
|
+
analyzer=cls.name,
|
|
207
|
+
category=IssueCategory.CORRECTNESS,
|
|
208
|
+
severity=Severity.HIGH,
|
|
209
|
+
title="Bare except clause",
|
|
210
|
+
description="Bare except catches all exceptions including SystemExit and KeyboardInterrupt",
|
|
211
|
+
file_path=rel_path,
|
|
212
|
+
line_start=i,
|
|
213
|
+
line_end=i,
|
|
214
|
+
suggestion="Use 'except Exception:' to avoid catching system-level exceptions.",
|
|
215
|
+
code_snippet=stripped[:200],
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
# Error handling -- JS/TS
|
|
219
|
+
if is_js_ts:
|
|
220
|
+
for pattern, desc in _ERROR_HANDLING_PATTERNS_JS:
|
|
221
|
+
if pattern.search(line):
|
|
222
|
+
findings.append(TrustIssue(
|
|
223
|
+
analyzer=cls.name,
|
|
224
|
+
category=IssueCategory.CORRECTNESS,
|
|
225
|
+
severity=Severity.MEDIUM,
|
|
226
|
+
title="Error handling gap",
|
|
227
|
+
description=desc,
|
|
228
|
+
file_path=rel_path,
|
|
229
|
+
line_start=i,
|
|
230
|
+
line_end=i,
|
|
231
|
+
suggestion="Handle errors properly: log, re-throw, or return an error state.",
|
|
232
|
+
code_snippet=stripped[:200],
|
|
233
|
+
))
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Multi-line checks: except ... pass pattern
|
|
237
|
+
if is_python:
|
|
238
|
+
findings.extend(cls._check_swallowed_exceptions(rel_path, content, lines))
|
|
239
|
+
|
|
240
|
+
# Return type consistency
|
|
241
|
+
if is_python:
|
|
242
|
+
findings.extend(cls._check_return_consistency(rel_path, content, lines))
|
|
243
|
+
|
|
244
|
+
return findings
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def _check_swallowed_exceptions(
|
|
248
|
+
cls, rel_path: str, content: str, lines: list[str]
|
|
249
|
+
) -> list[TrustIssue]:
|
|
250
|
+
"""Detect except blocks that silently swallow errors with pass."""
|
|
251
|
+
findings: list[TrustIssue] = []
|
|
252
|
+
for i, line in enumerate(lines, 1):
|
|
253
|
+
stripped = line.strip()
|
|
254
|
+
if re.match(r'except\s+\w+', stripped) and i < len(lines):
|
|
255
|
+
next_line = lines[i].strip() if i < len(lines) else ""
|
|
256
|
+
if next_line == "pass":
|
|
257
|
+
findings.append(TrustIssue(
|
|
258
|
+
analyzer=cls.name,
|
|
259
|
+
category=IssueCategory.CORRECTNESS,
|
|
260
|
+
severity=Severity.MEDIUM,
|
|
261
|
+
title="Swallowed exception",
|
|
262
|
+
description="Exception caught but silently ignored with pass -- errors will be hidden",
|
|
263
|
+
file_path=rel_path,
|
|
264
|
+
line_start=i,
|
|
265
|
+
line_end=i + 1,
|
|
266
|
+
suggestion="At minimum, log the exception. Consider re-raising or returning an error.",
|
|
267
|
+
code_snippet=stripped[:200],
|
|
268
|
+
))
|
|
269
|
+
return findings
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
def _check_return_consistency(
|
|
273
|
+
cls, rel_path: str, content: str, lines: list[str]
|
|
274
|
+
) -> list[TrustIssue]:
|
|
275
|
+
"""Detect functions with inconsistent return types (value + bare return)."""
|
|
276
|
+
findings: list[TrustIssue] = []
|
|
277
|
+
func_pattern = re.compile(r'^(\s*)def\s+(\w+)')
|
|
278
|
+
in_func = False
|
|
279
|
+
func_name = ""
|
|
280
|
+
func_indent = ""
|
|
281
|
+
func_line = 0
|
|
282
|
+
has_value_return = False
|
|
283
|
+
has_bare_return = False
|
|
284
|
+
|
|
285
|
+
for i, line in enumerate(lines, 1):
|
|
286
|
+
m = func_pattern.match(line)
|
|
287
|
+
if m:
|
|
288
|
+
# Evaluate previous function
|
|
289
|
+
if in_func and has_value_return and has_bare_return:
|
|
290
|
+
findings.append(TrustIssue(
|
|
291
|
+
analyzer=cls.name,
|
|
292
|
+
category=IssueCategory.CORRECTNESS,
|
|
293
|
+
severity=Severity.MEDIUM,
|
|
294
|
+
title="Inconsistent return type",
|
|
295
|
+
description=f"Function '{func_name}' has both value-returning and bare return statements",
|
|
296
|
+
file_path=rel_path,
|
|
297
|
+
line_start=func_line,
|
|
298
|
+
line_end=i - 1,
|
|
299
|
+
suggestion="Ensure all return paths return the same type or use Optional.",
|
|
300
|
+
))
|
|
301
|
+
in_func = True
|
|
302
|
+
func_name = m.group(2)
|
|
303
|
+
func_indent = m.group(1)
|
|
304
|
+
func_line = i
|
|
305
|
+
has_value_return = False
|
|
306
|
+
has_bare_return = False
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
if in_func:
|
|
310
|
+
stripped = line.strip()
|
|
311
|
+
if stripped.startswith("return ") and stripped != "return":
|
|
312
|
+
has_value_return = True
|
|
313
|
+
elif stripped == "return":
|
|
314
|
+
has_bare_return = True
|
|
315
|
+
|
|
316
|
+
# Check last function
|
|
317
|
+
if in_func and has_value_return and has_bare_return:
|
|
318
|
+
findings.append(TrustIssue(
|
|
319
|
+
analyzer=cls.name,
|
|
320
|
+
category=IssueCategory.CORRECTNESS,
|
|
321
|
+
severity=Severity.MEDIUM,
|
|
322
|
+
title="Inconsistent return type",
|
|
323
|
+
description=f"Function '{func_name}' has both value-returning and bare return statements",
|
|
324
|
+
file_path=rel_path,
|
|
325
|
+
line_start=func_line,
|
|
326
|
+
line_end=len(lines),
|
|
327
|
+
suggestion="Ensure all return paths return the same type or use Optional.",
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
return findings
|