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