Skip to main content

code_moniker_workspace/
extract.rs

1use std::path::Path;
2
3use code_moniker_core::core::code_graph::CodeGraph;
4use code_moniker_core::core::moniker::{Moniker, MonikerBuilder};
5use code_moniker_core::lang::{Lang, ts};
6
7use crate::sources::CBuildContext;
8use crate::tsconfig::TsResolution;
9
10#[derive(Debug, Clone, Default)]
11pub struct Context {
12	pub c: CBuildContext,
13	pub ts: TsResolution,
14	pub project: Option<String>,
15	pub srcset: Option<String>,
16}
17
18pub fn extract(lang: Lang, source: &str, path: &Path) -> CodeGraph {
19	extract_with(lang, source, path, &Context::default())
20}
21
22pub fn source_root(lang: Lang, path: &Path, ctx: &Context) -> Option<Moniker> {
23	let uri = path.to_str()?;
24	let anchor = path_anchor(path, ctx);
25	lang.file_root(uri, &anchor)
26}
27
28pub fn extract_with(lang: Lang, source: &str, path: &Path, ctx: &Context) -> CodeGraph {
29	let uri = path.to_str().unwrap_or("single-file");
30	let anchor = path_anchor(path, ctx);
31	let deep = true;
32	let mut graph = match lang {
33		Lang::Ts => {
34			let presets = ts::Presets {
35				path_aliases: ctx.ts.aliases.clone(),
36				sdk_profile: ctx.ts.sdk_profile_for(path).clone(),
37				..ts::Presets::default()
38			};
39			ts::extract(uri, source, &anchor, deep, &presets)
40		}
41		Lang::Rs => code_moniker_core::lang::rs::extract_sdk(
42			uri,
43			source,
44			&anchor,
45			deep,
46			&code_moniker_core::lang::rs::Presets::default(),
47		),
48		Lang::Java => code_moniker_core::lang::java::extract_sdk(
49			uri,
50			source,
51			&anchor,
52			deep,
53			&code_moniker_core::lang::java::Presets::default(),
54		),
55		Lang::Python => code_moniker_core::lang::python::extract(
56			uri,
57			source,
58			&anchor,
59			deep,
60			&code_moniker_core::lang::python::Presets::default(),
61		),
62		Lang::Go => code_moniker_core::lang::go::extract(
63			uri,
64			source,
65			&anchor,
66			deep,
67			&code_moniker_core::lang::go::Presets::default(),
68		),
69		Lang::C => {
70			let presets = ctx.c.extraction_presets();
71			code_moniker_core::lang::c::extract(uri, source, &anchor, deep, &presets)
72		}
73		Lang::Cs => code_moniker_core::lang::cs::extract(
74			uri,
75			source,
76			&anchor,
77			deep,
78			&code_moniker_core::lang::cs::Presets::default(),
79		),
80		Lang::Sql => code_moniker_core::lang::sql::extract(
81			uri,
82			source,
83			&anchor,
84			deep,
85			&code_moniker_core::lang::sql::Presets::default(),
86		),
87	};
88	graph.shrink_to_fit();
89	graph
90}
91
92fn path_anchor(path: &Path, ctx: &Context) -> Moniker {
93	let project = ctx.project.as_deref().map(str::as_bytes).unwrap_or(b".");
94	let srcset = ctx
95		.srcset
96		.as_deref()
97		.or_else(|| srcset(path))
98		.map(str::as_bytes);
99	anchor_moniker(project, srcset)
100}
101
102fn anchor_moniker(project: &[u8], srcset: Option<&[u8]>) -> Moniker {
103	let mut b = MonikerBuilder::new();
104	b.project(project);
105	if let Some(srcset) = srcset {
106		b.segment(b"srcset", srcset);
107	}
108	b.build()
109}
110
111fn srcset(path: &Path) -> Option<&'static str> {
112	let parts: Vec<_> = path
113		.components()
114		.filter_map(|component| component.as_os_str().to_str())
115		.collect();
116	for window in parts.windows(2) {
117		match window {
118			["src", "main"] => return Some("main"),
119			["src", "test"] | ["src", "tests"] => return Some("test"),
120			_ => {}
121		}
122	}
123	None
124}
125
126pub fn file_uri(path: &Path) -> String {
127	let abs = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
128	format!("file://{}", abs.display())
129}
130
131#[cfg(test)]
132mod tests {
133	use super::*;
134	use std::fs;
135
136	use tempfile::tempdir;
137
138	#[test]
139	fn path_derived_roots_match_extracted_graph_roots() {
140		let cases = [
141			(Lang::Rs, "src/tools/mod.rs", ""),
142			(Lang::Ts, "src/tools/read.ts", ""),
143			(Lang::Python, "src/tools/read.py", ""),
144			(Lang::Go, "src/tools/read.go", ""),
145			(Lang::C, "src/tools/read.c", ""),
146			(Lang::Cs, "src/tools/Read.cs", ""),
147			(Lang::Sql, "db/tools/read.sql", ""),
148			(
149				Lang::Java,
150				"src/main/java/app/tools/Read.java",
151				"package app.tools; class Read {}",
152			),
153		];
154		let ctx = Context::default();
155		for (lang, path, source) in cases {
156			let path = Path::new(path);
157			let catalog_root = source_root(lang, path, &ctx)
158				.unwrap_or_else(|| panic!("{} should expose a path root", lang.tag()));
159			let graph = extract_with(lang, source, path, &ctx);
160			assert_eq!(
161				&catalog_root,
162				graph.root(),
163				"{} catalog root drifted from extraction",
164				lang.tag()
165			);
166		}
167	}
168
169	#[test]
170	fn typescript_extraction_uses_the_nearest_tsconfig_sdk_profile() {
171		let temp = tempdir().expect("tempdir");
172		let server = temp.path().join("server");
173		let web = temp.path().join("web");
174		fs::create_dir_all(&server).expect("server dir");
175		fs::create_dir_all(&web).expect("web dir");
176		fs::write(
177			server.join("tsconfig.json"),
178			r#"{"compilerOptions":{"target":"ES2022","lib":["ES2022"]}}"#,
179		)
180		.expect("server tsconfig");
181		fs::write(
182			web.join("tsconfig.json"),
183			r#"{"compilerOptions":{"target":"ES2022","lib":["ES2022","DOM"]}}"#,
184		)
185		.expect("web tsconfig");
186		let ctx = Context {
187			ts: crate::tsconfig::load(temp.path()),
188			..Context::default()
189		};
190		let source = "export function render() { document.body.replaceChildren(); return Promise.resolve(); }";
191
192		let server_graph = extract_with(Lang::Ts, source, &server.join("main.ts"), &ctx);
193		let web_graph = extract_with(Lang::Ts, source, &web.join("main.ts"), &ctx);
194		let is_sdk_target = |graph: &CodeGraph, name: &[u8]| {
195			graph.refs().any(|reference| {
196				let segments = reference.target.as_view().segments().collect::<Vec<_>>();
197				segments
198					.first()
199					.is_some_and(|segment| segment.kind == b"sdk")
200					&& segments.last().is_some_and(|segment| segment.name == name)
201			})
202		};
203
204		assert!(is_sdk_target(&web_graph, b"document"));
205		assert!(is_sdk_target(&web_graph, b"replaceChildren"));
206		assert!(!is_sdk_target(&server_graph, b"document"));
207		assert!(!is_sdk_target(&server_graph, b"replaceChildren"));
208		assert!(is_sdk_target(&server_graph, b"resolve"));
209	}
210
211	#[test]
212	fn java_path_root_requires_a_standard_source_root() {
213		let ctx = Context::default();
214		assert!(source_root(Lang::Java, Path::new("fixtures/app/tools/Read.java"), &ctx).is_none());
215	}
216
217	#[test]
218	fn java_path_root_keeps_java_package_segments_after_the_source_root() {
219		let ctx = Context::default();
220		let path = Path::new("src/main/java/com/acme/java/util/Read.java");
221		let source = "package com.acme.java.util; class Read {}";
222		let catalog_root = source_root(Lang::Java, path, &ctx).expect("standard Java path");
223		let graph = extract_with(Lang::Java, source, path, &ctx);
224
225		assert_eq!(&catalog_root, graph.root());
226	}
227
228	#[test]
229	fn java_path_root_accepts_test_and_custom_source_sets() {
230		let ctx = Context::default();
231		for path in [
232			"src/test/java/app/tools/Read.java",
233			"src/integrationTest/java/app/tools/Read.java",
234		] {
235			let path = Path::new(path);
236			let catalog_root =
237				source_root(Lang::Java, path, &ctx).expect("standard Java source root");
238			let graph = extract_with(Lang::Java, "package app.tools; class Read {}", path, &ctx);
239
240			assert_eq!(&catalog_root, graph.root());
241		}
242	}
243
244	#[test]
245	fn java_path_root_exposes_package_misalignment() {
246		let ctx = Context::default();
247		let path = Path::new("src/main/java/app/tools/Read.java");
248		let catalog_root = source_root(Lang::Java, path, &ctx).expect("standard Java path");
249		let graph = extract_with(Lang::Java, "package other; class Read {}", path, &ctx);
250		assert_ne!(
251			&catalog_root,
252			graph.root(),
253			"a declared package mismatch must remain observable"
254		);
255	}
256}