codehound 1.2.0__tar.gz → 1.3.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.
- {codehound-1.2.0 → codehound-1.3.0}/PKG-INFO +38 -3
- {codehound-1.2.0 → codehound-1.3.0}/README.md +37 -2
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/__init__.py +1 -1
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/cli.py +20 -16
- codehound-1.3.0/src/codehound/sarif.py +67 -0
- codehound-1.3.0/src/codehound/terminal.py +57 -0
- codehound-1.3.0/tests/test_output_formats.py +74 -0
- {codehound-1.2.0 → codehound-1.3.0}/.gitignore +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/LICENSE +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/pyproject.toml +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/__init__.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/asyncio_run_in_loop.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/blocking_async.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/datetime_utcnow.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/floating_task.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/floating_thread.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/get_event_loop.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/loop_closure_capture.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/mutable_defaults.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/resource_leak.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/checks/unawaited_coroutine.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/src/codehound/core.py +0 -0
- {codehound-1.2.0 → codehound-1.3.0}/tests/test_checks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codehound
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: An AST-based static analyzer that hunts real correctness and async-safety bugs in Python code.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kratos0718/codehound
|
|
6
6
|
Project-URL: Issues, https://github.com/kratos0718/codehound/issues
|
|
@@ -91,6 +91,9 @@ PYTHONPATH=src python -m codehound.cli scan path/to/project
|
|
|
91
91
|
# scan a project (skips tests/, docs/, examples/, vendored code by default)
|
|
92
92
|
codehound scan path/to/project
|
|
93
93
|
|
|
94
|
+
# scan multiple files/directories in one invocation (what pre-commit does)
|
|
95
|
+
codehound scan file1.py file2.py src/
|
|
96
|
+
|
|
94
97
|
# only run specific checks
|
|
95
98
|
codehound scan path/to/project --select CH001,CH006
|
|
96
99
|
|
|
@@ -98,6 +101,9 @@ codehound scan path/to/project --select CH001,CH006
|
|
|
98
101
|
codehound scan path/to/project --format json
|
|
99
102
|
codehound scan path/to/project --format csv
|
|
100
103
|
|
|
104
|
+
# GitHub Code Scanning (Security tab) can ingest this directly
|
|
105
|
+
codehound scan path/to/project --format sarif > results.sarif
|
|
106
|
+
|
|
101
107
|
# list every available check
|
|
102
108
|
codehound list
|
|
103
109
|
```
|
|
@@ -108,6 +114,29 @@ codehound list
|
|
|
108
114
|
- run: codehound scan src # fails the build on a regression
|
|
109
115
|
```
|
|
110
116
|
|
|
117
|
+
### GitHub Action
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
- uses: kratos0718/codehound@v1.2.0
|
|
121
|
+
with:
|
|
122
|
+
path: src
|
|
123
|
+
# select: CH001,CH006 # optional, defaults to all checks
|
|
124
|
+
# fail-on-findings: "false" # optional, report without failing the build
|
|
125
|
+
# upload-sarif: "false" # optional, skip the Code Scanning upload
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Uploads findings to the repo's **Security → Code Scanning** tab via SARIF, in addition to failing the step (unless `fail-on-findings: "false"`).
|
|
129
|
+
|
|
130
|
+
### pre-commit
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
repos:
|
|
134
|
+
- repo: https://github.com/kratos0718/codehound
|
|
135
|
+
rev: v1.2.0
|
|
136
|
+
hooks:
|
|
137
|
+
- id: codehound
|
|
138
|
+
```
|
|
139
|
+
|
|
111
140
|
---
|
|
112
141
|
|
|
113
142
|
## The checks
|
|
@@ -189,7 +218,9 @@ codehound/
|
|
|
189
218
|
├── core.py # file discovery, AST parsing, the Finding/Check contract,
|
|
190
219
|
│ # and a child→parent map so checks can ask "what's my
|
|
191
220
|
│ # enclosing function / am I inside a `with`?"
|
|
192
|
-
├── cli.py # `scan` / `list`, text|json|csv output, CI-friendly exit codes
|
|
221
|
+
├── cli.py # `scan` / `list`, text|json|csv|sarif output, CI-friendly exit codes
|
|
222
|
+
├── sarif.py # SARIF 2.1.0 output for GitHub Code Scanning
|
|
223
|
+
├── terminal.py # colored text output (auto-disabled for non-TTY / NO_COLOR)
|
|
193
224
|
└── checks/ # one small, independently-tested class per rule
|
|
194
225
|
├── blocking_async.py (CH001)
|
|
195
226
|
├── mutable_defaults.py (CH002)
|
|
@@ -227,10 +258,14 @@ Every check has paired tests: the buggy pattern *is* flagged, and the idiomatic
|
|
|
227
258
|
- [x] `asyncio.run()` inside a running loop — CH008
|
|
228
259
|
- [x] Non-daemon thread started without a join — CH009 (the thread analog of CH006)
|
|
229
260
|
- [x] Loop-variable closure capture in lambdas — CH010
|
|
261
|
+
- [x] Pre-commit hook — `.pre-commit-hooks.yaml`
|
|
262
|
+
- [x] GitHub Action — `action.yml`, uploads SARIF to Code Scanning
|
|
263
|
+
- [x] SARIF output — `--format sarif`
|
|
264
|
+
- [x] Colored terminal output (auto-disabled for non-TTY / `NO_COLOR`)
|
|
265
|
+
- [x] Multi-path `scan` invocation (what the pre-commit hook needs)
|
|
230
266
|
- [ ] Cross-module resolution for CH007/CH009 (currently same-file only)
|
|
231
267
|
- [ ] Sync HTTP clients constructed inside async request handlers
|
|
232
268
|
- [ ] `--fix` for the mechanical rules (CH002, CH003, CH004)
|
|
233
|
-
- [ ] Pre-commit hook
|
|
234
269
|
|
|
235
270
|
---
|
|
236
271
|
|
|
@@ -71,6 +71,9 @@ PYTHONPATH=src python -m codehound.cli scan path/to/project
|
|
|
71
71
|
# scan a project (skips tests/, docs/, examples/, vendored code by default)
|
|
72
72
|
codehound scan path/to/project
|
|
73
73
|
|
|
74
|
+
# scan multiple files/directories in one invocation (what pre-commit does)
|
|
75
|
+
codehound scan file1.py file2.py src/
|
|
76
|
+
|
|
74
77
|
# only run specific checks
|
|
75
78
|
codehound scan path/to/project --select CH001,CH006
|
|
76
79
|
|
|
@@ -78,6 +81,9 @@ codehound scan path/to/project --select CH001,CH006
|
|
|
78
81
|
codehound scan path/to/project --format json
|
|
79
82
|
codehound scan path/to/project --format csv
|
|
80
83
|
|
|
84
|
+
# GitHub Code Scanning (Security tab) can ingest this directly
|
|
85
|
+
codehound scan path/to/project --format sarif > results.sarif
|
|
86
|
+
|
|
81
87
|
# list every available check
|
|
82
88
|
codehound list
|
|
83
89
|
```
|
|
@@ -88,6 +94,29 @@ codehound list
|
|
|
88
94
|
- run: codehound scan src # fails the build on a regression
|
|
89
95
|
```
|
|
90
96
|
|
|
97
|
+
### GitHub Action
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
- uses: kratos0718/codehound@v1.2.0
|
|
101
|
+
with:
|
|
102
|
+
path: src
|
|
103
|
+
# select: CH001,CH006 # optional, defaults to all checks
|
|
104
|
+
# fail-on-findings: "false" # optional, report without failing the build
|
|
105
|
+
# upload-sarif: "false" # optional, skip the Code Scanning upload
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Uploads findings to the repo's **Security → Code Scanning** tab via SARIF, in addition to failing the step (unless `fail-on-findings: "false"`).
|
|
109
|
+
|
|
110
|
+
### pre-commit
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
repos:
|
|
114
|
+
- repo: https://github.com/kratos0718/codehound
|
|
115
|
+
rev: v1.2.0
|
|
116
|
+
hooks:
|
|
117
|
+
- id: codehound
|
|
118
|
+
```
|
|
119
|
+
|
|
91
120
|
---
|
|
92
121
|
|
|
93
122
|
## The checks
|
|
@@ -169,7 +198,9 @@ codehound/
|
|
|
169
198
|
├── core.py # file discovery, AST parsing, the Finding/Check contract,
|
|
170
199
|
│ # and a child→parent map so checks can ask "what's my
|
|
171
200
|
│ # enclosing function / am I inside a `with`?"
|
|
172
|
-
├── cli.py # `scan` / `list`, text|json|csv output, CI-friendly exit codes
|
|
201
|
+
├── cli.py # `scan` / `list`, text|json|csv|sarif output, CI-friendly exit codes
|
|
202
|
+
├── sarif.py # SARIF 2.1.0 output for GitHub Code Scanning
|
|
203
|
+
├── terminal.py # colored text output (auto-disabled for non-TTY / NO_COLOR)
|
|
173
204
|
└── checks/ # one small, independently-tested class per rule
|
|
174
205
|
├── blocking_async.py (CH001)
|
|
175
206
|
├── mutable_defaults.py (CH002)
|
|
@@ -207,10 +238,14 @@ Every check has paired tests: the buggy pattern *is* flagged, and the idiomatic
|
|
|
207
238
|
- [x] `asyncio.run()` inside a running loop — CH008
|
|
208
239
|
- [x] Non-daemon thread started without a join — CH009 (the thread analog of CH006)
|
|
209
240
|
- [x] Loop-variable closure capture in lambdas — CH010
|
|
241
|
+
- [x] Pre-commit hook — `.pre-commit-hooks.yaml`
|
|
242
|
+
- [x] GitHub Action — `action.yml`, uploads SARIF to Code Scanning
|
|
243
|
+
- [x] SARIF output — `--format sarif`
|
|
244
|
+
- [x] Colored terminal output (auto-disabled for non-TTY / `NO_COLOR`)
|
|
245
|
+
- [x] Multi-path `scan` invocation (what the pre-commit hook needs)
|
|
210
246
|
- [ ] Cross-module resolution for CH007/CH009 (currently same-file only)
|
|
211
247
|
- [ ] Sync HTTP clients constructed inside async request handlers
|
|
212
248
|
- [ ] `--fix` for the mechanical rules (CH002, CH003, CH004)
|
|
213
|
-
- [ ] Pre-commit hook
|
|
214
249
|
|
|
215
250
|
---
|
|
216
251
|
|
|
@@ -9,6 +9,8 @@ import sys
|
|
|
9
9
|
from codehound import __version__
|
|
10
10
|
from codehound.checks import ALL_CHECKS, get_checks
|
|
11
11
|
from codehound.core import DEFAULT_SKIP_DIRS, scan_path
|
|
12
|
+
from codehound.sarif import to_sarif
|
|
13
|
+
from codehound.terminal import format_findings_text, format_summary
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def _cmd_scan(args: argparse.Namespace) -> int:
|
|
@@ -21,7 +23,10 @@ def _cmd_scan(args: argparse.Namespace) -> int:
|
|
|
21
23
|
skip = set(DEFAULT_SKIP_DIRS)
|
|
22
24
|
if args.include_tests:
|
|
23
25
|
skip -= {"tests", "test", "testing"}
|
|
24
|
-
findings =
|
|
26
|
+
findings = []
|
|
27
|
+
for path in args.paths:
|
|
28
|
+
findings.extend(scan_path(path, checks, skip_dirs=frozenset(skip)))
|
|
29
|
+
findings.sort(key=lambda f: (f.path, f.line, f.col, f.code))
|
|
25
30
|
|
|
26
31
|
if args.format == "json":
|
|
27
32
|
print(json.dumps([f.as_dict() for f in findings], indent=2))
|
|
@@ -30,18 +35,12 @@ def _cmd_scan(args: argparse.Namespace) -> int:
|
|
|
30
35
|
for f in findings:
|
|
31
36
|
msg = f.message.replace('"', "'")
|
|
32
37
|
print(f'{f.path},{f.line},{f.col},{f.code},"{msg}"')
|
|
38
|
+
elif args.format == "sarif":
|
|
39
|
+
print(json.dumps(to_sarif(findings, ALL_CHECKS), indent=2))
|
|
33
40
|
else: # text
|
|
34
|
-
for
|
|
35
|
-
print(
|
|
36
|
-
|
|
37
|
-
for f in findings:
|
|
38
|
-
counts[f.code] = counts.get(f.code, 0) + 1
|
|
39
|
-
summary = ", ".join(f"{k}: {v}" for k, v in sorted(counts.items()))
|
|
40
|
-
print(
|
|
41
|
-
f"\nFound {len(findings)} issue(s)"
|
|
42
|
-
+ (f" ({summary})" if summary else ""),
|
|
43
|
-
file=sys.stderr,
|
|
44
|
-
)
|
|
41
|
+
for line in format_findings_text(findings):
|
|
42
|
+
print(line)
|
|
43
|
+
print(f"\n{format_summary(findings)}", file=sys.stderr)
|
|
45
44
|
|
|
46
45
|
if findings and not args.exit_zero:
|
|
47
46
|
return 1
|
|
@@ -62,17 +61,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
62
61
|
parser.add_argument("--version", action="version", version=f"codehound {__version__}")
|
|
63
62
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
64
63
|
|
|
65
|
-
scan = sub.add_parser("scan", help="scan
|
|
66
|
-
scan.add_argument(
|
|
64
|
+
scan = sub.add_parser("scan", help="scan one or more files/directories for issues")
|
|
65
|
+
scan.add_argument(
|
|
66
|
+
"paths",
|
|
67
|
+
nargs="+",
|
|
68
|
+
metavar="path",
|
|
69
|
+
help="file(s) or director(y/ies) to scan (accepts multiple, for pre-commit)",
|
|
70
|
+
)
|
|
67
71
|
scan.add_argument(
|
|
68
72
|
"--select",
|
|
69
73
|
help="comma-separated check codes/names to run (default: all), e.g. CH001,CH006",
|
|
70
74
|
)
|
|
71
75
|
scan.add_argument(
|
|
72
76
|
"--format",
|
|
73
|
-
choices=["text", "json", "csv"],
|
|
77
|
+
choices=["text", "json", "csv", "sarif"],
|
|
74
78
|
default="text",
|
|
75
|
-
help="output format (default: text)",
|
|
79
|
+
help="output format (default: text; sarif for GitHub Code Scanning)",
|
|
76
80
|
)
|
|
77
81
|
scan.add_argument(
|
|
78
82
|
"--include-tests",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""SARIF 2.1.0 output, so a `codehound` run can feed GitHub's Code Scanning
|
|
2
|
+
tab directly (``github/codeql-action/upload-sarif``) instead of only being
|
|
3
|
+
readable as CI log text.
|
|
4
|
+
|
|
5
|
+
Deliberately minimal - just the fields GitHub's ingester actually needs:
|
|
6
|
+
one ``tool.driver`` with a ``rules`` array (so finding codes get a name and
|
|
7
|
+
description in the UI instead of a bare code), and one ``result`` per
|
|
8
|
+
finding with a single physical location. No fingerprinting, no nested
|
|
9
|
+
regions, no multi-run merging - those are real SARIF features this
|
|
10
|
+
doesn't need yet.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from codehound import __version__
|
|
16
|
+
from codehound.core import Check, Finding
|
|
17
|
+
|
|
18
|
+
SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _rule(check_cls: type[Check]) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"id": check_cls.code,
|
|
24
|
+
"name": check_cls.name,
|
|
25
|
+
"shortDescription": {"text": check_cls.description},
|
|
26
|
+
"helpUri": "https://github.com/kratos0718/codehound#the-checks",
|
|
27
|
+
"properties": {"tags": ["correctness", "codehound"]},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _result(finding: Finding) -> dict:
|
|
32
|
+
return {
|
|
33
|
+
"ruleId": finding.code,
|
|
34
|
+
"level": "error",
|
|
35
|
+
"message": {"text": finding.message},
|
|
36
|
+
"locations": [
|
|
37
|
+
{
|
|
38
|
+
"physicalLocation": {
|
|
39
|
+
"artifactLocation": {"uri": finding.path.replace("\\", "/")},
|
|
40
|
+
"region": {
|
|
41
|
+
"startLine": max(finding.line, 1),
|
|
42
|
+
"startColumn": max(finding.col + 1, 1),
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_sarif(findings: list[Finding], all_checks: list[type[Check]]) -> dict:
|
|
51
|
+
return {
|
|
52
|
+
"$schema": SARIF_SCHEMA,
|
|
53
|
+
"version": "2.1.0",
|
|
54
|
+
"runs": [
|
|
55
|
+
{
|
|
56
|
+
"tool": {
|
|
57
|
+
"driver": {
|
|
58
|
+
"name": "codehound",
|
|
59
|
+
"informationUri": "https://github.com/kratos0718/codehound",
|
|
60
|
+
"version": __version__,
|
|
61
|
+
"rules": [_rule(c) for c in all_checks],
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"results": [_result(f) for f in findings],
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Colored text output for a terminal, plain text everywhere else.
|
|
2
|
+
|
|
3
|
+
Color only when stdout is actually a terminal, and never when ``NO_COLOR``
|
|
4
|
+
is set (https://no-color.org) or ``TERM=dumb`` - the same convention ruff,
|
|
5
|
+
eslint, and most modern CLI tools follow, so piping to a file or into `less`
|
|
6
|
+
never ends up with raw escape codes in it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
_RED = "\033[31m"
|
|
15
|
+
_YELLOW = "\033[33m"
|
|
16
|
+
_CYAN = "\033[36m"
|
|
17
|
+
_DIM = "\033[2m"
|
|
18
|
+
_BOLD = "\033[1m"
|
|
19
|
+
_RESET = "\033[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _color_enabled(stream=None) -> bool:
|
|
23
|
+
stream = stream or sys.stdout
|
|
24
|
+
if os.environ.get("NO_COLOR") is not None:
|
|
25
|
+
return False
|
|
26
|
+
if os.environ.get("TERM") == "dumb":
|
|
27
|
+
return False
|
|
28
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_finding_text(finding, color: bool) -> str:
|
|
32
|
+
if not color:
|
|
33
|
+
return finding.as_text()
|
|
34
|
+
return (
|
|
35
|
+
f"{_CYAN}{finding.path}{_RESET}:{_DIM}{finding.line}:{finding.col}{_RESET}: "
|
|
36
|
+
f"{_RED}{_BOLD}{finding.code}{_RESET} {finding.message}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_findings_text(findings, stream=None) -> list[str]:
|
|
41
|
+
color = _color_enabled(stream)
|
|
42
|
+
return [format_finding_text(f, color) for f in findings]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_summary(findings, stream=None) -> str:
|
|
46
|
+
color = _color_enabled(stream)
|
|
47
|
+
counts: dict[str, int] = {}
|
|
48
|
+
for f in findings:
|
|
49
|
+
counts[f.code] = counts.get(f.code, 0) + 1
|
|
50
|
+
summary = ", ".join(f"{k}: {v}" for k, v in sorted(counts.items()))
|
|
51
|
+
count_str = str(len(findings))
|
|
52
|
+
if color and findings:
|
|
53
|
+
count_str = f"{_YELLOW}{_BOLD}{count_str}{_RESET}"
|
|
54
|
+
line = f"Found {count_str} issue(s)"
|
|
55
|
+
if summary:
|
|
56
|
+
line += f" ({summary})"
|
|
57
|
+
return line
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Tests for the non-default output formats: SARIF and colored text.
|
|
2
|
+
|
|
3
|
+
Scan results themselves are covered by test_checks.py; these tests only
|
|
4
|
+
check that each format serializes a Finding correctly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from codehound.checks import ALL_CHECKS
|
|
10
|
+
from codehound.core import Finding
|
|
11
|
+
from codehound.sarif import to_sarif
|
|
12
|
+
from codehound.terminal import format_finding_text, format_summary
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sample_finding() -> Finding:
|
|
16
|
+
return Finding(path="pkg/mod.py", line=10, col=4, code="CH002", message="test message")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_sarif_has_required_top_level_shape():
|
|
20
|
+
doc = to_sarif([_sample_finding()], ALL_CHECKS)
|
|
21
|
+
assert doc["version"] == "2.1.0"
|
|
22
|
+
assert "$schema" in doc
|
|
23
|
+
assert len(doc["runs"]) == 1
|
|
24
|
+
run = doc["runs"][0]
|
|
25
|
+
assert run["tool"]["driver"]["name"] == "codehound"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_sarif_includes_a_rule_entry_per_check():
|
|
29
|
+
doc = to_sarif([], ALL_CHECKS)
|
|
30
|
+
rule_ids = {r["id"] for r in doc["runs"][0]["tool"]["driver"]["rules"]}
|
|
31
|
+
assert rule_ids == {c.code for c in ALL_CHECKS}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_sarif_result_location_and_1_indexed_column():
|
|
35
|
+
doc = to_sarif([_sample_finding()], ALL_CHECKS)
|
|
36
|
+
result = doc["runs"][0]["results"][0]
|
|
37
|
+
assert result["ruleId"] == "CH002"
|
|
38
|
+
loc = result["locations"][0]["physicalLocation"]
|
|
39
|
+
assert loc["artifactLocation"]["uri"] == "pkg/mod.py"
|
|
40
|
+
# Finding.col is a 0-indexed ast col_offset; SARIF columns are 1-indexed.
|
|
41
|
+
assert loc["region"]["startLine"] == 10
|
|
42
|
+
assert loc["region"]["startColumn"] == 5
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_sarif_with_no_findings_has_empty_results():
|
|
46
|
+
doc = to_sarif([], ALL_CHECKS)
|
|
47
|
+
assert doc["runs"][0]["results"] == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_colored_text_contains_plain_text_content():
|
|
51
|
+
finding = _sample_finding()
|
|
52
|
+
colored = format_finding_text(finding, color=True)
|
|
53
|
+
plain = format_finding_text(finding, color=False)
|
|
54
|
+
assert plain == finding.as_text()
|
|
55
|
+
assert "\033[" in colored
|
|
56
|
+
assert "CH002" in colored
|
|
57
|
+
assert "test message" in colored
|
|
58
|
+
assert "pkg/mod.py" in colored
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_summary_reports_correct_counts():
|
|
62
|
+
findings = [
|
|
63
|
+
Finding(path="a.py", line=1, col=0, code="CH001", message="x"),
|
|
64
|
+
Finding(path="b.py", line=2, col=0, code="CH001", message="y"),
|
|
65
|
+
Finding(path="c.py", line=3, col=0, code="CH002", message="z"),
|
|
66
|
+
]
|
|
67
|
+
summary = format_summary(findings)
|
|
68
|
+
assert "Found 3 issue(s)" in summary
|
|
69
|
+
assert "CH001: 2" in summary
|
|
70
|
+
assert "CH002: 1" in summary
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_summary_with_no_findings():
|
|
74
|
+
assert format_summary([]) == "Found 0 issue(s)"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|