Last active
October 31, 2025 10:47
-
-
Save lmmx/0c8b157c58365c18663a57a7a0c050dc to your computer and use it in GitHub Desktop.
Rust-script to parse cargo doc diagnostics as JSON in Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env rust-script | |
| //! ```cargo | |
| //! [dependencies] | |
| //! anstream = "0.6" | |
| //! serde = { version = "1", features = ["derive"] } | |
| //! serde_json = "1" | |
| //! ``` | |
| use serde::{Deserialize, Serialize}; | |
| use std::io::{self, BufRead}; | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Coordinate { | |
| cfgs: Option<Vec<String>>, | |
| env: Option<Vec<Option<serde_json::Value>>>, | |
| executable: Option<serde_json::Value>, | |
| features: Option<Vec<String>>, | |
| filenames: Option<Vec<String>>, | |
| fresh: Option<bool>, | |
| linked_libs: Option<Vec<String>>, | |
| linked_paths: Option<Vec<Option<serde_json::Value>>>, | |
| manifest_path: Option<String>, | |
| message: Option<Message>, | |
| out_dir: Option<String>, | |
| package_id: Option<String>, | |
| profile: Option<Profile>, | |
| reason: String, | |
| success: Option<bool>, | |
| target: Option<Target>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Message { | |
| #[serde(rename = "$message_type")] | |
| message_type: String, | |
| children: Vec<Child>, | |
| code: Code, | |
| level: String, | |
| message: String, | |
| rendered: String, | |
| spans: Vec<Span>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Child { | |
| children: Vec<Option<serde_json::Value>>, | |
| code: Option<serde_json::Value>, | |
| level: String, | |
| message: String, | |
| rendered: Option<serde_json::Value>, | |
| spans: Vec<Option<serde_json::Value>>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Code { | |
| code: String, | |
| explanation: Option<serde_json::Value>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Span { | |
| byte_end: i64, | |
| byte_start: i64, | |
| column_end: i64, | |
| column_start: i64, | |
| expansion: Option<serde_json::Value>, | |
| file_name: String, | |
| is_primary: bool, | |
| label: Option<serde_json::Value>, | |
| line_end: i64, | |
| line_start: i64, | |
| suggested_replacement: Option<serde_json::Value>, | |
| suggestion_applicability: Option<serde_json::Value>, | |
| text: Vec<Text>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Text { | |
| highlight_end: i64, | |
| highlight_start: i64, | |
| text: String, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Profile { | |
| debug_assertions: bool, | |
| debuginfo: i64, | |
| opt_level: String, | |
| overflow_checks: bool, | |
| test: bool, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Target { | |
| crate_types: Vec<String>, | |
| doc: bool, | |
| doctest: bool, | |
| edition: String, | |
| kind: Vec<String>, | |
| name: String, | |
| src_path: String, | |
| test: bool, | |
| } | |
| fn main() { | |
| let stdin = io::stdin(); | |
| let reader = stdin.lock(); | |
| for (line_num, line) in reader.lines().enumerate() { | |
| match line { | |
| Ok(line_content) => { | |
| if line_content.trim().is_empty() { | |
| continue; | |
| } | |
| match serde_json::from_str::<Coordinate>(&line_content) { | |
| Ok(coord) => { | |
| anstream::println!("=== Entry {} ===", line_num + 1); | |
| anstream::println!("Reason: {}", coord.reason); | |
| if let Some(pkg_id) = &coord.package_id { | |
| anstream::println!("Package ID: {}", pkg_id); | |
| } | |
| if let Some(target) = &coord.target { | |
| anstream::println!("Target: {} ({})", target.name, target.kind.join(", ")); | |
| anstream::println!("Source: {}", target.src_path); | |
| } | |
| if let Some(success) = coord.success { | |
| anstream::println!("Success: {}", success); | |
| } | |
| if let Some(msg) = &coord.message { | |
| anstream::println!("Message Level: {}", msg.level); | |
| anstream::println!("Message: {}", msg.message); | |
| if let Some(code) = msg.code.explanation.as_ref() { | |
| anstream::println!("Code: {} ({})", msg.code.code, code); | |
| } else { | |
| anstream::println!("Code: {}", msg.code.code); | |
| } | |
| } | |
| anstream::println!(); | |
| } | |
| Err(e) => { | |
| anstream::eprintln!("Error parsing line {}: {}", line_num + 1, e); | |
| anstream::eprintln!("Line content: {}", line_content); | |
| } | |
| } | |
| } | |
| Err(e) => { | |
| anstream::eprintln!("Error reading line {}: {}", line_num + 1, e); | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env rust-script | |
| //! ```cargo | |
| //! [dependencies] | |
| //! anstream = "0.6" | |
| //! serde = { version = "1", features = ["derive"] } | |
| //! serde_json = "1" | |
| //! ``` | |
| use serde::{Deserialize, Serialize}; | |
| use std::io::{self, BufRead}; | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Coordinate { | |
| cfgs: Option<Vec<String>>, | |
| env: Option<Vec<Option<serde_json::Value>>>, | |
| executable: Option<serde_json::Value>, | |
| features: Option<Vec<String>>, | |
| filenames: Option<Vec<String>>, | |
| fresh: Option<bool>, | |
| linked_libs: Option<Vec<String>>, | |
| linked_paths: Option<Vec<Option<serde_json::Value>>>, | |
| manifest_path: Option<String>, | |
| message: Option<Message>, | |
| out_dir: Option<String>, | |
| package_id: Option<String>, | |
| profile: Option<Profile>, | |
| reason: String, | |
| success: Option<bool>, | |
| target: Option<Target>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Message { | |
| #[serde(rename = "$message_type")] | |
| message_type: String, | |
| children: Vec<Child>, | |
| code: Code, | |
| level: String, | |
| message: String, | |
| rendered: String, | |
| spans: Vec<Span>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Child { | |
| children: Vec<Option<serde_json::Value>>, | |
| code: Option<serde_json::Value>, | |
| level: String, | |
| message: String, | |
| rendered: Option<serde_json::Value>, | |
| spans: Vec<Option<serde_json::Value>>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Code { | |
| code: String, | |
| explanation: Option<serde_json::Value>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Span { | |
| byte_end: i64, | |
| byte_start: i64, | |
| column_end: i64, | |
| column_start: i64, | |
| expansion: Option<serde_json::Value>, | |
| file_name: String, | |
| is_primary: bool, | |
| label: Option<serde_json::Value>, | |
| line_end: i64, | |
| line_start: i64, | |
| suggested_replacement: Option<serde_json::Value>, | |
| suggestion_applicability: Option<serde_json::Value>, | |
| text: Vec<Text>, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Text { | |
| highlight_end: i64, | |
| highlight_start: i64, | |
| text: String, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Profile { | |
| debug_assertions: bool, | |
| debuginfo: i64, | |
| opt_level: String, | |
| overflow_checks: bool, | |
| test: bool, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| pub struct Target { | |
| crate_types: Vec<String>, | |
| doc: bool, | |
| doctest: bool, | |
| edition: String, | |
| kind: Vec<String>, | |
| name: String, | |
| src_path: String, | |
| test: bool, | |
| } | |
| fn main() { | |
| let stdin = io::stdin(); | |
| let reader = stdin.lock(); | |
| for (line_num, line) in reader.lines().enumerate() { | |
| match line { | |
| Ok(line_content) => { | |
| if line_content.trim().is_empty() { | |
| continue; | |
| } | |
| match serde_json::from_str::<Coordinate>(&line_content) { | |
| Ok(coord) => { | |
| anstream::println!("╔═══ Entry {} ═══", line_num + 1); | |
| anstream::println!("║ Reason: {}", coord.reason); | |
| if let Some(pkg_id) = &coord.package_id { | |
| anstream::println!("║ Package ID: {}", pkg_id); | |
| } | |
| if let Some(manifest) = &coord.manifest_path { | |
| anstream::println!("║ Manifest: {}", manifest); | |
| } | |
| if let Some(target) = &coord.target { | |
| anstream::println!("║ Target: {} ({})", target.name, target.kind.join(", ")); | |
| anstream::println!("║ Edition: {}", target.edition); | |
| anstream::println!("║ Source: {}", target.src_path); | |
| anstream::println!("║ Crate types: {}", target.crate_types.join(", ")); | |
| } | |
| if let Some(profile) = &coord.profile { | |
| anstream::println!("║ Profile: opt_level={}, debuginfo={}, debug_assertions={}", | |
| profile.opt_level, profile.debuginfo, profile.debug_assertions); | |
| } | |
| if let Some(features) = &coord.features { | |
| if !features.is_empty() { | |
| anstream::println!("║ Features: {}", features.join(", ")); | |
| } | |
| } | |
| if let Some(filenames) = &coord.filenames { | |
| if !filenames.is_empty() { | |
| anstream::println!("║ Output files:"); | |
| for file in filenames { | |
| anstream::println!("║ - {}", file); | |
| } | |
| } | |
| } | |
| if let Some(success) = coord.success { | |
| anstream::println!("║ Success: {}", success); | |
| } | |
| if let Some(fresh) = coord.fresh { | |
| anstream::println!("║ Fresh: {}", fresh); | |
| } | |
| if let Some(out_dir) = &coord.out_dir { | |
| anstream::println!("║ Out dir: {}", out_dir); | |
| } | |
| if let Some(msg) = &coord.message { | |
| anstream::println!("║"); | |
| anstream::println!("║ ┌─ Compiler Message ─"); | |
| anstream::println!("║ │ Level: {}", msg.level); | |
| anstream::println!("║ │ Type: {}", msg.message_type); | |
| anstream::println!("║ │ Code: {}", msg.code.code); | |
| anstream::println!("║ │ Message: {}", msg.message); | |
| if !msg.spans.is_empty() { | |
| anstream::println!("║ │"); | |
| anstream::println!("║ │ Code Spans:"); | |
| for (i, span) in msg.spans.iter().enumerate() { | |
| anstream::println!("║ │ Span {} [{}]:", i + 1, | |
| if span.is_primary { "PRIMARY" } else { "secondary" }); | |
| anstream::println!("║ │ File: {}", span.file_name); | |
| anstream::println!("║ │ Lines: {}-{}, Cols: {}-{}", | |
| span.line_start, span.line_end, | |
| span.column_start, span.column_end); | |
| if let Some(label) = &span.label { | |
| anstream::println!("║ │ Label: {}", label); | |
| } | |
| if !span.text.is_empty() { | |
| anstream::println!("║ │ Code:"); | |
| for text in &span.text { | |
| anstream::println!("║ │ {}", text.text); | |
| // Show highlight if applicable | |
| if text.highlight_start > 0 || text.highlight_end > 0 { | |
| let spaces = " ".repeat(text.highlight_start as usize); | |
| let carets = "^".repeat( | |
| (text.highlight_end - text.highlight_start).max(1) as usize | |
| ); | |
| anstream::println!("║ │ {}{}", spaces, carets); | |
| } | |
| } | |
| } | |
| if let Some(suggestion) = &span.suggested_replacement { | |
| anstream::println!("║ │ Suggested replacement: {}", suggestion); | |
| } | |
| } | |
| } | |
| if !msg.children.is_empty() { | |
| anstream::println!("║ │"); | |
| anstream::println!("║ │ Additional notes:"); | |
| for (i, child) in msg.children.iter().enumerate() { | |
| anstream::println!("║ │ [{}] {}: {}", i + 1, child.level, child.message); | |
| } | |
| } | |
| anstream::println!("║ │"); | |
| anstream::println!("║ │ Rendered output:"); | |
| for line in msg.rendered.lines() { | |
| anstream::println!("║ │ {}", line); | |
| } | |
| anstream::println!("║ └─"); | |
| } | |
| anstream::println!("╚═══════════════════════════════"); | |
| anstream::println!(); | |
| } | |
| Err(e) => { | |
| anstream::eprintln!("❌ Error parsing line {}: {}", line_num + 1, e); | |
| anstream::eprintln!("Line content: {}", line_content); | |
| } | |
| } | |
| } | |
| Err(e) => { | |
| anstream::eprintln!("❌ Error reading line {}: {}", line_num + 1, e); | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| cargo doc --message-format=json 2>/dev/null | ./cdoc.rs - |
Parsing cargo doc diagnostics output as Rust structs via the JSON message format ✨ https://gist.github.com/lmmx/0c8b157c58365c18663a57a7a0c050dc
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example output