autoai-codetrust 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autoai_codetrust-0.1.0/.gitignore +7 -0
- autoai_codetrust-0.1.0/.hypothesis/unicode_data/13.0.0/charmap.json.gz +0 -0
- autoai_codetrust-0.1.0/Dockerfile +24 -0
- autoai_codetrust-0.1.0/PKG-INFO +64 -0
- autoai_codetrust-0.1.0/README.md +32 -0
- autoai_codetrust-0.1.0/data/codetrust.db +0 -0
- autoai_codetrust-0.1.0/github-action/action.yml +144 -0
- autoai_codetrust-0.1.0/pyproject.toml +58 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/__init__.py +8 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/__init__.py +29 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/consistency.py +255 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/correctness.py +330 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/dependency.py +249 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/hallucination.py +607 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/safety.py +253 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/analyzers/test_coverage.py +222 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/api.py +348 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/cli.py +462 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/server.py +476 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/store.py +182 -0
- autoai_codetrust-0.1.0/src/aicodetrustcore/types.py +158 -0
- autoai_codetrust-0.1.0/tests/__init__.py +0 -0
- autoai_codetrust-0.1.0/tests/test_analyzers.py +888 -0
- autoai_codetrust-0.1.0/tests/test_api.py +251 -0
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install dependencies first (cache layer)
|
|
6
|
+
COPY pyproject.toml README.md ./
|
|
7
|
+
COPY src/ src/
|
|
8
|
+
RUN pip install --no-cache-dir . && \
|
|
9
|
+
pip install --no-cache-dir gunicorn
|
|
10
|
+
|
|
11
|
+
# Create data directory for SQLite
|
|
12
|
+
RUN mkdir -p /app/data
|
|
13
|
+
|
|
14
|
+
ENV CODETRUST_DB_PATH=/app/data/codetrust.db
|
|
15
|
+
ENV CODETRUST_HOST=0.0.0.0
|
|
16
|
+
ENV CODETRUST_PORT=8080
|
|
17
|
+
|
|
18
|
+
EXPOSE 8080
|
|
19
|
+
|
|
20
|
+
CMD ["gunicorn", "aicodetrustcore.api:app", \
|
|
21
|
+
"-k", "uvicorn.workers.UvicornWorker", \
|
|
22
|
+
"-b", "0.0.0.0:8080", \
|
|
23
|
+
"--workers", "2", \
|
|
24
|
+
"--timeout", "120"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autoai-codetrust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI code security scanner — catches vulnerabilities, hallucinated dependencies, and compliance issues in AI-generated code. Snyk, but built for the AI coding era.
|
|
5
|
+
Project-URL: Homepage, https://codetrust.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/autoailabadmin/codetrust
|
|
7
|
+
Project-URL: Documentation, https://codetrust.dev/docs
|
|
8
|
+
Author-email: AutoAI Labs <info@autoailabs.co.uk>
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: ai-code,ai-scanner,claude,code-quality,code-security,copilot,cursor,hallucination-detection,mcp,sast,security,snyk-alternative,vulnerability-scanner
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: click>=8.0
|
|
22
|
+
Requires-Dist: fastapi>=0.110.0
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# AICodeTrustScore
|
|
34
|
+
|
|
35
|
+
Continuous trust scoring for AI-generated code. Analyzes code from Cursor, Copilot, Claude, and other AI tools, assigning a 0-100 trust score with hallucination detection.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install autoai-aicodetrustcore
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Score a file
|
|
47
|
+
aicodetrustcore score path/to/file.py
|
|
48
|
+
|
|
49
|
+
# Score a git diff
|
|
50
|
+
aicodetrustcore diff
|
|
51
|
+
|
|
52
|
+
# Full report for a directory
|
|
53
|
+
aicodetrustcore report path/to/project/
|
|
54
|
+
|
|
55
|
+
# Check for AI hallucinations
|
|
56
|
+
aicodetrustcore hallucination-check path/to/file.py
|
|
57
|
+
|
|
58
|
+
# Start MCP server
|
|
59
|
+
aicodetrustcore serve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
Apache-2.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# AICodeTrustScore
|
|
2
|
+
|
|
3
|
+
Continuous trust scoring for AI-generated code. Analyzes code from Cursor, Copilot, Claude, and other AI tools, assigning a 0-100 trust score with hallucination detection.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install autoai-aicodetrustcore
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Score a file
|
|
15
|
+
aicodetrustcore score path/to/file.py
|
|
16
|
+
|
|
17
|
+
# Score a git diff
|
|
18
|
+
aicodetrustcore diff
|
|
19
|
+
|
|
20
|
+
# Full report for a directory
|
|
21
|
+
aicodetrustcore report path/to/project/
|
|
22
|
+
|
|
23
|
+
# Check for AI hallucinations
|
|
24
|
+
aicodetrustcore hallucination-check path/to/file.py
|
|
25
|
+
|
|
26
|
+
# Start MCP server
|
|
27
|
+
aicodetrustcore serve
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
Apache-2.0
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
name: 'CodeTrust AI Code Scanner'
|
|
2
|
+
description: 'Scan AI-generated code for security vulnerabilities, hallucinated dependencies, and quality issues. Snyk, but built for the AI coding era.'
|
|
3
|
+
author: 'AutoAI Labs'
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: 'shield'
|
|
7
|
+
color: 'blue'
|
|
8
|
+
|
|
9
|
+
inputs:
|
|
10
|
+
api-key:
|
|
11
|
+
description: 'CodeTrust API key (get one at codetrust.dev)'
|
|
12
|
+
required: false
|
|
13
|
+
default: ''
|
|
14
|
+
scan-mode:
|
|
15
|
+
description: 'Scan mode: "diff" (only changed files) or "full" (entire repo)'
|
|
16
|
+
required: false
|
|
17
|
+
default: 'diff'
|
|
18
|
+
fail-on:
|
|
19
|
+
description: 'Fail the check if score is below this threshold (0-100). Set to 0 to never fail.'
|
|
20
|
+
required: false
|
|
21
|
+
default: '50'
|
|
22
|
+
languages:
|
|
23
|
+
description: 'Comma-separated languages to scan (python,typescript,javascript,go). Default: all.'
|
|
24
|
+
required: false
|
|
25
|
+
default: 'all'
|
|
26
|
+
api-url:
|
|
27
|
+
description: 'CodeTrust API URL (for self-hosted installations)'
|
|
28
|
+
required: false
|
|
29
|
+
default: 'https://codetrust-api.autoailabs.co.uk'
|
|
30
|
+
|
|
31
|
+
outputs:
|
|
32
|
+
score:
|
|
33
|
+
description: 'Overall trust score (0-100)'
|
|
34
|
+
value: ${{ steps.scan.outputs.score }}
|
|
35
|
+
category:
|
|
36
|
+
description: 'Trust category: ship_it, review, needs_work, dont_ship'
|
|
37
|
+
value: ${{ steps.scan.outputs.category }}
|
|
38
|
+
total-issues:
|
|
39
|
+
description: 'Total number of issues found'
|
|
40
|
+
value: ${{ steps.scan.outputs.total_issues }}
|
|
41
|
+
critical-issues:
|
|
42
|
+
description: 'Number of critical issues'
|
|
43
|
+
value: ${{ steps.scan.outputs.critical_issues }}
|
|
44
|
+
report-url:
|
|
45
|
+
description: 'URL to the full report on codetrust.dev'
|
|
46
|
+
value: ${{ steps.scan.outputs.report_url }}
|
|
47
|
+
|
|
48
|
+
runs:
|
|
49
|
+
using: 'composite'
|
|
50
|
+
steps:
|
|
51
|
+
- name: Set up Python
|
|
52
|
+
uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: '3.12'
|
|
55
|
+
|
|
56
|
+
- name: Install CodeTrust
|
|
57
|
+
shell: bash
|
|
58
|
+
run: pip install codetrust --quiet
|
|
59
|
+
|
|
60
|
+
- name: Run CodeTrust Scan
|
|
61
|
+
id: scan
|
|
62
|
+
shell: bash
|
|
63
|
+
env:
|
|
64
|
+
CODETRUST_API_KEY: ${{ inputs.api-key }}
|
|
65
|
+
CODETRUST_API_URL: ${{ inputs.api-url }}
|
|
66
|
+
run: |
|
|
67
|
+
set -e
|
|
68
|
+
|
|
69
|
+
SCAN_MODE="${{ inputs.scan-mode }}"
|
|
70
|
+
FAIL_THRESHOLD="${{ inputs.fail-on }}"
|
|
71
|
+
|
|
72
|
+
if [ "$SCAN_MODE" = "diff" ]; then
|
|
73
|
+
# Get the diff for this PR or push
|
|
74
|
+
if [ -n "${{ github.event.pull_request.base.sha }}" ]; then
|
|
75
|
+
DIFF=$(git diff ${{ github.event.pull_request.base.sha }}..HEAD)
|
|
76
|
+
else
|
|
77
|
+
DIFF=$(git diff HEAD~1..HEAD 2>/dev/null || git diff HEAD)
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if [ -z "$DIFF" ]; then
|
|
81
|
+
echo "No code changes detected."
|
|
82
|
+
echo "score=100" >> "$GITHUB_OUTPUT"
|
|
83
|
+
echo "category=ship_it" >> "$GITHUB_OUTPUT"
|
|
84
|
+
echo "total_issues=0" >> "$GITHUB_OUTPUT"
|
|
85
|
+
echo "critical_issues=0" >> "$GITHUB_OUTPUT"
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Run scan via CLI with JSON output
|
|
90
|
+
RESULT=$(echo "$DIFF" | codetrust diff --json-output 2>/dev/null || echo '{"score": 0, "trust_category": "dont_ship", "total_issues": -1}')
|
|
91
|
+
else
|
|
92
|
+
RESULT=$(codetrust report . --json-output 2>/dev/null || echo '{"score": 0, "trust_category": "dont_ship", "total_issues": -1}')
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Parse results
|
|
96
|
+
SCORE=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(int(d.get('score',0)))")
|
|
97
|
+
CATEGORY=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('trust_category','unknown'))")
|
|
98
|
+
TOTAL=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('total_issues',0))")
|
|
99
|
+
CRITICAL=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len([i for i in d.get('issues',[]) if i.get('severity')=='critical']))")
|
|
100
|
+
|
|
101
|
+
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
|
|
102
|
+
echo "category=$CATEGORY" >> "$GITHUB_OUTPUT"
|
|
103
|
+
echo "total_issues=$TOTAL" >> "$GITHUB_OUTPUT"
|
|
104
|
+
echo "critical_issues=$CRITICAL" >> "$GITHUB_OUTPUT"
|
|
105
|
+
|
|
106
|
+
# Print summary
|
|
107
|
+
echo "## CodeTrust Scan Results" >> "$GITHUB_STEP_SUMMARY"
|
|
108
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
109
|
+
echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY"
|
|
110
|
+
echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
|
|
111
|
+
echo "| **Trust Score** | **$SCORE/100** |" >> "$GITHUB_STEP_SUMMARY"
|
|
112
|
+
echo "| Category | $CATEGORY |" >> "$GITHUB_STEP_SUMMARY"
|
|
113
|
+
echo "| Total Issues | $TOTAL |" >> "$GITHUB_STEP_SUMMARY"
|
|
114
|
+
echo "| Critical Issues | $CRITICAL |" >> "$GITHUB_STEP_SUMMARY"
|
|
115
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
116
|
+
|
|
117
|
+
# Print issues if any
|
|
118
|
+
if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
|
|
119
|
+
echo "### Issues Found" >> "$GITHUB_STEP_SUMMARY"
|
|
120
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
121
|
+
echo "$RESULT" | python3 -c "
|
|
122
|
+
import sys, json
|
|
123
|
+
d = json.load(sys.stdin)
|
|
124
|
+
for i in d.get('issues', [])[:20]:
|
|
125
|
+
sev = i.get('severity', 'info').upper()
|
|
126
|
+
title = i.get('title', 'Unknown')
|
|
127
|
+
desc = i.get('description', '')
|
|
128
|
+
line = i.get('line_start', '?')
|
|
129
|
+
fp = i.get('file_path', '?')
|
|
130
|
+
suggestion = i.get('suggestion', '')
|
|
131
|
+
print(f'- **[{sev}]** {title} ({fp}:{line})')
|
|
132
|
+
if suggestion:
|
|
133
|
+
print(f' - Fix: {suggestion}')
|
|
134
|
+
" >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
138
|
+
echo "*Scanned by [CodeTrust](https://codetrust.dev) — AI code security by AutoAI Labs*" >> "$GITHUB_STEP_SUMMARY"
|
|
139
|
+
|
|
140
|
+
# Fail if below threshold
|
|
141
|
+
if [ "$SCORE" -lt "$FAIL_THRESHOLD" ] 2>/dev/null; then
|
|
142
|
+
echo "::error::CodeTrust score ($SCORE) is below threshold ($FAIL_THRESHOLD). Fix the issues above."
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "autoai-codetrust"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI code security scanner — catches vulnerabilities, hallucinated dependencies, and compliance issues in AI-generated code. Snyk, but built for the AI coding era."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "AutoAI Labs", email = "info@autoailabs.co.uk"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ai-code", "code-security", "hallucination-detection", "ai-scanner",
|
|
17
|
+
"code-quality", "security", "sast", "copilot", "cursor", "claude",
|
|
18
|
+
"snyk-alternative", "vulnerability-scanner", "mcp",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: Apache Software License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
29
|
+
"Topic :: Security",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"mcp>=1.0.0",
|
|
33
|
+
"click>=8.0",
|
|
34
|
+
"rich>=13.0",
|
|
35
|
+
"fastapi>=0.110.0",
|
|
36
|
+
"uvicorn[standard]>=0.29.0",
|
|
37
|
+
"python-multipart>=0.0.9",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=7.0",
|
|
43
|
+
"pytest-asyncio>=0.21",
|
|
44
|
+
"httpx>=0.27.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
codetrust = "aicodetrustcore.cli:main"
|
|
49
|
+
codetrust-api = "aicodetrustcore.api:main"
|
|
50
|
+
codetrust-mcp = "aicodetrustcore.server:main"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://codetrust.dev"
|
|
54
|
+
Repository = "https://github.com/autoailabadmin/codetrust"
|
|
55
|
+
Documentation = "https://codetrust.dev/docs"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/aicodetrustcore"]
|
|
@@ -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:])
|