Coverage for plugin/scripts/_skeleton_rust.py: 100%

80 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-15 14:46 +0000

1#!/usr/bin/env python3 

2"""Finish what `cargo init --lib` leaves out of a crate. 

3 

4Two gaps, and they are different in kind. The **manifest** is missing everything 

5crates.io and the template's gates want — `cargo init --lib` writes only 

6``name``/``version``/``edition``, so ``description``, ``repository``, ``homepage`` and 

7``authors`` are all absent, and every one is added only if missing. 

8 

9The **crate root** is missing doc comments. The Rust docs gate is 

10``RUSTDOCFLAGS="-D missing_docs" cargo doc`` (the template's `make docs-coverage`) and it 

11fires on *every* undocumented public item, not just the crate root — so cargo's stub 

12needs both a `//!` module doc and a `///` on the `pub fn add` it writes. Seeding only the 

13first is why a crate straight out of `/rhiza:init` could not pass its own gates. 

14 

15**Unlike the Python path, cargo's placeholder is never substituted** — only added to. 

16`src/lib.rs` carries the crate's only test, so rewriting the file the way 

17`_skeleton_python` rewrites uv's `__init__.py` would delete it, and the template's 

18coverage gate would then measure a crate with no tests. 

19""" 

20 

21from __future__ import annotations 

22 

23import json 

24import re 

25import sys 

26from pathlib import Path 

27from typing import Any 

28 

29sys.path.insert(0, str(Path(__file__).resolve().parent)) 

30import _skeleton_common as common # noqa: E402 

31from _rhiza_toml import merge_table, table_span # noqa: E402 

32 

33_CARGO = "Cargo.toml" 

34 

35# The doc comment cargo's placeholder needs to clear the docs gate, and the line it goes 

36# above. `cargo init --lib` writes `pub fn add` with no `///`, which is a public item — so 

37# a crate straight out of `/rhiza:init` failed `make docs-coverage` until this was seeded. 

38_PLACEHOLDER_FN = "pub fn add(" 

39_PLACEHOLDER_FN_DOC = "/// Returns the sum of `left` and `right`.\n" 

40 

41 

42def is_cargo_placeholder_lib(text: str) -> bool: 

43 """Is *text* still `cargo init --lib`'s untouched `add`/`it_works` skeleton? 

44 

45 Conservative in the same way as `_skeleton_python.is_uv_placeholder_init`: the whole 

46 file must consist of cargo's own lines, so anything the user has added makes this 

47 False and real code is never touched. It gates the `///` :func:`seed_crate_docs` puts 

48 on cargo's `pub fn add` — documenting a *user's* undocumented API on their behalf 

49 would be presumptuous, and wrong more often than right. The placeholder code itself is 

50 never deleted either way, since that would take the project's only test with it. 

51 """ 

52 body = [line.strip() for line in text.splitlines() if line.strip()] 

53 allowed = { 

54 "pub fn add(left: u64, right: u64) -> u64 {", 

55 "left + right", 

56 "}", 

57 "#[cfg(test)]", 

58 "mod tests {", 

59 "use super::*;", 

60 "#[test]", 

61 "fn it_works() {", 

62 "let result = add(2, 2);", 

63 "assert_eq!(result, 4);", 

64 } 

65 return bool(body) and all(line in allowed for line in body) 

66 

67 

68def cargo_package_name(target: Path) -> str | None: 

69 """Return `[package] name` exactly as `Cargo.toml` writes it, or None.""" 

70 manifest = target / _CARGO 

71 if not manifest.is_file(): 

72 return None 

73 lines = manifest.read_text(encoding="utf-8", errors="ignore").splitlines() 

74 span = table_span(lines, "package") 

75 for line in lines[span[0] + 1 : span[1]] if span else []: 

76 match = re.match(r"""^\s*name\s*=\s*["']([^"']+)["']""", line) 

77 if match: 

78 return match.group(1) 

79 return None 

80 

81 

82def crate_name(target: Path) -> str: 

83 """Return the crate's Rust identifier: `[package] name` with `-` mapped to `_`. 

84 

85 The directory is only a fallback. `cargo init --lib --name widget` inside `some-dir/` 

86 produces a crate called `widget`, and naming its doc comment after the folder would 

87 describe a crate that does not exist — the same class of "confidently wrong" the 

88 language axis exists to avoid. 

89 """ 

90 return (cargo_package_name(target) or target.name).replace("-", "_") 

91 

92 

93def seed_crate_docs(target: Path) -> list[str]: 

94 """Document what `cargo init` leaves undocumented in a crate root. 

95 

96 Both edits are additive, and the second is gated on 

97 :func:`is_cargo_placeholder_lib` — cargo's stub gets a `///`, a user's own public API 

98 never does. The file is never rewritten wholesale. 

99 

100 Returns the relative paths modified (empty when both roots already have docs). 

101 """ 

102 modified: list[str] = [] 

103 crate = crate_name(target) 

104 for name in ("lib.rs", "main.rs"): 

105 root = target / "src" / name 

106 if not root.is_file(): 

107 continue 

108 text = root.read_text(encoding="utf-8") 

109 new_text = text 

110 if is_cargo_placeholder_lib(new_text): 

111 new_text = new_text.replace(_PLACEHOLDER_FN, _PLACEHOLDER_FN_DOC + _PLACEHOLDER_FN, 1) 

112 if not new_text.lstrip().startswith("//!"): 

113 new_text = ( 

114 f"//! {crate} crate.\n\n{new_text}" if new_text.strip() else f"//! {crate} crate.\n" 

115 ) 

116 if new_text == text: 

117 continue 

118 root.write_text(new_text, encoding="utf-8") 

119 modified.append(root.relative_to(target).as_posix()) 

120 return modified 

121 

122 

123def set_cargo_keys(text: str, wanted: dict[str, str]) -> tuple[str, list[str]]: 

124 """Insert absent ``[package]`` keys from *wanted*; return ``(new_text, added)``. 

125 

126 Only missing keys are written — a value already in the manifest is the user's and 

127 wins. An absent ``[package]`` table raises ValueError rather than being created: a 

128 manifest without one is a virtual workspace, which has no package to describe. 

129 """ 

130 return merge_table(text, "package", wanted, filename=_CARGO, required=True) 

131 

132 

133def fill_cargo_manifest( 

134 target: Path, 

135 *, 

136 owner: str, 

137 repo: str, 

138 domain: str, 

139 description: str | None, 

140 modified: list[str], 

141 notes: list[str], 

142) -> dict[str, Any]: 

143 """Fill in the `[package]` metadata `cargo init` omits; return a summary dict.""" 

144 manifest = target / _CARGO 

145 # `is_file`, not `exists`: a directory named Cargo.toml would pass the gate here and 

146 # then be read as an absent manifest by every helper downstream. 

147 if not manifest.is_file(): 

148 notes.append("Cargo.toml absent — run `cargo init --lib` first") 

149 return {"modified": modified, "changes": [], "notes": notes, "ok": False} 

150 

151 url = common.host_url(domain, owner, repo) 

152 identity_name, identity_email = common.git_identity(target) 

153 wanted = { 

154 "repository": json.dumps(url), 

155 "homepage": json.dumps(url), 

156 "authors": json.dumps([common.author_entry(owner, identity_name, identity_email)]), 

157 } 

158 if description: 

159 wanted["description"] = json.dumps(description) 

160 

161 original = manifest.read_text(encoding="utf-8") 

162 try: 

163 text, added = set_cargo_keys(original, wanted) 

164 except ValueError as exc: 

165 notes.append(f"Cargo.toml: {exc}") 

166 return {"modified": modified, "changes": [], "notes": notes, "ok": False} 

167 

168 if text != original: 

169 manifest.write_text(text, encoding="utf-8") 

170 modified.append(_CARGO) 

171 notes.append("Cargo.toml: " + ", ".join(added)) 

172 else: 

173 notes.append("Cargo.toml already rhiza-shaped") 

174 

175 notes.append("license is /rhiza:license's job") 

176 return {"modified": modified, "changes": added, "notes": notes, "ok": True} 

177 

178 

179def finish_rust( 

180 target: Path, 

181 *, 

182 owner: str, 

183 repo: str, 

184 domain: str, 

185 description: str | None, 

186 modified: list[str], 

187 notes: list[str], 

188) -> dict[str, Any]: 

189 """Finish a `cargo init --lib` skeleton; return a summary dict.""" 

190 modified.extend(seed_crate_docs(target)) 

191 if modified: 

192 notes.append( 

193 "documented what cargo leaves bare — missing_docs is denied on every " 

194 "public item, not just the crate root" 

195 ) 

196 if common.seed_readme(target, repo=repo, description=description, create=True): 

197 modified.append("README.md") 

198 notes.append("seeded the README.md cargo never writes — /rhiza:docs owns the real one") 

199 return fill_cargo_manifest( 

200 target, 

201 owner=owner, 

202 repo=repo, 

203 domain=domain, 

204 description=description, 

205 modified=modified, 

206 notes=notes, 

207 )