1use std::path::Path;
5
6use crate::core::moniker::Moniker;
7#[cfg(test)]
8use crate::core::moniker::MonikerBuilder;
9#[cfg(test)]
10use crate::lang::kinds;
11use crate::lang::{cs, go, java, python, rs, ts};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct Dep {
15 pub package_moniker: Moniker,
16 pub name: String,
17 pub import_root: String,
18 pub version: Option<String>,
19 pub dep_kind: String,
20}
21
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
23pub enum Manifest {
24 Cargo,
25 PackageJson,
26 PomXml,
27 Pyproject,
28 GoMod,
29 Csproj,
30}
31
32impl Manifest {
33 pub const ALL: &'static [Manifest] = &[
34 Self::Cargo,
35 Self::PackageJson,
36 Self::PomXml,
37 Self::Pyproject,
38 Self::GoMod,
39 Self::Csproj,
40 ];
41
42 pub fn tag(self) -> &'static str {
43 match self {
44 Self::Cargo => "cargo",
45 Self::PackageJson => "package_json",
46 Self::PomXml => "pom_xml",
47 Self::Pyproject => "pyproject",
48 Self::GoMod => "go_mod",
49 Self::Csproj => "csproj",
50 }
51 }
52
53 pub fn for_filename(path: &Path) -> Option<Self> {
54 let name = path.file_name()?.to_str()?;
55 match name {
56 "Cargo.toml" => Some(Self::Cargo),
57 "package.json" => Some(Self::PackageJson),
58 "pom.xml" => Some(Self::PomXml),
59 "pyproject.toml" => Some(Self::Pyproject),
60 "go.mod" => Some(Self::GoMod),
61 _ if name.ends_with(".csproj") => Some(Self::Csproj),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Debug)]
68pub enum ManifestError {
69 Cargo(rs::build::CargoError),
70 PackageJson(ts::build::PackageJsonError),
71 PomXml(java::build::PomXmlError),
72 Pyproject(python::build::PyprojectError),
73 GoMod(go::build::GoModError),
74 Csproj(cs::build::CsprojError),
75}
76
77impl std::fmt::Display for ManifestError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::Cargo(e) => e.fmt(f),
81 Self::PackageJson(e) => e.fmt(f),
82 Self::PomXml(e) => e.fmt(f),
83 Self::Pyproject(e) => e.fmt(f),
84 Self::GoMod(e) => e.fmt(f),
85 Self::Csproj(e) => e.fmt(f),
86 }
87 }
88}
89
90impl std::error::Error for ManifestError {
91 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92 match self {
93 Self::Cargo(e) => Some(e),
94 Self::PackageJson(e) => Some(e),
95 Self::PomXml(e) => Some(e),
96 Self::Pyproject(e) => Some(e),
97 Self::GoMod(e) => Some(e),
98 Self::Csproj(e) => Some(e),
99 }
100 }
101}
102
103pub fn parse(manifest: Manifest, project: &[u8], content: &str) -> Result<Vec<Dep>, ManifestError> {
104 match manifest {
105 Manifest::Cargo => rs::build::parse(content)
106 .map_err(ManifestError::Cargo)
107 .map(|v| {
108 v.into_iter()
109 .map(|d| project_into(manifest, project, d))
110 .collect()
111 }),
112 Manifest::PackageJson => ts::build::parse(content)
113 .map_err(ManifestError::PackageJson)
114 .map(|v| {
115 v.into_iter()
116 .map(|d| project_into(manifest, project, d))
117 .collect()
118 }),
119 Manifest::PomXml => java::build::parse(content)
120 .map_err(ManifestError::PomXml)
121 .map(|v| {
122 v.into_iter()
123 .map(|d| project_into(manifest, project, d))
124 .collect()
125 }),
126 Manifest::Pyproject => python::build::parse(content)
127 .map_err(ManifestError::Pyproject)
128 .map(|v| {
129 v.into_iter()
130 .map(|d| project_into(manifest, project, d))
131 .collect()
132 }),
133 Manifest::GoMod => go::build::parse(content)
134 .map_err(ManifestError::GoMod)
135 .map(|v| {
136 v.into_iter()
137 .map(|d| project_into(manifest, project, d))
138 .collect()
139 }),
140 Manifest::Csproj => cs::build::parse(content)
141 .map_err(ManifestError::Csproj)
142 .map(|v| {
143 v.into_iter()
144 .map(|d| project_into(manifest, project, d))
145 .collect()
146 }),
147 }
148}
149
150pub fn package_moniker(manifest: Manifest, project: &[u8], import_root: &str) -> Moniker {
151 match manifest {
152 Manifest::Cargo => rs::build::package_moniker(project, import_root),
153 Manifest::PackageJson => ts::build::package_moniker(project, import_root),
154 Manifest::PomXml => java::build::package_moniker(project, import_root),
155 Manifest::Pyproject => python::build::package_moniker(project, import_root),
156 Manifest::GoMod => go::build::package_moniker(project, import_root),
157 Manifest::Csproj => cs::build::package_moniker(project, import_root),
158 }
159}
160
161trait IntoDep {
162 fn into_dep(self, manifest: Manifest, project: &[u8]) -> Dep;
163}
164
165fn project_into<T: IntoDep>(manifest: Manifest, project: &[u8], dep: T) -> Dep {
166 dep.into_dep(manifest, project)
167}
168
169macro_rules! impl_into_dep {
170 ($($t:ty),* $(,)?) => {
171 $(
172 impl IntoDep for $t {
173 fn into_dep(self, manifest: Manifest, project: &[u8]) -> Dep {
174 let package_moniker = package_moniker(manifest, project, &self.import_root);
175 Dep {
176 package_moniker,
177 name: self.name,
178 import_root: self.import_root,
179 version: self.version,
180 dep_kind: self.dep_kind,
181 }
182 }
183 }
184 )*
185 };
186}
187
188impl_into_dep!(
189 rs::build::Dep,
190 ts::build::Dep,
191 java::build::Dep,
192 python::build::Dep,
193 go::build::Dep,
194 cs::build::Dep,
195);
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use std::path::PathBuf;
201
202 #[test]
203 fn for_filename_recognises_each_manifest() {
204 for (name, want) in [
205 ("Cargo.toml", Manifest::Cargo),
206 ("package.json", Manifest::PackageJson),
207 ("pom.xml", Manifest::PomXml),
208 ("pyproject.toml", Manifest::Pyproject),
209 ("go.mod", Manifest::GoMod),
210 ("MyApp.csproj", Manifest::Csproj),
211 ] {
212 assert_eq!(
213 Manifest::for_filename(&PathBuf::from(name)),
214 Some(want),
215 "{name}"
216 );
217 }
218 }
219
220 #[test]
221 fn for_filename_ignores_unknown_and_directories() {
222 assert!(Manifest::for_filename(&PathBuf::from("README.md")).is_none());
223 assert!(Manifest::for_filename(&PathBuf::from("")).is_none());
224 }
225
226 #[test]
227 fn package_moniker_round_trips_through_uri() {
228 use crate::core::uri::{UriConfig, to_uri};
229 let m = package_moniker(Manifest::PackageJson, b".", "react");
230 let cfg = UriConfig {
231 scheme: "code+moniker://",
232 };
233 let uri = to_uri(&m, &cfg).expect("utf-8 segments");
234 assert_eq!(uri, "code+moniker://./external_pkg:react");
235 }
236
237 #[test]
238 fn parse_cargo_includes_package_moniker_for_each_row() {
239 let toml = r#"
240 [package]
241 name = "demo"
242 version = "0.1.0"
243
244 [dependencies]
245 serde-json = "1.0"
246 "#;
247 let deps = parse(Manifest::Cargo, b".", toml).expect("ok");
248 let demo = deps.iter().find(|d| d.name == "demo").unwrap();
249 assert_eq!(
250 demo.package_moniker,
251 package_moniker(Manifest::Cargo, b".", "demo")
252 );
253 let sj = deps.iter().find(|d| d.name == "serde-json").unwrap();
254 assert_eq!(sj.import_root, "serde_json");
255 assert_eq!(
256 sj.package_moniker,
257 package_moniker(Manifest::Cargo, b".", "serde_json")
258 );
259 }
260
261 #[test]
262 fn parse_pyproject_normalises_import_root_in_moniker() {
263 let toml = r#"
264 [project]
265 name = "demo"
266 dependencies = ["requests-html >=1.0"]
267 "#;
268 let deps = parse(Manifest::Pyproject, b".", toml).expect("ok");
269 let rh = deps
270 .iter()
271 .find(|d| d.name == "requests-html")
272 .expect("dep parsed");
273 assert_eq!(rh.import_root, "requests_html");
274 assert_eq!(
275 rh.package_moniker,
276 package_moniker(Manifest::Pyproject, b".", "requests_html")
277 );
278 }
279
280 #[test]
281 fn package_moniker_splits_go_module_path_on_slash() {
282 let m = package_moniker(Manifest::GoMod, b"app", "github.com/gorilla/mux");
283 use crate::core::uri::{UriConfig, to_uri};
284 let uri = to_uri(
285 &m,
286 &UriConfig {
287 scheme: "code+moniker://",
288 },
289 )
290 .expect("utf-8");
291 assert_eq!(
292 uri,
293 "code+moniker://app/external_pkg:github.com/path:gorilla/path:mux"
294 );
295 }
296
297 #[test]
298 fn package_moniker_splits_csharp_namespace_on_dot() {
299 let m = package_moniker(Manifest::Csproj, b"app", "Newtonsoft.Json");
300 use crate::core::uri::{UriConfig, to_uri};
301 let uri = to_uri(
302 &m,
303 &UriConfig {
304 scheme: "code+moniker://",
305 },
306 )
307 .expect("utf-8");
308 assert_eq!(uri, "code+moniker://app/external_pkg:Newtonsoft/path:Json");
309 }
310
311 #[test]
312 fn parse_dispatches_each_manifest_kind() {
313 let cases: Vec<(Manifest, &str, &str)> = vec![
314 (
315 Manifest::Cargo,
316 r#"[package]
317name = "x"
318version = "0""#,
319 "x",
320 ),
321 (Manifest::PackageJson, r#"{"name":"x","version":"0"}"#, "x"),
322 (
323 Manifest::GoMod,
324 r#"module x
325go 1.21"#,
326 "x",
327 ),
328 ];
329 for (m, content, head) in cases {
330 let deps =
331 parse(m, b".", content).unwrap_or_else(|e| panic!("{} parse failed: {e}", m.tag()));
332 assert!(
333 deps.iter().any(|d| d.import_root == head),
334 "{} did not yield head {head}",
335 m.tag()
336 );
337 }
338 }
339
340 #[test]
341 fn parse_propagates_per_lang_error_variant() {
342 let err = parse(Manifest::Cargo, b".", "not [valid toml").unwrap_err();
343 assert!(matches!(err, ManifestError::Cargo(_)));
344 let err = parse(Manifest::PackageJson, b".", "{not json").unwrap_err();
345 assert!(matches!(err, ManifestError::PackageJson(_)));
346 }
347
348 fn first_external_target(
349 g: &crate::core::code_graph::CodeGraph,
350 head_name: &str,
351 ) -> Option<Moniker> {
352 g.refs()
353 .find(|r| {
354 let mut segs = r.target.as_view().segments();
355 match segs.next() {
356 Some(s) => s.kind == kinds::EXTERNAL_PKG && s.name == head_name.as_bytes(),
357 None => false,
358 }
359 })
360 .map(|r| r.target.clone())
361 }
362
363 #[test]
368 fn package_moniker_binds_extractor_ref_per_language() {
369 use crate::lang::{cs, go, python, rs, ts};
370 let anchor = MonikerBuilder::new().project(b"app").build();
371
372 struct Case {
373 lang: &'static str,
374 manifest: Manifest,
375 extractor_head: &'static str,
376 import_root: &'static str,
377 run: fn(&Moniker) -> crate::core::code_graph::CodeGraph,
378 }
379
380 fn run_ts(a: &Moniker) -> crate::core::code_graph::CodeGraph {
381 ts::extract(
382 "util.ts",
383 "import { x } from 'react';",
384 a,
385 false,
386 &ts::Presets::default(),
387 )
388 }
389 fn run_rs(a: &Moniker) -> crate::core::code_graph::CodeGraph {
390 rs::extract(
391 "util.rs",
392 "use serde_json;",
393 a,
394 false,
395 &rs::Presets::default(),
396 )
397 }
398 fn run_python(a: &Moniker) -> crate::core::code_graph::CodeGraph {
399 python::extract("m.py", "import os\n", a, false, &python::Presets::default())
400 }
401 fn run_go(a: &Moniker) -> crate::core::code_graph::CodeGraph {
402 go::extract(
403 "foo.go",
404 "package foo\nimport \"github.com/gorilla/mux\"\n",
405 a,
406 false,
407 &go::Presets::default(),
408 )
409 }
410 fn run_cs(a: &Moniker) -> crate::core::code_graph::CodeGraph {
411 cs::extract(
412 "F.cs",
413 "using Newtonsoft.Json;\n",
414 a,
415 false,
416 &cs::Presets::default(),
417 )
418 }
419
420 let cases = [
421 Case {
422 lang: "ts",
423 manifest: Manifest::PackageJson,
424 extractor_head: "react",
425 import_root: "react",
426 run: run_ts,
427 },
428 Case {
429 lang: "rs",
430 manifest: Manifest::Cargo,
431 extractor_head: "serde_json",
432 import_root: "serde_json",
433 run: run_rs,
434 },
435 Case {
436 lang: "python",
437 manifest: Manifest::Pyproject,
438 extractor_head: "os",
439 import_root: "os",
440 run: run_python,
441 },
442 Case {
443 lang: "go",
444 manifest: Manifest::GoMod,
445 extractor_head: "github.com",
446 import_root: "github.com/gorilla/mux",
447 run: run_go,
448 },
449 Case {
450 lang: "cs",
451 manifest: Manifest::Csproj,
452 extractor_head: "Newtonsoft",
453 import_root: "Newtonsoft.Json",
454 run: run_cs,
455 },
456 ];
457
458 for case in cases {
459 let g = (case.run)(&anchor);
460 let target = first_external_target(&g, case.extractor_head).unwrap_or_else(|| {
461 panic!(
462 "lang={}: no ref target with head external_pkg:{}",
463 case.lang, case.extractor_head
464 )
465 });
466 let pkg = package_moniker(case.manifest, b"app", case.import_root);
467 assert!(
468 pkg.as_view().is_ancestor_of(&target.as_view()) || pkg == target,
469 "lang={}: package_moniker({}) must be @>-ancestor of ref target (pkg={:?} target={:?})",
470 case.lang,
471 case.import_root,
472 pkg.as_bytes(),
473 target.as_bytes(),
474 );
475 }
476 }
477
478 #[test]
479 fn ts_scoped_package_moniker_binds_extractor_ref() {
480 use crate::lang::ts;
481 let anchor = MonikerBuilder::new().project(b"app").build();
482 let g = ts::extract(
483 "util.ts",
484 "import x from '@scope/pkg';",
485 &anchor,
486 false,
487 &ts::Presets::default(),
488 );
489 let target = first_external_target(&g, "@scope/pkg").expect("scoped ref");
490 let pkg = package_moniker(Manifest::PackageJson, b"app", "@scope/pkg");
491 assert!(
492 pkg.as_view().is_ancestor_of(&target.as_view()) || pkg == target,
493 "scoped pkg must bind extractor ref via @>"
494 );
495 }
496}