Skip to main content

gen_gomod/
golist.rs

1//! `go list -deps -json` typed model + stream parser.
2//!
3//! `go list` is the offline resolver — the cargo-metadata analogue.
4//! Given a vendored tree (`-mod=vendor`, `GOPROXY=off`) it does the
5//! three hard things the encoder never re-implements: build-constraint
6//! resolution (`GoFiles` are already tag-filtered for the tuple —
7//! Go-I6), `replace`/vendor rewriting (`Dir` points at the replacement,
8//! `ImportMap` carries the rewrite), and the transitive dep closure
9//! (`-deps` emits one JSON object per package in the closure, std
10//! included).
11//!
12//! Output is a CONCATENATION of JSON objects (not a JSON array), so it
13//! is parsed with `serde_json::Deserializer::into_iter` — the same
14//! stream shape `go list` has always emitted.
15
16use serde::Deserialize;
17
18use crate::build_spec::PackageKind;
19use crate::error::GomodError;
20
21/// One `go list -json` package object. Field names are Go's PascalCase;
22/// every list/map/bool field defaults so an omitted-when-empty field
23/// (go list's convention) deserializes cleanly.
24#[derive(Clone, Debug, Deserialize)]
25#[serde(rename_all = "PascalCase")]
26pub struct GoListPackage {
27    /// Absolute on-disk directory of the package.
28    pub dir: String,
29    /// Canonical import path — the node id.
30    pub import_path: String,
31    /// Package clause name; `"main"` ⇒ a linkable binary.
32    #[serde(default)]
33    pub name: String,
34    /// Module root directory this package lives under. For own packages
35    /// AND vendored deps this is the main module's root (vendor/ is
36    /// under it); for std it is `$GOROOT/src`.
37    #[serde(default)]
38    pub root: String,
39    /// True ⇒ standard-library package (provided by the shared std
40    /// derivation, never per-node source).
41    #[serde(default)]
42    pub standard: bool,
43    /// True ⇒ this package is only in the graph as a dependency (never
44    /// a queried root) — so it is never a `workspace_member`.
45    #[serde(default)]
46    pub dep_only: bool,
47    /// Tag-resolved non-test Go sources for this tuple (Go-I6).
48    #[serde(default)]
49    pub go_files: Vec<String>,
50    /// cgo sources. Non-empty on a module/main node ⇒ M1 rejects the
51    /// build (Go-I12); M-cgo lifts this to `kind = Cgo`.
52    #[serde(default)]
53    pub cgo_files: Vec<String>,
54    /// Assembly sources. Non-empty on a non-std node ⇒ M1 rejects
55    /// (Go-I12); std asm lives inside the opaque std-tree.
56    #[serde(default)]
57    pub s_files: Vec<String>,
58    /// `//go:embed` patterns (drives `-embedcfg`).
59    #[serde(default)]
60    pub embed_patterns: Vec<String>,
61    /// Resolved embed files (relative to `Dir`).
62    #[serde(default)]
63    pub embed_files: Vec<String>,
64    /// Direct imports (the per-node DAG edges). `Deps` (transitive) is
65    /// intentionally NOT modeled — the closure is seeded by the full
66    /// stream, and per-node importcfg needs only direct edges.
67    #[serde(default)]
68    pub imports: Vec<String>,
69    /// Vendor/replace rewrite: source import path → actual package path.
70    #[serde(default)]
71    pub import_map: std::collections::BTreeMap<String, String>,
72    /// Owning module (own module vs a vendored dep). Absent for std.
73    #[serde(default)]
74    pub module: Option<GoListModule>,
75}
76
77#[derive(Clone, Debug, Deserialize)]
78#[serde(rename_all = "PascalCase")]
79pub struct GoListModule {
80    #[serde(default)]
81    pub path: String,
82    /// Absent for the main module; `Some` for a vendored dep.
83    #[serde(default)]
84    pub version: Option<String>,
85    /// True for the main module.
86    #[serde(default)]
87    pub main: bool,
88    #[serde(default)]
89    pub go_version: Option<String>,
90}
91
92impl GoListPackage {
93    /// Classify into the M1 [`PackageKind`]. cgo/asm nodes are NOT
94    /// classified here — the encoder rejects them up front (Go-I12).
95    #[must_use]
96    pub fn kind(&self) -> PackageKind {
97        if self.standard {
98            PackageKind::Std
99        } else if self.name == "main" {
100            PackageKind::Main
101        } else {
102            PackageKind::Module
103        }
104    }
105}
106
107/// Parse the concatenated-JSON-object stream `go list -json` emits.
108/// Empty input ⇒ empty vec (a module with no packages).
109pub fn parse_stream(json: &str) -> Result<Vec<GoListPackage>, GomodError> {
110    let de = serde_json::Deserializer::from_str(json);
111    let mut out = Vec::new();
112    for item in de.into_iter::<GoListPackage>() {
113        let pkg = item.map_err(|e| GomodError::GoListParse(e.to_string()))?;
114        out.push(pkg);
115    }
116    Ok(out)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    // Two concatenated objects, exactly as `go list -json` emits them
124    // (verified against go1.25 on 2026-07-06).
125    const STREAM: &str = r#"
126{
127	"Dir": "/w/internal/greet",
128	"ImportPath": "example.com/fix/internal/greet",
129	"Name": "greet",
130	"Root": "/w",
131	"DepOnly": true,
132	"GoFiles": ["greet.go", "greet_linux.go"],
133	"EmbedPatterns": ["banner.txt"],
134	"EmbedFiles": ["banner.txt"],
135	"Imports": ["embed", "fmt"],
136	"Module": {"Path": "example.com/fix", "Main": true, "GoVersion": "1.25"}
137}
138{
139	"Dir": "/w/cmd/hello",
140	"ImportPath": "example.com/fix/cmd/hello",
141	"Name": "main",
142	"Root": "/w",
143	"GoFiles": ["main.go"],
144	"Imports": ["example.com/fix/internal/greet"],
145	"Module": {"Path": "example.com/fix", "Main": true}
146}
147{
148	"Dir": "/goroot/src/fmt",
149	"ImportPath": "fmt",
150	"Name": "fmt",
151	"Root": "/goroot/src",
152	"Standard": true,
153	"DepOnly": true,
154	"GoFiles": ["print.go"]
155}
156"#;
157
158    #[test]
159    fn parses_concatenated_object_stream() {
160        let pkgs = parse_stream(STREAM).expect("stream parses");
161        assert_eq!(pkgs.len(), 3);
162        let greet = &pkgs[0];
163        assert_eq!(greet.import_path, "example.com/fix/internal/greet");
164        assert_eq!(greet.kind(), PackageKind::Module);
165        assert!(greet.dep_only);
166        assert_eq!(greet.embed_files, vec!["banner.txt"]);
167        assert_eq!(pkgs[1].kind(), PackageKind::Main);
168        assert_eq!(pkgs[2].kind(), PackageKind::Std);
169        assert!(pkgs[2].standard);
170    }
171
172    #[test]
173    fn empty_stream_is_empty_vec() {
174        assert!(parse_stream("").unwrap().is_empty());
175        assert!(parse_stream("   \n").unwrap().is_empty());
176    }
177
178    #[test]
179    fn malformed_stream_errors_not_panics() {
180        let err = parse_stream("{not json}").unwrap_err();
181        assert!(matches!(err, GomodError::GoListParse(_)));
182    }
183}