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}