errpath 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.
- errpath/__init__.py +19 -0
- errpath/__main__.py +4 -0
- errpath/_pydantic.py +102 -0
- errpath/analyse.py +1035 -0
- errpath/cli.py +141 -0
- errpath/diagram.py +135 -0
- errpath/ir.py +274 -0
- errpath/machine.py +165 -0
- errpath/py.typed +0 -0
- errpath/result.py +254 -0
- errpath/review.py +517 -0
- errpath/safe.py +129 -0
- errpath-0.1.0.dist-info/METADATA +622 -0
- errpath-0.1.0.dist-info/RECORD +17 -0
- errpath-0.1.0.dist-info/WHEEL +4 -0
- errpath-0.1.0.dist-info/entry_points.txt +2 -0
- errpath-0.1.0.dist-info/licenses/LICENSE +21 -0
errpath/cli.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""errpath CLI: ir | machine | diagram | review."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from errpath.analyse import analyse_path
|
|
11
|
+
from errpath.diagram import render_function
|
|
12
|
+
from errpath.ir import Function, to_dict
|
|
13
|
+
from errpath.machine import function_to_machine
|
|
14
|
+
from errpath.review import review_to_dict
|
|
15
|
+
from errpath.review import run as run_review
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _find_function(report_functions: list[Function], name: str) -> Function | None:
|
|
19
|
+
matches = [
|
|
20
|
+
f for f in report_functions if f.name == name or f.id == name or f.id.endswith("." + name)
|
|
21
|
+
]
|
|
22
|
+
if not matches:
|
|
23
|
+
return None
|
|
24
|
+
if len(matches) > 1:
|
|
25
|
+
# Prefer exact name match in the given file scope
|
|
26
|
+
exact = [f for f in matches if f.name == name]
|
|
27
|
+
return exact[0] if exact else matches[0]
|
|
28
|
+
return matches[0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _emit(text: str, output: str | None) -> int:
|
|
32
|
+
if output:
|
|
33
|
+
Path(output).write_text(text + "\n", encoding="utf-8")
|
|
34
|
+
else:
|
|
35
|
+
print(text)
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_function(args: argparse.Namespace) -> Function | None:
|
|
40
|
+
report = analyse_path(Path(args.path))
|
|
41
|
+
fn = _find_function(report.functions, args.function)
|
|
42
|
+
if fn is None:
|
|
43
|
+
names = ", ".join(f.name for f in report.functions) or "(none)"
|
|
44
|
+
print(f"function {args.function!r} not found; have: {names}", file=sys.stderr)
|
|
45
|
+
return None
|
|
46
|
+
for u in report.unanalysed: # say what the output can't show, on stderr
|
|
47
|
+
if u.function == fn.id:
|
|
48
|
+
print(f"errpath: note: {u.file}:{u.line}: {u.reason}: {u.code}", file=sys.stderr)
|
|
49
|
+
return fn
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_ir(args: argparse.Namespace) -> int:
|
|
53
|
+
report = analyse_path(Path(args.path))
|
|
54
|
+
return _emit(json.dumps(to_dict(report), indent=2), args.output)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_machine(args: argparse.Namespace) -> int:
|
|
58
|
+
fn = _load_function(args)
|
|
59
|
+
if fn is None:
|
|
60
|
+
return 1
|
|
61
|
+
return _emit(json.dumps(function_to_machine(fn), indent=2), args.output)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_diagram(args: argparse.Namespace) -> int:
|
|
65
|
+
fn = _load_function(args)
|
|
66
|
+
if fn is None:
|
|
67
|
+
return 1
|
|
68
|
+
return _emit(render_function(fn), args.output)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_review(args: argparse.Namespace) -> int:
|
|
72
|
+
review = run_review(
|
|
73
|
+
repo=Path(args.repo or ".").resolve(),
|
|
74
|
+
base=args.base,
|
|
75
|
+
head=args.head,
|
|
76
|
+
paths=args.paths or [],
|
|
77
|
+
diagrams=args.diagrams,
|
|
78
|
+
)
|
|
79
|
+
if args.format == "json":
|
|
80
|
+
text = json.dumps(review_to_dict(review), indent=2)
|
|
81
|
+
else:
|
|
82
|
+
text = review.markdown or review.summary
|
|
83
|
+
return _emit(text, args.output)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
parser = argparse.ArgumentParser(
|
|
88
|
+
prog="errpath",
|
|
89
|
+
description="Result type helpers and error-path analysis for Python",
|
|
90
|
+
)
|
|
91
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
92
|
+
|
|
93
|
+
ir_p = sub.add_parser("ir", help="Emit versioned IR JSON for a path")
|
|
94
|
+
ir_p.add_argument("path", nargs="?", default=".", help="File or directory")
|
|
95
|
+
ir_p.add_argument("-o", "--output", help="Write to file instead of stdout")
|
|
96
|
+
ir_p.set_defaults(func=cmd_ir)
|
|
97
|
+
|
|
98
|
+
machine_p = sub.add_parser("machine", help="Emit XState MachineJSON for a function")
|
|
99
|
+
machine_p.add_argument("path", help="File or directory")
|
|
100
|
+
machine_p.add_argument("function", help="Function name")
|
|
101
|
+
machine_p.add_argument("-o", "--output", help="Write to file instead of stdout")
|
|
102
|
+
machine_p.set_defaults(func=cmd_machine)
|
|
103
|
+
|
|
104
|
+
diagram_p = sub.add_parser("diagram", help="Emit Mermaid flowchart for a function")
|
|
105
|
+
diagram_p.add_argument("path", help="File or directory")
|
|
106
|
+
diagram_p.add_argument("function", help="Function name")
|
|
107
|
+
diagram_p.add_argument("-o", "--output", help="Write to file instead of stdout")
|
|
108
|
+
diagram_p.set_defaults(func=cmd_diagram)
|
|
109
|
+
|
|
110
|
+
review_p = sub.add_parser("review", help="Compare error paths between git refs")
|
|
111
|
+
review_p.add_argument("paths", nargs="*", help="Optional paths to limit the review")
|
|
112
|
+
review_p.add_argument("--base", required=True, help="Base git ref")
|
|
113
|
+
review_p.add_argument("--head", default="HEAD", help="Head git ref (default HEAD)")
|
|
114
|
+
review_p.add_argument("--repo", default=".", help="Git repository root")
|
|
115
|
+
review_p.add_argument(
|
|
116
|
+
"--diagrams",
|
|
117
|
+
choices=("open", "collapsed"),
|
|
118
|
+
default="collapsed",
|
|
119
|
+
help="Mermaid diagrams in markdown",
|
|
120
|
+
)
|
|
121
|
+
review_p.add_argument(
|
|
122
|
+
"--format",
|
|
123
|
+
choices=("markdown", "json"),
|
|
124
|
+
default="markdown",
|
|
125
|
+
help="Output format",
|
|
126
|
+
)
|
|
127
|
+
review_p.add_argument("-o", "--output", help="Write to file instead of stdout")
|
|
128
|
+
review_p.set_defaults(func=cmd_review)
|
|
129
|
+
|
|
130
|
+
return parser
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(argv: list[str] | None = None) -> None:
|
|
134
|
+
parser = build_parser()
|
|
135
|
+
args = parser.parse_args(argv)
|
|
136
|
+
code = args.func(args)
|
|
137
|
+
raise SystemExit(code)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
errpath/diagram.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Render Mermaid flowchart LR from a function's IR nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from errpath.ir import (
|
|
6
|
+
CATCH_ANY,
|
|
7
|
+
Branch,
|
|
8
|
+
Fail,
|
|
9
|
+
Function,
|
|
10
|
+
Jump,
|
|
11
|
+
Loop,
|
|
12
|
+
Node,
|
|
13
|
+
Panic,
|
|
14
|
+
Recover,
|
|
15
|
+
Step,
|
|
16
|
+
Succeed,
|
|
17
|
+
Swallow,
|
|
18
|
+
escaping,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _text(label: str) -> str:
|
|
23
|
+
return label.replace('"', "'")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render(fn_name: str, nodes: list[Node]) -> str:
|
|
27
|
+
"""Mermaid flowchart of ``nodes``.
|
|
28
|
+
|
|
29
|
+
Built back to front, like the state machine: what follows a fork is drawn once and
|
|
30
|
+
every arm points at it, and each error type gets one shared box.
|
|
31
|
+
"""
|
|
32
|
+
lines = ["flowchart LR", f' Start(["{_text(fn_name)}"])', " Done((Success))"]
|
|
33
|
+
counter = {"n": 0}
|
|
34
|
+
handlers: dict[str, str] = {} # "<try tag>#<arm index>" -> handler entry node
|
|
35
|
+
errors: dict[str, str] = {} # error label -> its box
|
|
36
|
+
|
|
37
|
+
def add(prefix: str, shape: str) -> str:
|
|
38
|
+
counter["n"] += 1
|
|
39
|
+
node_id = f"{prefix}{counter['n']}"
|
|
40
|
+
lines.append(f" {node_id}{shape}")
|
|
41
|
+
return node_id
|
|
42
|
+
|
|
43
|
+
def link(src: str, label: str, dst: str) -> None:
|
|
44
|
+
if not label:
|
|
45
|
+
lines.append(f" {src} --> {dst}")
|
|
46
|
+
elif label.replace("_", "").replace(" ", "").isalnum():
|
|
47
|
+
lines.append(f" {src} -->|{label}| {dst}")
|
|
48
|
+
else:
|
|
49
|
+
lines.append(f' {src} -->|"{_text(label)}"| {dst}')
|
|
50
|
+
|
|
51
|
+
def error_box(label: str) -> str:
|
|
52
|
+
if label not in errors:
|
|
53
|
+
errors[label] = add("E", f'[/"{_text(label)}"/]')
|
|
54
|
+
return errors[label]
|
|
55
|
+
|
|
56
|
+
def error_edges(src: str, edge: str, node: Step | Panic, escape_label: str | None) -> None:
|
|
57
|
+
"""One edge per caught type to its handler, one for what escapes; through finally first."""
|
|
58
|
+
for key, catch in node.caught_by.items():
|
|
59
|
+
label = "any" if key == CATCH_ANY else key
|
|
60
|
+
link(src, f"{edge} {label}", emit(list(catch.cleanup), handlers[catch.handler]))
|
|
61
|
+
left = escaping(node.error, node.caught_by)
|
|
62
|
+
if left is not None:
|
|
63
|
+
link(src, edge, emit([*node.cleanup, Fail(error=escape_label or left)], "Done"))
|
|
64
|
+
|
|
65
|
+
def emit(nodes: list[Node], join: str) -> str:
|
|
66
|
+
"""Draw ``nodes``, falling through to ``join``; return the first node's id."""
|
|
67
|
+
if not nodes:
|
|
68
|
+
return join
|
|
69
|
+
node, after = nodes[0], nodes[1:]
|
|
70
|
+
if isinstance(node, Succeed):
|
|
71
|
+
return "Done"
|
|
72
|
+
if isinstance(node, Fail):
|
|
73
|
+
return error_box(node.error or "Error")
|
|
74
|
+
rest = emit(after, join)
|
|
75
|
+
|
|
76
|
+
if isinstance(node, Step):
|
|
77
|
+
sid = add("S", f'["{_text(node.name)}"]')
|
|
78
|
+
link(sid, "ok", rest)
|
|
79
|
+
error_edges(sid, "err", node, None)
|
|
80
|
+
return sid
|
|
81
|
+
|
|
82
|
+
if isinstance(node, Panic):
|
|
83
|
+
pid = add("P", f'["{_text(node.name)}"]')
|
|
84
|
+
if not node.terminal:
|
|
85
|
+
link(pid, "ok", rest)
|
|
86
|
+
label = node.error if node.terminal and node.error else "Panic" # raise X: X
|
|
87
|
+
error_edges(pid, "raise" if node.terminal else "panic", node, label)
|
|
88
|
+
return pid
|
|
89
|
+
|
|
90
|
+
if isinstance(node, Swallow):
|
|
91
|
+
wid = add("W", f'["swallow {_text(node.name)}"]')
|
|
92
|
+
link(wid, "", rest)
|
|
93
|
+
return wid
|
|
94
|
+
|
|
95
|
+
if isinstance(node, Recover):
|
|
96
|
+
rid = add("R", f'["recover {_text(node.how)}"]')
|
|
97
|
+
link(rid, "", rest)
|
|
98
|
+
return rid
|
|
99
|
+
|
|
100
|
+
if isinstance(node, Jump):
|
|
101
|
+
jid = add("J", f'["{_text(node.name)}"]')
|
|
102
|
+
link(jid, "", rest)
|
|
103
|
+
return jid
|
|
104
|
+
|
|
105
|
+
if isinstance(node, Branch):
|
|
106
|
+
did = add("D", f'{{"{_text(node.label)}"}}')
|
|
107
|
+
targets: dict[int, str] = {}
|
|
108
|
+
# try: draw handlers first so steps in the body can point their errors at them
|
|
109
|
+
order = [*range(1, len(node.arms)), 0] if node.tag else range(len(node.arms))
|
|
110
|
+
for i in order:
|
|
111
|
+
targets[i] = emit(list(node.arms[i].body), rest)
|
|
112
|
+
if node.tag and i > 0:
|
|
113
|
+
handlers[f"{node.tag}#{i}"] = targets[i]
|
|
114
|
+
for i, arm in enumerate(node.arms):
|
|
115
|
+
link(did, arm.label, targets[i])
|
|
116
|
+
return did
|
|
117
|
+
|
|
118
|
+
if isinstance(node, Loop):
|
|
119
|
+
lid = add("L", f'["{_text(node.label)}"]')
|
|
120
|
+
if node.body:
|
|
121
|
+
link(lid, "next", emit(list(node.body), lid)) # the body loops back
|
|
122
|
+
link(lid, "done", rest)
|
|
123
|
+
return lid
|
|
124
|
+
|
|
125
|
+
return rest
|
|
126
|
+
|
|
127
|
+
link("Start", "", emit(nodes, "Done"))
|
|
128
|
+
if errors:
|
|
129
|
+
lines.append(" classDef error fill:#fdecea,stroke:#d93025,color:#a50e0e")
|
|
130
|
+
lines.append(f" class {','.join(errors.values())} error")
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_function(fn: Function) -> str:
|
|
135
|
+
return render(fn.name, fn.nodes)
|
errpath/ir.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Versioned error-path IR (aligned with Rust errpath schema_version 1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from enum import Enum, StrEnum
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
SCHEMA_VERSION = 1
|
|
10
|
+
NO_PANIC = "no-panic"
|
|
11
|
+
NO_SWALLOW = "no-swallow"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Confidence(StrEnum):
|
|
15
|
+
CERTAIN = "certain"
|
|
16
|
+
LIKELY = "likely"
|
|
17
|
+
POSSIBLE = "possible"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorSource(StrEnum):
|
|
21
|
+
CALLEE = "callee"
|
|
22
|
+
ENCLOSING = "enclosing"
|
|
23
|
+
UNKNOWN = "unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Arm:
|
|
28
|
+
label: str
|
|
29
|
+
body: list[Node]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Catch:
|
|
34
|
+
"""Where one exception type raised by a step goes: an ``except`` arm of an enclosing try."""
|
|
35
|
+
|
|
36
|
+
handler: str # "<try tag>#<arm index>"
|
|
37
|
+
how: str # the handler's label, e.g. "except ValueError"
|
|
38
|
+
cleanup: list[Node] = field(default_factory=list) # inner finally nodes run before the handler
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
CATCH_ANY = "*" # caught_by key: an error of unknown type, caught by a catch-all handler
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def error_members(error: str | None) -> list[str]:
|
|
45
|
+
"""``"ValueError | KeyError"`` -> ``["ValueError", "KeyError"]``; unknown -> ``[]``."""
|
|
46
|
+
return [m.strip() for m in error.split("|")] if error else []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def escaping(error: str | None, caught_by: dict[str, Catch]) -> str | None:
|
|
50
|
+
"""The part of ``error`` that no handler catches, or None when every part is caught."""
|
|
51
|
+
members = error_members(error)
|
|
52
|
+
if not members:
|
|
53
|
+
return None if CATCH_ANY in caught_by else (error or "Error")
|
|
54
|
+
left = [m for m in members if m not in caught_by]
|
|
55
|
+
return " | ".join(left) if left else None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Step:
|
|
60
|
+
kind: Literal["step"] = "step"
|
|
61
|
+
name: str = ""
|
|
62
|
+
error: str | None = None
|
|
63
|
+
source: ErrorSource = ErrorSource.UNKNOWN
|
|
64
|
+
conversions: list[str] = field(default_factory=list)
|
|
65
|
+
line: int = 0
|
|
66
|
+
cleanup: list[Node] = field(default_factory=list) # finally nodes run on the error exit
|
|
67
|
+
raises: bool = False # error exits by exception (unwrap), so except can catch it
|
|
68
|
+
# exception type -> handler; for an unknown error, keyed by each handler type it may match
|
|
69
|
+
caught_by: dict[str, Catch] = field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Branch:
|
|
74
|
+
kind: Literal["branch"] = "branch"
|
|
75
|
+
label: str = ""
|
|
76
|
+
arms: list[Arm] = field(default_factory=list)
|
|
77
|
+
tag: str | None = None # set on try branches; arm 0 is the normal path, the rest are handlers
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Loop:
|
|
82
|
+
kind: Literal["loop"] = "loop"
|
|
83
|
+
label: str = ""
|
|
84
|
+
tag: str | None = None
|
|
85
|
+
can_skip: bool = True
|
|
86
|
+
head: list[Node] = field(default_factory=list)
|
|
87
|
+
body: list[Node] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class Scope:
|
|
92
|
+
kind: Literal["scope"] = "scope"
|
|
93
|
+
label: str = ""
|
|
94
|
+
body: list[Node] = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class Fail:
|
|
99
|
+
kind: Literal["fail"] = "fail"
|
|
100
|
+
error: str = ""
|
|
101
|
+
line: int = 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Succeed:
|
|
106
|
+
kind: Literal["succeed"] = "succeed"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class Block:
|
|
111
|
+
kind: Literal["block"] = "block"
|
|
112
|
+
tag: str = ""
|
|
113
|
+
body: list[Node] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class Jump:
|
|
118
|
+
kind: Literal["jump"] = "jump"
|
|
119
|
+
name: str = ""
|
|
120
|
+
line: int = 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Convert:
|
|
125
|
+
kind: Literal["convert"] = "convert"
|
|
126
|
+
name: str = ""
|
|
127
|
+
conversions: list[str] = field(default_factory=list)
|
|
128
|
+
line: int = 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class Panic:
|
|
133
|
+
kind: Literal["panic"] = "panic"
|
|
134
|
+
name: str = ""
|
|
135
|
+
terminal: bool = False
|
|
136
|
+
line: int = 0
|
|
137
|
+
allowed: str | None = None
|
|
138
|
+
cleanup: list[Node] = field(default_factory=list) # finally nodes run on the error exit
|
|
139
|
+
error: str | None = None # exception type raised, when known
|
|
140
|
+
caught_by: dict[str, Catch] = field(default_factory=dict) # as on Step
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class Swallow:
|
|
145
|
+
kind: Literal["swallow"] = "swallow"
|
|
146
|
+
name: str = ""
|
|
147
|
+
how: str = ""
|
|
148
|
+
confidence: Confidence = Confidence.LIKELY
|
|
149
|
+
line: int = 0
|
|
150
|
+
allowed: str | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class Recover:
|
|
155
|
+
kind: Literal["recover"] = "recover"
|
|
156
|
+
name: str = ""
|
|
157
|
+
how: str = ""
|
|
158
|
+
arms: list[str] = field(default_factory=list)
|
|
159
|
+
line: int = 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
Node = (
|
|
163
|
+
Step
|
|
164
|
+
| Branch
|
|
165
|
+
| Loop
|
|
166
|
+
| Scope
|
|
167
|
+
| Fail
|
|
168
|
+
| Succeed
|
|
169
|
+
| Block
|
|
170
|
+
| Jump
|
|
171
|
+
| Convert
|
|
172
|
+
| Panic
|
|
173
|
+
| Swallow
|
|
174
|
+
| Recover
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class ReturnsResult:
|
|
180
|
+
kind: Literal["result"] = "result"
|
|
181
|
+
error: str = ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class ReturnsOption:
|
|
186
|
+
kind: Literal["option"] = "option"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass
|
|
190
|
+
class ReturnsOther:
|
|
191
|
+
kind: Literal["other"] = "other"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
Returns = ReturnsResult | ReturnsOption | ReturnsOther
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class Function:
|
|
199
|
+
id: str
|
|
200
|
+
name: str
|
|
201
|
+
file: str
|
|
202
|
+
line: int
|
|
203
|
+
returns: Returns
|
|
204
|
+
nodes: list[Node] = field(default_factory=list)
|
|
205
|
+
macro_try: int = 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class Finding:
|
|
210
|
+
rule: str
|
|
211
|
+
file: str
|
|
212
|
+
line: int
|
|
213
|
+
message: str
|
|
214
|
+
confidence: Confidence = Confidence.CERTAIN
|
|
215
|
+
function: str | None = None
|
|
216
|
+
allowed: str | None = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass
|
|
220
|
+
class Coverage:
|
|
221
|
+
functions: int = 0
|
|
222
|
+
steps: int = 0
|
|
223
|
+
error_from_callee: int = 0
|
|
224
|
+
error_from_enclosing: int = 0
|
|
225
|
+
error_unknown: int = 0
|
|
226
|
+
macro_try: int = 0
|
|
227
|
+
item_macro_try: int = 0
|
|
228
|
+
unanalysed: int = 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class Unanalysed:
|
|
233
|
+
"""Code errpath saw but couldn't follow, so a diagram may miss a path there."""
|
|
234
|
+
|
|
235
|
+
file: str
|
|
236
|
+
line: int
|
|
237
|
+
function: str
|
|
238
|
+
code: str
|
|
239
|
+
reason: str
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@dataclass
|
|
243
|
+
class ParseFailure:
|
|
244
|
+
file: str
|
|
245
|
+
message: str
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass
|
|
249
|
+
class Report:
|
|
250
|
+
schema_version: int = SCHEMA_VERSION
|
|
251
|
+
coverage: Coverage = field(default_factory=Coverage)
|
|
252
|
+
functions: list[Function] = field(default_factory=list)
|
|
253
|
+
findings: list[Finding] = field(default_factory=list)
|
|
254
|
+
parse_failures: list[ParseFailure] = field(default_factory=list)
|
|
255
|
+
unanalysed: list[Unanalysed] = field(default_factory=list)
|
|
256
|
+
support_files_skipped: int = 0
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _enum_to_value(obj: Any) -> Any:
|
|
260
|
+
if isinstance(obj, Enum):
|
|
261
|
+
return obj.value
|
|
262
|
+
if isinstance(obj, list):
|
|
263
|
+
return [_enum_to_value(x) for x in obj]
|
|
264
|
+
if isinstance(obj, dict):
|
|
265
|
+
return {k: _enum_to_value(v) for k, v in obj.items()}
|
|
266
|
+
return obj
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def to_dict(obj: Any) -> Any:
|
|
270
|
+
"""Serialize dataclasses (and nested Enums) to JSON-ready dicts."""
|
|
271
|
+
if hasattr(obj, "__dataclass_fields__"):
|
|
272
|
+
raw = asdict(obj)
|
|
273
|
+
return _enum_to_value(raw)
|
|
274
|
+
return _enum_to_value(obj)
|