cpdocs 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.
- cpdocs/__init__.py +5 -0
- cpdocs/__main__.py +5 -0
- cpdocs/build.py +645 -0
- cpdocs/clang_backend.py +1254 -0
- cpdocs/cli.py +78 -0
- cpdocs/config.py +160 -0
- cpdocs/contract.py +407 -0
- cpdocs/html_renderer.py +1803 -0
- cpdocs/model.py +228 -0
- cpdocs/properdocs_core.py +158 -0
- cpdocs/properdocs_plugin.py +236 -0
- cpdocs/python_backend.py +469 -0
- cpdocs/python_manifest.py +84 -0
- cpdocs/render.py +838 -0
- cpdocs/rust_backend.py +234 -0
- cpdocs/share/cmake/cpdocs.cmake +464 -0
- cpdocs/versioning.py +201 -0
- cpdocs-0.1.0.dist-info/METADATA +48 -0
- cpdocs-0.1.0.dist-info/RECORD +21 -0
- cpdocs-0.1.0.dist-info/WHEEL +4 -0
- cpdocs-0.1.0.dist-info/entry_points.txt +5 -0
cpdocs/rust_backend.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 cpdocs developers
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Rustdoc-JSON backend for the shared cpdocs API graph."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .model import ApiEntity, ApiGraph, ApiParameter, ApiSignature, SourceLocation, stable_entity_id
|
|
11
|
+
|
|
12
|
+
_KIND_MAP = {
|
|
13
|
+
"module": "module",
|
|
14
|
+
"struct": "struct",
|
|
15
|
+
"enum": "enum",
|
|
16
|
+
"trait": "trait",
|
|
17
|
+
"function": "function",
|
|
18
|
+
"type_alias": "type_alias",
|
|
19
|
+
"constant": "constant",
|
|
20
|
+
"static": "variable",
|
|
21
|
+
"macro": "macro",
|
|
22
|
+
"proc_macro": "macro",
|
|
23
|
+
"union": "union",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _rust_type(value: object) -> str:
|
|
28
|
+
"""Render the stable, user-facing portion of a rustdoc JSON type node."""
|
|
29
|
+
|
|
30
|
+
if value is None:
|
|
31
|
+
return ""
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
return value
|
|
34
|
+
if isinstance(value, list):
|
|
35
|
+
return ", ".join(filter(None, (_rust_type(item) for item in value)))
|
|
36
|
+
if not isinstance(value, dict) or not value:
|
|
37
|
+
return ""
|
|
38
|
+
if "primitive" in value:
|
|
39
|
+
return str(value["primitive"])
|
|
40
|
+
if "generic" in value:
|
|
41
|
+
return str(value["generic"])
|
|
42
|
+
if "infer" in value:
|
|
43
|
+
return "_"
|
|
44
|
+
if "slice" in value:
|
|
45
|
+
return f"[{_rust_type(value['slice'])}]"
|
|
46
|
+
if "array" in value and isinstance(value["array"], dict):
|
|
47
|
+
array = value["array"]
|
|
48
|
+
length = array.get("len", "_")
|
|
49
|
+
return f"[{_rust_type(array.get('type'))}; {length}]"
|
|
50
|
+
if "tuple" in value:
|
|
51
|
+
parts = value["tuple"] if isinstance(value["tuple"], list) else []
|
|
52
|
+
rendered = ", ".join(_rust_type(item) for item in parts)
|
|
53
|
+
if len(parts) == 1:
|
|
54
|
+
rendered += ","
|
|
55
|
+
return f"({rendered})"
|
|
56
|
+
if "borrowed_ref" in value and isinstance(value["borrowed_ref"], dict):
|
|
57
|
+
ref = value["borrowed_ref"]
|
|
58
|
+
lifetime = ref.get("lifetime")
|
|
59
|
+
lifetime_text = f"'{lifetime} " if isinstance(lifetime, str) and lifetime else ""
|
|
60
|
+
mutable = "mut " if ref.get("is_mutable") else ""
|
|
61
|
+
return f"&{lifetime_text}{mutable}{_rust_type(ref.get('type'))}"
|
|
62
|
+
if "raw_pointer" in value and isinstance(value["raw_pointer"], dict):
|
|
63
|
+
pointer = value["raw_pointer"]
|
|
64
|
+
qualifier = "mut" if pointer.get("is_mutable") else "const"
|
|
65
|
+
return f"*{qualifier} {_rust_type(pointer.get('type'))}"
|
|
66
|
+
if "resolved_path" in value and isinstance(value["resolved_path"], dict):
|
|
67
|
+
path = value["resolved_path"]
|
|
68
|
+
name = str(path.get("name", ""))
|
|
69
|
+
args = path.get("args")
|
|
70
|
+
rendered_args: list[str] = []
|
|
71
|
+
if isinstance(args, dict):
|
|
72
|
+
angle = args.get("angle_bracketed")
|
|
73
|
+
if isinstance(angle, dict):
|
|
74
|
+
raw_args = angle.get("args")
|
|
75
|
+
if isinstance(raw_args, list):
|
|
76
|
+
for argument in raw_args:
|
|
77
|
+
if not isinstance(argument, dict):
|
|
78
|
+
continue
|
|
79
|
+
if "type" in argument:
|
|
80
|
+
rendered_args.append(_rust_type(argument["type"]))
|
|
81
|
+
elif "lifetime" in argument:
|
|
82
|
+
rendered_args.append(str(argument["lifetime"]))
|
|
83
|
+
elif "const" in argument:
|
|
84
|
+
rendered_args.append(str(argument["const"]))
|
|
85
|
+
if rendered_args:
|
|
86
|
+
name += "<" + ", ".join(rendered_args) + ">"
|
|
87
|
+
return name
|
|
88
|
+
if "impl_trait" in value:
|
|
89
|
+
bounds = value["impl_trait"] if isinstance(value["impl_trait"], list) else []
|
|
90
|
+
return "impl " + " + ".join(filter(None, (_rust_type(item) for item in bounds)))
|
|
91
|
+
if "dyn_trait" in value:
|
|
92
|
+
traits = value["dyn_trait"] if isinstance(value["dyn_trait"], list) else []
|
|
93
|
+
return "dyn " + " + ".join(filter(None, (_rust_type(item) for item in traits)))
|
|
94
|
+
# rustdoc evolves its JSON schema. Unknown type variants stay localized to this adapter rather
|
|
95
|
+
# than leaking backend-specific structure into ApiGraph or the renderer.
|
|
96
|
+
return next((_rust_type(item) for item in value.values() if _rust_type(item)), "")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _function_signature(item: dict[str, object]) -> tuple[ApiSignature | None, list[str]]:
|
|
100
|
+
inner = item.get("inner")
|
|
101
|
+
if not isinstance(inner, dict):
|
|
102
|
+
return None, []
|
|
103
|
+
function = inner.get("function")
|
|
104
|
+
if not isinstance(function, dict):
|
|
105
|
+
return None, []
|
|
106
|
+
signature = function.get("sig")
|
|
107
|
+
parameters: list[ApiParameter] = []
|
|
108
|
+
returns = ""
|
|
109
|
+
if isinstance(signature, dict):
|
|
110
|
+
inputs = signature.get("inputs")
|
|
111
|
+
if isinstance(inputs, list):
|
|
112
|
+
for raw in inputs:
|
|
113
|
+
if isinstance(raw, list) and len(raw) >= 2:
|
|
114
|
+
parameters.append(ApiParameter(name=str(raw[0]), type=_rust_type(raw[1])))
|
|
115
|
+
returns = _rust_type(signature.get("output"))
|
|
116
|
+
properties: list[str] = ["pub"]
|
|
117
|
+
header = function.get("header")
|
|
118
|
+
if isinstance(header, dict):
|
|
119
|
+
for field, label in (("is_const", "const"), ("is_async", "async"), ("is_unsafe", "unsafe")):
|
|
120
|
+
if header.get(field):
|
|
121
|
+
properties.append(label)
|
|
122
|
+
abi = header.get("abi")
|
|
123
|
+
if isinstance(abi, str) and abi not in {"", "Rust"}:
|
|
124
|
+
properties.append(f'extern "{abi}"')
|
|
125
|
+
return ApiSignature(
|
|
126
|
+
parameters=tuple(parameters), returns=returns, qualifiers=tuple(properties[1:])
|
|
127
|
+
), properties
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _inner_kind(item: dict[str, object]) -> str | None:
|
|
131
|
+
inner = item.get("inner")
|
|
132
|
+
if not isinstance(inner, dict) or not inner:
|
|
133
|
+
return None
|
|
134
|
+
return next(iter(inner))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def build_rust_graph(
|
|
138
|
+
*, rustdoc_json: Path, project: str, version: str = "main", project_root: Path | None = None
|
|
139
|
+
) -> ApiGraph:
|
|
140
|
+
try:
|
|
141
|
+
raw = json.loads(rustdoc_json.read_text(encoding="utf-8"))
|
|
142
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
143
|
+
raise RuntimeError(f"Cannot read rustdoc JSON {rustdoc_json}: {error}") from error
|
|
144
|
+
if not isinstance(raw, dict):
|
|
145
|
+
raise RuntimeError(f"Unexpected rustdoc JSON root in {rustdoc_json}")
|
|
146
|
+
|
|
147
|
+
index = raw.get("index")
|
|
148
|
+
paths = raw.get("paths")
|
|
149
|
+
if not isinstance(index, dict) or not isinstance(paths, dict):
|
|
150
|
+
raise RuntimeError("rustdoc JSON does not contain index/paths maps")
|
|
151
|
+
|
|
152
|
+
graph = ApiGraph(project=project, version=version, language="rust")
|
|
153
|
+
rust_to_entity: dict[str, str] = {}
|
|
154
|
+
path_to_id: dict[str, str] = {}
|
|
155
|
+
|
|
156
|
+
for rust_id, raw_item in index.items():
|
|
157
|
+
if not isinstance(raw_item, dict):
|
|
158
|
+
continue
|
|
159
|
+
visibility = raw_item.get("visibility")
|
|
160
|
+
if visibility not in {"public", None} and not (
|
|
161
|
+
isinstance(visibility, dict) and "public" in visibility
|
|
162
|
+
):
|
|
163
|
+
continue
|
|
164
|
+
kind_value = _inner_kind(raw_item)
|
|
165
|
+
kind = _KIND_MAP.get(kind_value or "")
|
|
166
|
+
if kind is None:
|
|
167
|
+
continue
|
|
168
|
+
path_info = paths.get(rust_id)
|
|
169
|
+
path_values = path_info.get("path") if isinstance(path_info, dict) else None
|
|
170
|
+
if isinstance(path_values, list) and path_values:
|
|
171
|
+
qualified = "::".join(str(value) for value in path_values)
|
|
172
|
+
else:
|
|
173
|
+
name = raw_item.get("name")
|
|
174
|
+
if not isinstance(name, str) or not name:
|
|
175
|
+
continue
|
|
176
|
+
qualified = name
|
|
177
|
+
name = str(raw_item.get("name") or qualified.rsplit("::", 1)[-1])
|
|
178
|
+
entity_id = stable_entity_id("rust", kind, qualified)
|
|
179
|
+
source = None
|
|
180
|
+
span = raw_item.get("span")
|
|
181
|
+
if isinstance(span, dict) and isinstance(span.get("filename"), str):
|
|
182
|
+
filename = Path(str(span["filename"]))
|
|
183
|
+
if project_root is not None:
|
|
184
|
+
try:
|
|
185
|
+
filename_text = (
|
|
186
|
+
filename.resolve().relative_to(project_root.resolve()).as_posix()
|
|
187
|
+
)
|
|
188
|
+
except (OSError, ValueError):
|
|
189
|
+
filename_text = filename.as_posix()
|
|
190
|
+
else:
|
|
191
|
+
filename_text = filename.as_posix()
|
|
192
|
+
begin = span.get("begin")
|
|
193
|
+
line = None
|
|
194
|
+
column = None
|
|
195
|
+
if isinstance(begin, list) and len(begin) >= 2:
|
|
196
|
+
if isinstance(begin[0], int):
|
|
197
|
+
line = begin[0]
|
|
198
|
+
if isinstance(begin[1], int):
|
|
199
|
+
column = begin[1]
|
|
200
|
+
source = SourceLocation(filename_text, line, column)
|
|
201
|
+
if project_root is not None:
|
|
202
|
+
physical = filename if filename.is_absolute() else project_root / filename
|
|
203
|
+
try:
|
|
204
|
+
graph.sources.setdefault(filename_text, physical.read_text(encoding="utf-8"))
|
|
205
|
+
except (OSError, UnicodeDecodeError):
|
|
206
|
+
pass
|
|
207
|
+
function_signature, function_properties = _function_signature(raw_item)
|
|
208
|
+
entity = ApiEntity(
|
|
209
|
+
id=entity_id,
|
|
210
|
+
language="rust",
|
|
211
|
+
kind=kind,
|
|
212
|
+
name=name,
|
|
213
|
+
qualified_name=qualified,
|
|
214
|
+
signatures=[function_signature] if function_signature is not None else [],
|
|
215
|
+
documentation=str(raw_item.get("docs") or ""),
|
|
216
|
+
source=source,
|
|
217
|
+
properties=function_properties or ["pub"],
|
|
218
|
+
)
|
|
219
|
+
graph.add(entity)
|
|
220
|
+
rust_to_entity[str(rust_id)] = entity_id
|
|
221
|
+
path_to_id[qualified] = entity_id
|
|
222
|
+
|
|
223
|
+
# Infer hierarchy from semantic paths. rustdoc ids are deliberately not exposed outside this
|
|
224
|
+
# adapter; renderer URLs depend only on the normalized cpdocs identity.
|
|
225
|
+
for entity in graph.entities.values():
|
|
226
|
+
parent_path = (
|
|
227
|
+
entity.qualified_name.rsplit("::", 1)[0] if "::" in entity.qualified_name else ""
|
|
228
|
+
)
|
|
229
|
+
parent = path_to_id.get(parent_path)
|
|
230
|
+
if parent and parent != entity.id:
|
|
231
|
+
entity.parent = parent
|
|
232
|
+
|
|
233
|
+
graph.rebuild_children()
|
|
234
|
+
return graph
|