sourcey 3.6.0 → 3.6.2

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.
Files changed (86) hide show
  1. package/README.md +5 -2
  2. package/dist/adapters/index.d.ts +3 -2
  3. package/dist/adapters/index.d.ts.map +1 -1
  4. package/dist/adapters/index.js +35 -3
  5. package/dist/adapters/types.d.ts +8 -2
  6. package/dist/adapters/types.d.ts.map +1 -1
  7. package/dist/cli.js +28 -34
  8. package/dist/client/search.js +181 -119
  9. package/dist/client/tabs.js +23 -0
  10. package/dist/components/layout/Head.d.ts.map +1 -1
  11. package/dist/components/layout/Head.js +7 -6
  12. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  13. package/dist/components/openapi/Introduction.js +16 -1
  14. package/dist/components/openapi/Security.d.ts.map +1 -1
  15. package/dist/components/openapi/Security.js +18 -2
  16. package/dist/config.d.ts +70 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +23 -7
  19. package/dist/core/api-rendering.d.ts +47 -0
  20. package/dist/core/api-rendering.d.ts.map +1 -0
  21. package/dist/core/api-rendering.js +76 -0
  22. package/dist/core/doxygen-loader.d.ts +1 -1
  23. package/dist/core/doxygen-loader.d.ts.map +1 -1
  24. package/dist/core/doxygen-loader.js +37 -19
  25. package/dist/core/godoc-loader.d.ts.map +1 -1
  26. package/dist/core/godoc-loader.js +17 -6
  27. package/dist/core/markdown-loader.d.ts.map +1 -1
  28. package/dist/core/markdown-loader.js +8 -5
  29. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  30. package/dist/core/mcp-normalizer.js +48 -34
  31. package/dist/core/parser.d.ts.map +1 -1
  32. package/dist/core/parser.js +75 -4
  33. package/dist/core/rustdoc-introspector.d.ts +29 -0
  34. package/dist/core/rustdoc-introspector.d.ts.map +1 -0
  35. package/dist/core/rustdoc-introspector.js +236 -0
  36. package/dist/core/rustdoc-loader.d.ts +25 -0
  37. package/dist/core/rustdoc-loader.d.ts.map +1 -0
  38. package/dist/core/rustdoc-loader.js +472 -0
  39. package/dist/core/rustdoc-render.d.ts +82 -0
  40. package/dist/core/rustdoc-render.d.ts.map +1 -0
  41. package/dist/core/rustdoc-render.js +808 -0
  42. package/dist/core/rustdoc-types.d.ts +314 -0
  43. package/dist/core/rustdoc-types.d.ts.map +1 -0
  44. package/dist/core/rustdoc-types.js +18 -0
  45. package/dist/core/search-indexer.js +1 -1
  46. package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
  47. package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
  48. package/dist/core/sourcey-rustdoc/README.md +59 -0
  49. package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
  50. package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
  51. package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
  52. package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
  53. package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
  54. package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
  55. package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
  56. package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
  57. package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
  58. package/dist/dev-server.d.ts.map +1 -1
  59. package/dist/dev-server.js +50 -1
  60. package/dist/index.d.ts +4 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -13
  63. package/dist/init.js +2 -2
  64. package/dist/renderer/changelog-feed.d.ts.map +1 -1
  65. package/dist/renderer/changelog-feed.js +27 -19
  66. package/dist/renderer/context.d.ts +1 -0
  67. package/dist/renderer/context.d.ts.map +1 -1
  68. package/dist/renderer/html-builder.d.ts.map +1 -1
  69. package/dist/renderer/html-builder.js +4 -3
  70. package/dist/renderer/llms.js +57 -2
  71. package/dist/site-assembly.d.ts +5 -0
  72. package/dist/site-assembly.d.ts.map +1 -1
  73. package/dist/site-assembly.js +57 -2
  74. package/dist/themes/default/sourcey.css +300 -0
  75. package/dist/utils/code-samples.d.ts +1 -0
  76. package/dist/utils/code-samples.d.ts.map +1 -1
  77. package/dist/utils/code-samples.js +30 -22
  78. package/dist/utils/html.d.ts +10 -0
  79. package/dist/utils/html.d.ts.map +1 -0
  80. package/dist/utils/html.js +152 -0
  81. package/dist/utils/http.d.ts +0 -2
  82. package/dist/utils/http.d.ts.map +1 -1
  83. package/dist/utils/http.js +19 -34
  84. package/dist/utils/markdown.d.ts.map +1 -1
  85. package/dist/utils/markdown.js +102 -37
  86. package/package.json +10 -5
@@ -0,0 +1,256 @@
1
+ use std::fs;
2
+ use std::io::Write;
3
+ use std::path::{Path, PathBuf};
4
+ use std::process::ExitCode;
5
+
6
+ use anyhow::{anyhow, Context, Result};
7
+ use clap::Parser;
8
+ use flate2::write::GzEncoder;
9
+ use flate2::Compression;
10
+ use rustdoc_json::Builder;
11
+ use rustdoc_types::Crate;
12
+ use time::OffsetDateTime;
13
+
14
+ use sourcey_rustdoc::diagnostics::{self, codes};
15
+ use sourcey_rustdoc::extract::{extract_crate, ExtractOptions};
16
+ use sourcey_rustdoc::spec::{Diagnostic, RustdocSpec, SOURCEY_RUSTDOC_VERSION, SPEC_VERSION};
17
+
18
+ const EXPECTED_FORMAT_VERSION: u32 = rustdoc_types::FORMAT_VERSION;
19
+
20
+ #[derive(Parser, Debug)]
21
+ #[command(version, about = "Convert nightly rustdoc JSON into a sourcey RustdocSpec snapshot.", long_about = None)]
22
+ struct Args {
23
+ /// Path to a Cargo manifest (Cargo.toml) or a workspace directory.
24
+ #[arg(long)]
25
+ manifest: PathBuf,
26
+
27
+ /// Crate names to include. Repeat to include multiple. If omitted, the
28
+ /// manifest's own package is used.
29
+ #[arg(long = "crate", value_name = "NAME")]
30
+ crates: Vec<String>,
31
+
32
+ /// Enable a feature. Repeat for multiple.
33
+ #[arg(long = "features", value_name = "NAME")]
34
+ features: Vec<String>,
35
+
36
+ /// Enable all features.
37
+ #[arg(long)]
38
+ all_features: bool,
39
+
40
+ /// Disable default features.
41
+ #[arg(long)]
42
+ no_default_features: bool,
43
+
44
+ /// Include `pub(crate)` and private items.
45
+ #[arg(long)]
46
+ include_private: bool,
47
+
48
+ /// Include items marked `#[doc(hidden)]`.
49
+ #[arg(long)]
50
+ include_hidden: bool,
51
+
52
+ /// Target triple to build docs for.
53
+ #[arg(long)]
54
+ target: Option<String>,
55
+
56
+ /// rustup toolchain name (default: nightly).
57
+ #[arg(long, default_value = "nightly")]
58
+ toolchain: String,
59
+
60
+ /// rustc cap-lints level (default: warn).
61
+ #[arg(long, default_value = "warn")]
62
+ cap_lints: String,
63
+
64
+ /// Output path; "-" for stdout.
65
+ #[arg(long, default_value = "-")]
66
+ output: String,
67
+
68
+ /// Fail (exit code 2) on any blocking diagnostic.
69
+ #[arg(long)]
70
+ strict: bool,
71
+ }
72
+
73
+ fn main() -> ExitCode {
74
+ let args = Args::parse();
75
+ match run(args) {
76
+ Ok(exit) => exit,
77
+ Err(err) => {
78
+ eprintln!("sourcey-rustdoc: {err:#}");
79
+ ExitCode::from(2)
80
+ }
81
+ }
82
+ }
83
+
84
+ fn run(args: Args) -> Result<ExitCode> {
85
+ let crate_names = resolve_crate_names(&args)?;
86
+ let mut crates = Vec::new();
87
+ let mut diagnostics: Vec<Diagnostic> = Vec::new();
88
+
89
+ for crate_name in &crate_names {
90
+ let json_path = build_rustdoc_json(&args, crate_name)
91
+ .with_context(|| format!("building rustdoc JSON for {crate_name}"))?;
92
+ let krate = load_rustdoc_json(&json_path)
93
+ .with_context(|| format!("loading rustdoc JSON for {crate_name}"))?;
94
+ if krate.format_version != EXPECTED_FORMAT_VERSION {
95
+ let msg = format!(
96
+ "rustdoc JSON format v{} produced by toolchain `{}`; this helper was built against v{}. Install `rustup toolchain install nightly-YYYY-MM-DD` to match.",
97
+ krate.format_version, args.toolchain, EXPECTED_FORMAT_VERSION
98
+ );
99
+ diagnostics.push(diagnostics::error(codes::FORMAT_VERSION_MISMATCH, msg));
100
+ if args.strict {
101
+ return Ok(ExitCode::from(2));
102
+ }
103
+ continue;
104
+ }
105
+ let opts = ExtractOptions {
106
+ include_private: args.include_private,
107
+ include_hidden: args.include_hidden,
108
+ crate_name_hint: crate_name.clone(),
109
+ };
110
+ let result = extract_crate(&krate, &opts);
111
+ diagnostics.extend(result.diagnostics);
112
+ crates.push(result.crate_spec);
113
+ }
114
+
115
+ let has_error_diagnostic = diagnostics
116
+ .iter()
117
+ .any(|d| matches!(d.severity, sourcey_rustdoc::spec::DiagnosticSeverity::Error));
118
+ let snapshot = RustdocSpec {
119
+ version: SPEC_VERSION,
120
+ sourcey_rustdoc_version: SOURCEY_RUSTDOC_VERSION.to_string(),
121
+ rustdoc_format_version: EXPECTED_FORMAT_VERSION,
122
+ rust_toolchain: args.toolchain.clone(),
123
+ generated_at: now_rfc3339(),
124
+ crates,
125
+ diagnostics,
126
+ };
127
+
128
+ // Compact JSON: large snapshots (multi-crate workspaces) benefit from
129
+ // dropping pretty-print whitespace. Gzip is layered on top when the
130
+ // output path ends in `.gz`.
131
+ let serialized =
132
+ serde_json::to_vec(&snapshot).context("serializing RustdocSpec to JSON")?;
133
+ write_output(&args.output, &serialized)?;
134
+
135
+ if args.strict && has_error_diagnostic {
136
+ return Ok(ExitCode::from(2));
137
+ }
138
+ Ok(ExitCode::SUCCESS)
139
+ }
140
+
141
+ fn resolve_crate_names(args: &Args) -> Result<Vec<String>> {
142
+ if !args.crates.is_empty() {
143
+ return Ok(args.crates.clone());
144
+ }
145
+ // Read the package name from the manifest as a fallback.
146
+ let manifest = manifest_toml_path(&args.manifest)?;
147
+ let text = fs::read_to_string(&manifest)
148
+ .with_context(|| format!("reading {}", manifest.display()))?;
149
+ let parsed: toml::Value = toml::from_str(&text).context("parsing Cargo.toml")?;
150
+ if let Some(name) = parsed
151
+ .get("package")
152
+ .and_then(|p| p.get("name"))
153
+ .and_then(|n| n.as_str())
154
+ {
155
+ return Ok(vec![name.to_string()]);
156
+ }
157
+ Err(anyhow!(
158
+ "no --crate provided and no [package].name in {}",
159
+ manifest.display()
160
+ ))
161
+ }
162
+
163
+ fn manifest_toml_path(input: &PathBuf) -> Result<PathBuf> {
164
+ if input.is_file() {
165
+ return Ok(input.clone());
166
+ }
167
+ let joined = input.join("Cargo.toml");
168
+ if joined.is_file() {
169
+ return Ok(joined);
170
+ }
171
+ Err(anyhow!("no Cargo.toml at {}", input.display()))
172
+ }
173
+
174
+ fn build_rustdoc_json(args: &Args, crate_name: &str) -> Result<PathBuf> {
175
+ let manifest = manifest_toml_path(&args.manifest)?;
176
+ let mut builder = Builder::default()
177
+ .toolchain(&args.toolchain)
178
+ .manifest_path(&manifest)
179
+ .package(crate_name)
180
+ .cap_lints(Some(&args.cap_lints))
181
+ .document_private_items(args.include_private)
182
+ .quiet(true);
183
+ if args.all_features {
184
+ builder = builder.all_features(true);
185
+ }
186
+ if args.no_default_features {
187
+ builder = builder.no_default_features(true);
188
+ }
189
+ if !args.features.is_empty() {
190
+ builder = builder.features(args.features.iter().map(String::as_str));
191
+ }
192
+ if let Some(target) = &args.target {
193
+ builder = builder.target(target.clone());
194
+ }
195
+ let path = builder
196
+ .build()
197
+ .map_err(|err| anyhow!("rustdoc-json builder failed: {err}"))?;
198
+ Ok(path)
199
+ }
200
+
201
+ fn load_rustdoc_json(path: &PathBuf) -> Result<Crate> {
202
+ let bytes =
203
+ fs::read(path).with_context(|| format!("reading rustdoc JSON at {}", path.display()))?;
204
+ let krate: Crate = serde_json::from_slice(&bytes).context("parsing rustdoc JSON")?;
205
+ Ok(krate)
206
+ }
207
+
208
+ fn write_output(target: &str, contents: &[u8]) -> Result<()> {
209
+ if target == "-" {
210
+ let stdout = std::io::stdout();
211
+ let mut handle = stdout.lock();
212
+ handle.write_all(contents)?;
213
+ handle.write_all(b"\n")?;
214
+ return Ok(());
215
+ }
216
+ let path = PathBuf::from(target);
217
+ if let Some(parent) = path.parent() {
218
+ if !parent.as_os_str().is_empty() {
219
+ fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
220
+ }
221
+ }
222
+ if is_gz_path(&path) {
223
+ let file = fs::File::create(&path)
224
+ .with_context(|| format!("creating {}", path.display()))?;
225
+ let mut encoder = GzEncoder::new(file, Compression::default());
226
+ encoder
227
+ .write_all(contents)
228
+ .with_context(|| format!("writing gzip stream to {}", path.display()))?;
229
+ encoder.finish().with_context(|| format!("finalising {}", path.display()))?;
230
+ } else {
231
+ fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))?;
232
+ }
233
+ Ok(())
234
+ }
235
+
236
+ fn is_gz_path(path: &Path) -> bool {
237
+ path.extension()
238
+ .and_then(|e| e.to_str())
239
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("gz"))
240
+ }
241
+
242
+ fn now_rfc3339() -> String {
243
+ OffsetDateTime::now_utc()
244
+ .format(&time::format_description::well_known::Rfc3339)
245
+ .unwrap_or_else(|_| "0".to_string())
246
+ }
247
+
248
+ #[cfg(test)]
249
+ mod tests {
250
+ #[test]
251
+ fn format_version_constant_present() {
252
+ // Sentinel: confirms the helper's expected format version matches
253
+ // the pinned rustdoc-types dependency at compile time.
254
+ assert_eq!(super::EXPECTED_FORMAT_VERSION, rustdoc_types::FORMAT_VERSION);
255
+ }
256
+ }
@@ -0,0 +1,415 @@
1
+ use rustdoc_types::{
2
+ DynTrait, FunctionSignature, GenericArg, GenericArgs, GenericBound, Generics as RdGenerics,
3
+ Path as RdPath, PreciseCapturingArg, Type, WherePredicate,
4
+ };
5
+
6
+ use crate::links::LinkContext;
7
+ use crate::spec::{
8
+ GenericParam, GenericParamKind, Generics, SigInput, SigToken, Signature, TypePath,
9
+ };
10
+ use rustdoc_types::Type as RdType;
11
+
12
+ pub fn lower_signature(
13
+ sig: &FunctionSignature,
14
+ name: &str,
15
+ is_const: bool,
16
+ is_async: bool,
17
+ is_unsafe: bool,
18
+ links: &LinkContext<'_>,
19
+ ) -> Signature {
20
+ let inputs: Vec<SigInput> = sig
21
+ .inputs
22
+ .iter()
23
+ .map(|(name, ty)| SigInput {
24
+ name: name.clone(),
25
+ type_display: render_type(ty),
26
+ })
27
+ .collect();
28
+
29
+ let output_display = sig.output.as_ref().map(render_type);
30
+
31
+ let mut tokens: Vec<SigToken> = Vec::new();
32
+ let kw = |text: &str| SigToken::Keyword { text: text.to_string() };
33
+ let punct = |text: &str| SigToken::Punct { text: text.to_string() };
34
+ if is_const {
35
+ tokens.push(kw("const"));
36
+ tokens.push(SigToken::Whitespace);
37
+ }
38
+ if is_async {
39
+ tokens.push(kw("async"));
40
+ tokens.push(SigToken::Whitespace);
41
+ }
42
+ if is_unsafe {
43
+ tokens.push(kw("unsafe"));
44
+ tokens.push(SigToken::Whitespace);
45
+ }
46
+ tokens.push(kw("fn"));
47
+ tokens.push(SigToken::Whitespace);
48
+ tokens.push(SigToken::Type {
49
+ text: name.to_string(),
50
+ target: None,
51
+ });
52
+ tokens.push(punct("("));
53
+ for (idx, ((input_name, input_ty), input)) in sig.inputs.iter().zip(inputs.iter()).enumerate() {
54
+ let _ = input_name;
55
+ if idx > 0 {
56
+ tokens.push(punct(","));
57
+ tokens.push(SigToken::Whitespace);
58
+ }
59
+ tokens.push(SigToken::Punct { text: input.name.clone() });
60
+ tokens.push(punct(":"));
61
+ tokens.push(SigToken::Whitespace);
62
+ push_type_tokens(&mut tokens, &resolve_type_path(input_ty, links));
63
+ }
64
+ if sig.is_c_variadic {
65
+ if !inputs.is_empty() {
66
+ tokens.push(punct(","));
67
+ tokens.push(SigToken::Whitespace);
68
+ }
69
+ tokens.push(punct("..."));
70
+ }
71
+ tokens.push(punct(")"));
72
+
73
+ if let Some(output_ty) = &sig.output {
74
+ tokens.push(SigToken::Whitespace);
75
+ tokens.push(punct("->"));
76
+ tokens.push(SigToken::Whitespace);
77
+ push_type_tokens(&mut tokens, &resolve_type_path(output_ty, links));
78
+ }
79
+ let _ = output_display;
80
+
81
+ let mut display = String::new();
82
+ for token in &tokens {
83
+ match token {
84
+ SigToken::Keyword { text }
85
+ | SigToken::Punct { text }
86
+ | SigToken::Generic { text } => display.push_str(text),
87
+ SigToken::Lifetime { text } => {
88
+ display.push('\'');
89
+ display.push_str(text);
90
+ }
91
+ SigToken::Type { text, .. } => display.push_str(text),
92
+ SigToken::Whitespace => display.push(' '),
93
+ SigToken::Newline => display.push('\n'),
94
+ }
95
+ }
96
+
97
+ Signature {
98
+ display,
99
+ tokens,
100
+ inputs,
101
+ output_display,
102
+ is_c_variadic: sig.is_c_variadic,
103
+ }
104
+ }
105
+
106
+ fn push_type_tokens(out: &mut Vec<SigToken>, resolved: &ResolvedTypeDisplay) {
107
+ out.push(SigToken::Type {
108
+ text: resolved.text.clone(),
109
+ target: resolved.target.clone(),
110
+ });
111
+ }
112
+
113
+ struct ResolvedTypeDisplay {
114
+ text: String,
115
+ target: Option<TypePath>,
116
+ }
117
+
118
+ fn resolve_type_path(ty: &RdType, links: &LinkContext<'_>) -> ResolvedTypeDisplay {
119
+ let text = render_type(ty);
120
+ let target = type_path_target(ty, links);
121
+ ResolvedTypeDisplay { text, target }
122
+ }
123
+
124
+ fn type_path_target(ty: &RdType, links: &LinkContext<'_>) -> Option<TypePath> {
125
+ match ty {
126
+ RdType::ResolvedPath(p) => {
127
+ let resolved = links.resolve_id(&p.id);
128
+ let display = p.path.clone();
129
+ match resolved {
130
+ Some(r) => Some(TypePath {
131
+ crate_id: r.crate_id,
132
+ path: r.path,
133
+ display,
134
+ external: r.external,
135
+ html_root_url: r.html_root_url,
136
+ }),
137
+ None => Some(TypePath {
138
+ crate_id: 0,
139
+ path: vec![p.path.clone()],
140
+ display,
141
+ external: false,
142
+ html_root_url: None,
143
+ }),
144
+ }
145
+ }
146
+ RdType::BorrowedRef { type_, .. } | RdType::RawPointer { type_, .. } => {
147
+ type_path_target(type_, links)
148
+ }
149
+ _ => None,
150
+ }
151
+ }
152
+
153
+ pub fn lower_generics(generics: &RdGenerics) -> Generics {
154
+ let params = generics
155
+ .params
156
+ .iter()
157
+ .map(|p| GenericParam {
158
+ name: p.name.clone(),
159
+ kind: match p.kind {
160
+ rustdoc_types::GenericParamDefKind::Lifetime { .. } => GenericParamKind::Lifetime,
161
+ rustdoc_types::GenericParamDefKind::Type { .. } => GenericParamKind::Type,
162
+ rustdoc_types::GenericParamDefKind::Const { .. } => GenericParamKind::Const,
163
+ },
164
+ default_display: generic_param_default(&p.kind),
165
+ bounds: generic_param_bounds(&p.kind),
166
+ })
167
+ .collect();
168
+ let where_predicates = generics
169
+ .where_predicates
170
+ .iter()
171
+ .map(render_where_predicate)
172
+ .collect();
173
+ Generics {
174
+ params,
175
+ where_predicates,
176
+ }
177
+ }
178
+
179
+ fn generic_param_default(kind: &rustdoc_types::GenericParamDefKind) -> Option<String> {
180
+ match kind {
181
+ rustdoc_types::GenericParamDefKind::Type { default, .. } => default.as_ref().map(render_type),
182
+ rustdoc_types::GenericParamDefKind::Const { default, .. } => default.clone(),
183
+ rustdoc_types::GenericParamDefKind::Lifetime { .. } => None,
184
+ }
185
+ }
186
+
187
+ fn generic_param_bounds(kind: &rustdoc_types::GenericParamDefKind) -> Vec<String> {
188
+ match kind {
189
+ rustdoc_types::GenericParamDefKind::Type { bounds, .. } => {
190
+ bounds.iter().map(render_bound).collect()
191
+ }
192
+ rustdoc_types::GenericParamDefKind::Lifetime { outlives, .. } => outlives.clone(),
193
+ rustdoc_types::GenericParamDefKind::Const { .. } => Vec::new(),
194
+ }
195
+ }
196
+
197
+ fn render_where_predicate(pred: &WherePredicate) -> String {
198
+ match pred {
199
+ WherePredicate::BoundPredicate {
200
+ type_, bounds, generic_params, ..
201
+ } => {
202
+ let mut s = String::new();
203
+ if !generic_params.is_empty() {
204
+ s.push_str("for<");
205
+ let parts: Vec<String> = generic_params.iter().map(|p| p.name.clone()).collect();
206
+ s.push_str(&parts.join(", "));
207
+ s.push_str("> ");
208
+ }
209
+ s.push_str(&render_type(type_));
210
+ if !bounds.is_empty() {
211
+ s.push_str(": ");
212
+ let parts: Vec<String> = bounds.iter().map(render_bound).collect();
213
+ s.push_str(&parts.join(" + "));
214
+ }
215
+ s
216
+ }
217
+ WherePredicate::LifetimePredicate { lifetime, outlives } => {
218
+ let mut s = lifetime.clone();
219
+ if !outlives.is_empty() {
220
+ s.push_str(": ");
221
+ s.push_str(&outlives.join(" + "));
222
+ }
223
+ s
224
+ }
225
+ WherePredicate::EqPredicate { lhs, rhs } => {
226
+ format!("{} = {}", render_type(lhs), render_term(rhs))
227
+ }
228
+ }
229
+ }
230
+
231
+ pub fn render_bound(bound: &GenericBound) -> String {
232
+ match bound {
233
+ GenericBound::TraitBound {
234
+ trait_,
235
+ generic_params,
236
+ modifier,
237
+ } => {
238
+ let mut s = String::new();
239
+ if !generic_params.is_empty() {
240
+ s.push_str("for<");
241
+ let parts: Vec<String> = generic_params.iter().map(|p| p.name.clone()).collect();
242
+ s.push_str(&parts.join(", "));
243
+ s.push_str("> ");
244
+ }
245
+ match modifier {
246
+ rustdoc_types::TraitBoundModifier::None => {}
247
+ rustdoc_types::TraitBoundModifier::Maybe => s.push('?'),
248
+ rustdoc_types::TraitBoundModifier::MaybeConst => s.push_str("~const "),
249
+ }
250
+ s.push_str(&render_path(trait_));
251
+ s
252
+ }
253
+ GenericBound::Outlives(lifetime) => lifetime.clone(),
254
+ GenericBound::Use(captures) => {
255
+ let parts: Vec<String> = captures
256
+ .iter()
257
+ .map(|c| match c {
258
+ PreciseCapturingArg::Lifetime(s) | PreciseCapturingArg::Param(s) => s.clone(),
259
+ })
260
+ .collect();
261
+ format!("use<{}>", parts.join(", "))
262
+ }
263
+ }
264
+ }
265
+
266
+ fn render_term(term: &rustdoc_types::Term) -> String {
267
+ match term {
268
+ rustdoc_types::Term::Type(t) => render_type(t),
269
+ rustdoc_types::Term::Constant(c) => c.expr.clone(),
270
+ }
271
+ }
272
+
273
+ pub fn render_type(ty: &Type) -> String {
274
+ match ty {
275
+ Type::ResolvedPath(p) => render_path(p),
276
+ Type::DynTrait(d) => render_dyn(d),
277
+ Type::Generic(s) => s.clone(),
278
+ Type::Primitive(s) => s.clone(),
279
+ Type::FunctionPointer(f) => format!(
280
+ "fn({}){}",
281
+ f.sig
282
+ .inputs
283
+ .iter()
284
+ .map(|(_, t)| render_type(t))
285
+ .collect::<Vec<_>>()
286
+ .join(", "),
287
+ f.sig
288
+ .output
289
+ .as_ref()
290
+ .map(|t| format!(" -> {}", render_type(t)))
291
+ .unwrap_or_default()
292
+ ),
293
+ Type::Tuple(parts) => {
294
+ let inner: Vec<String> = parts.iter().map(render_type).collect();
295
+ format!("({})", inner.join(", "))
296
+ }
297
+ Type::Slice(inner) => format!("[{}]", render_type(inner)),
298
+ Type::Array { type_, len } => format!("[{}; {}]", render_type(type_), len),
299
+ Type::Pat { type_, .. } => render_type(type_),
300
+ Type::ImplTrait(bounds) => {
301
+ let parts: Vec<String> = bounds.iter().map(render_bound).collect();
302
+ format!("impl {}", parts.join(" + "))
303
+ }
304
+ Type::Infer => "_".to_string(),
305
+ Type::RawPointer { is_mutable, type_ } => {
306
+ let m = if *is_mutable { "*mut " } else { "*const " };
307
+ format!("{}{}", m, render_type(type_))
308
+ }
309
+ Type::BorrowedRef {
310
+ lifetime,
311
+ is_mutable,
312
+ type_,
313
+ } => {
314
+ let lt = lifetime
315
+ .as_ref()
316
+ .map(|l| format!("{} ", l))
317
+ .unwrap_or_default();
318
+ let m = if *is_mutable { "mut " } else { "" };
319
+ format!("&{}{}{}", lt, m, render_type(type_))
320
+ }
321
+ Type::QualifiedPath {
322
+ name,
323
+ args,
324
+ self_type,
325
+ trait_,
326
+ ..
327
+ } => {
328
+ let self_render = render_type(self_type);
329
+ let trait_render = trait_
330
+ .as_ref()
331
+ .map(|p| format!(" as {}", render_path(p)))
332
+ .unwrap_or_default();
333
+ let args_render = match args.as_ref() {
334
+ Some(a) => render_generic_args(a),
335
+ None => String::new(),
336
+ };
337
+ format!("<{}{}>::{}{}", self_render, trait_render, name, args_render)
338
+ }
339
+ }
340
+ }
341
+
342
+ fn render_path(path: &RdPath) -> String {
343
+ let mut s = path.path.clone();
344
+ if let Some(args) = &path.args {
345
+ s.push_str(&render_generic_args(args));
346
+ }
347
+ s
348
+ }
349
+
350
+ fn render_generic_args(args: &GenericArgs) -> String {
351
+ match args {
352
+ GenericArgs::AngleBracketed { args, constraints } => {
353
+ let mut parts: Vec<String> = args
354
+ .iter()
355
+ .map(|a| match a {
356
+ GenericArg::Lifetime(s) => s.clone(),
357
+ GenericArg::Type(t) => render_type(t),
358
+ GenericArg::Const(c) => c.expr.clone(),
359
+ GenericArg::Infer => "_".to_string(),
360
+ })
361
+ .collect();
362
+ for c in constraints {
363
+ let term = match &c.binding {
364
+ rustdoc_types::AssocItemConstraintKind::Equality(t) => render_term(t),
365
+ rustdoc_types::AssocItemConstraintKind::Constraint(bounds) => {
366
+ let bs: Vec<String> = bounds.iter().map(render_bound).collect();
367
+ bs.join(" + ")
368
+ }
369
+ };
370
+ let sep = matches!(
371
+ c.binding,
372
+ rustdoc_types::AssocItemConstraintKind::Equality(_)
373
+ )
374
+ .then(|| "=".to_string())
375
+ .unwrap_or_else(|| ":".to_string());
376
+ parts.push(format!("{}{}{}", c.name, sep, term));
377
+ }
378
+ if parts.is_empty() {
379
+ String::new()
380
+ } else {
381
+ format!("<{}>", parts.join(", "))
382
+ }
383
+ }
384
+ GenericArgs::Parenthesized { inputs, output } => {
385
+ let in_render: Vec<String> = inputs.iter().map(render_type).collect();
386
+ let out_render = output
387
+ .as_ref()
388
+ .map(|t| format!(" -> {}", render_type(t)))
389
+ .unwrap_or_default();
390
+ format!("({}){}", in_render.join(", "), out_render)
391
+ }
392
+ GenericArgs::ReturnTypeNotation => "(..)".to_string(),
393
+ }
394
+ }
395
+
396
+ fn render_dyn(d: &DynTrait) -> String {
397
+ let parts: Vec<String> = d
398
+ .traits
399
+ .iter()
400
+ .map(|t| {
401
+ let mut s = render_path(&t.trait_);
402
+ if !t.generic_params.is_empty() {
403
+ let lts: Vec<String> = t.generic_params.iter().map(|p| p.name.clone()).collect();
404
+ s.push_str(&format!(" + for<{}>", lts.join(", ")));
405
+ }
406
+ s
407
+ })
408
+ .collect();
409
+ let lt = d
410
+ .lifetime
411
+ .as_ref()
412
+ .map(|l| format!(" + {}", l))
413
+ .unwrap_or_default();
414
+ format!("dyn {}{}", parts.join(" + "), lt)
415
+ }