1pub mod build_manifest;
2pub mod c;
3pub mod callable;
4pub mod cs;
5pub mod extractor;
6pub mod go;
7pub mod java;
8pub mod kinds;
9pub mod python;
10pub mod rs;
11pub mod sdk;
12pub mod sql;
13pub mod tree_util;
14pub mod ts;
15
16#[doc(hidden)]
17pub use extractor::assert_conformance;
18pub use extractor::{KindSpec, LangExtractor};
19
20macro_rules! define_languages {
23 ($($(#[$attr:meta])* $variant:ident => $module:ty),* $(,)?) => {
24 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
25 pub enum Lang {
26 $(
27 $(#[$attr])*
28 $variant,
29 )*
30 }
31
32 impl Lang {
33 pub const ALL: &'static [Lang] = &[
34 $(
35 $(#[$attr])*
36 Self::$variant,
37 )*
38 ];
39
40 pub fn from_tag(s: &str) -> Option<Self> {
41 $(
42 $(#[$attr])*
43 if s == <$module as $crate::lang::LangExtractor>::LANG_TAG {
44 return Some(Self::$variant);
45 }
46 )*
47 None
48 }
49
50 pub fn tag(self) -> &'static str {
51 match self {
52 $(
53 $(#[$attr])*
54 Self::$variant => <$module as $crate::lang::LangExtractor>::LANG_TAG,
55 )*
56 }
57 }
58
59 pub fn allowed_kinds(self) -> &'static [&'static str] {
60 match self {
61 $(
62 $(#[$attr])*
63 Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_KINDS,
64 )*
65 }
66 }
67
68 pub fn kind_specs(self) -> &'static [$crate::lang::KindSpec] {
69 match self {
70 $(
71 $(#[$attr])*
72 Self::$variant => <$module as $crate::lang::LangExtractor>::KIND_SPECS,
73 )*
74 }
75 }
76
77 pub fn kind_spec(self, id: &str) -> Option<&'static $crate::lang::KindSpec> {
78 self.kind_specs().iter().find(|spec| spec.id == id)
79 }
80
81 pub fn allowed_visibilities(self) -> &'static [&'static str] {
82 match self {
83 $(
84 $(#[$attr])*
85 Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_VISIBILITIES,
86 )*
87 }
88 }
89
90 pub fn ignores_visibility(self) -> bool {
91 self.allowed_visibilities().is_empty()
92 }
93 }
94
95 #[cfg(test)]
96 mod _conformance_dispatch {
97 use $crate::lang::LangExtractor;
98
99 pub(crate) fn for_each_language(
102 mut f: impl FnMut(
103 &'static str,
104 &'static [&'static str],
105 &'static [&'static str],
106 &'static [$crate::lang::KindSpec],
107 ),
108 ) {
109 $(
110 $(#[$attr])*
111 f(
112 <$module as LangExtractor>::LANG_TAG,
113 <$module as LangExtractor>::ALLOWED_KINDS,
114 <$module as LangExtractor>::ALLOWED_VISIBILITIES,
115 <$module as LangExtractor>::KIND_SPECS,
116 );
117 )*
118 }
119 }
120 };
121}
122
123define_languages! {
124 Ts => crate::lang::ts::Lang,
125 Rs => crate::lang::rs::Lang,
126 Java => crate::lang::java::Lang,
127 Python => crate::lang::python::Lang,
128 Go => crate::lang::go::Lang,
129 C => crate::lang::c::Lang,
130 Cs => crate::lang::cs::Lang,
131 Sql => crate::lang::sql::Lang,
132}
133
134#[cfg(test)]
135pub(crate) use _conformance_dispatch::for_each_language;
136
137#[cfg(test)]
138mod language_registry_tests {
139 use super::for_each_language;
140
141 #[test]
142 fn language_registry_matches_dispatch_table() {
143 let mut visited = 0usize;
144 for_each_language(|tag, kinds, visibilities, _| {
145 visited += 1;
146 let lang = super::Lang::from_tag(tag).expect("dispatch tag must resolve through Lang");
147 assert_eq!(lang.tag(), tag);
148 assert_eq!(lang.allowed_kinds(), kinds);
149 assert_eq!(lang.allowed_visibilities(), visibilities);
150 });
151
152 assert_eq!(
153 visited,
154 super::Lang::ALL.len(),
155 "for_each_language visited {visited} languages but Lang::ALL contains {}; the cfg gates of the dispatch table and the macro variants are out of sync",
156 super::Lang::ALL.len()
157 );
158 }
159}
160
161#[cfg(test)]
162mod shape_coverage_tests {
163 use super::for_each_language;
164 use crate::core::shape::shape_of;
165
166 #[test]
167 fn every_allowed_kind_has_a_shape() {
168 let mut missing: Vec<(String, String)> = Vec::new();
169 for_each_language(|tag, kinds, _, _| {
170 for k in kinds {
171 if shape_of(k.as_bytes()).is_none() {
172 missing.push((tag.to_string(), (*k).to_string()));
173 }
174 }
175 });
176 assert!(
177 missing.is_empty(),
178 "kinds in ALLOWED_KINDS without an entry in core::shape::SHAPE_TABLE: {missing:?}"
179 );
180 }
181
182 #[test]
183 fn internal_kinds_have_a_shape() {
184 for k in [b"module".as_slice(), b"comment", b"local", b"param"] {
185 assert!(
186 shape_of(k).is_some(),
187 "internal kind {:?} must have a shape entry",
188 std::str::from_utf8(k).unwrap()
189 );
190 }
191 }
192}
193
194#[cfg(test)]
195mod kind_contract_tests {
196 use super::for_each_language;
197 use crate::core::shape::shape_of;
198
199 #[test]
200 fn every_language_kind_spec_matches_allowed_kinds() {
201 for_each_language(|tag, kinds, _, specs| {
202 let spec_ids: Vec<_> = specs.iter().map(|spec| spec.id).collect();
203 assert_eq!(
204 sort(&spec_ids),
205 sort(kinds),
206 "{tag} KIND_SPECS must describe exactly ALLOWED_KINDS"
207 );
208 });
209 }
210
211 #[test]
212 fn every_language_kind_spec_has_stable_semantics() {
213 for_each_language(|tag, _, _, specs| {
214 assert!(!specs.is_empty(), "{tag} must declare kind specs");
215 let mut seen_ids = std::collections::HashSet::new();
216 for spec in specs {
217 assert!(
218 seen_ids.insert(spec.id),
219 "{tag} duplicates kind spec `{}`",
220 spec.id
221 );
222 assert!(
223 !spec.label.is_empty(),
224 "{tag} kind `{}` has no label",
225 spec.id
226 );
227 assert_ne!(spec.order, 0, "{tag} kind `{}` has no order", spec.id);
228 assert_eq!(
229 shape_of(spec.id.as_bytes()),
230 Some(spec.shape),
231 "{tag} kind `{}` shape must stay aligned with core shape taxonomy",
232 spec.id
233 );
234 }
235 });
236 }
237
238 fn sort<'a>(xs: &[&'a str]) -> Vec<&'a str> {
239 let mut v: Vec<&str> = xs.to_vec();
240 v.sort_unstable();
241 v
242 }
243}
244
245#[cfg(test)]
246mod comment_collapse_tests {
247 use crate::core::moniker::MonikerBuilder;
248
249 struct Case {
250 tag: &'static str,
251 uri: &'static str,
252 run: fn(&'static str) -> crate::core::code_graph::CodeGraph,
253 }
254
255 fn anchor() -> crate::core::moniker::Moniker {
256 MonikerBuilder::new().project(b"app").build()
257 }
258
259 fn cases() -> Vec<Case> {
260 vec![
261 Case {
262 tag: "rs",
263 uri: "test.rs",
264 run: |src| {
265 super::rs::extract(
266 "test.rs",
267 src,
268 &anchor(),
269 false,
270 &super::rs::Presets::default(),
271 )
272 },
273 },
274 Case {
275 tag: "ts",
276 uri: "test.ts",
277 run: |src| {
278 super::ts::extract(
279 "test.ts",
280 src,
281 &anchor(),
282 false,
283 &super::ts::Presets::default(),
284 )
285 },
286 },
287 Case {
288 tag: "python",
289 uri: "test.py",
290 run: |src| {
291 super::python::extract(
292 "test.py",
293 src,
294 &anchor(),
295 false,
296 &super::python::Presets::default(),
297 )
298 },
299 },
300 Case {
301 tag: "go",
302 uri: "test.go",
303 run: |src| {
304 super::go::extract(
305 "test.go",
306 src,
307 &anchor(),
308 false,
309 &super::go::Presets::default(),
310 )
311 },
312 },
313 Case {
314 tag: "java",
315 uri: "test.java",
316 run: |src| {
317 super::java::extract(
318 "test.java",
319 src,
320 &anchor(),
321 false,
322 &super::java::Presets::default(),
323 )
324 },
325 },
326 Case {
327 tag: "c",
328 uri: "test.c",
329 run: |src| {
330 super::c::extract(
331 "test.c",
332 src,
333 &anchor(),
334 false,
335 &super::c::Presets::default(),
336 )
337 },
338 },
339 Case {
340 tag: "cs",
341 uri: "test.cs",
342 run: |src| {
343 super::cs::extract(
344 "test.cs",
345 src,
346 &anchor(),
347 false,
348 &super::cs::Presets::default(),
349 )
350 },
351 },
352 Case {
353 tag: "sql",
354 uri: "test.sql",
355 run: |src| {
356 super::sql::extract(
357 "test.sql",
358 src,
359 &anchor(),
360 false,
361 &super::sql::Presets::default(),
362 )
363 },
364 },
365 ]
366 }
367
368 const ADJACENT: &[(&str, &str)] = &[
369 ("rs", "// a\n// b\n// c\nstruct Foo;\n"),
370 ("ts", "// a\n// b\n// c\nclass Foo {}"),
371 ("python", "# a\n# b\n# c\nclass Foo: pass\n"),
372 ("go", "package x\n// a\n// b\n// c\nfunc Foo() {}\n"),
373 ("java", "// a\n// b\n// c\nclass Foo {}\n"),
374 ("c", "// a\n// b\n// c\nint foo(void) { return 0; }\n"),
375 ("cs", "// a\n// b\n// c\nclass Foo {}\n"),
376 (
377 "sql",
378 "-- a\n-- b\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
379 ),
380 ];
381
382 const SPLIT_BY_BLANK: &[(&str, &str)] = &[
383 ("rs", "// a\n// b\n\n// c\nstruct Foo;\n"),
384 ("ts", "// a\n// b\n\n// c\nclass Foo {}"),
385 ("python", "# a\n# b\n\n# c\nclass Foo: pass\n"),
386 ("go", "package x\n// a\n// b\n\n// c\nfunc Foo() {}\n"),
387 ("java", "// a\n// b\n\n// c\nclass Foo {}\n"),
388 ("c", "// a\n// b\n\n// c\nint foo(void) { return 0; }\n"),
389 ("cs", "// a\n// b\n\n// c\nclass Foo {}\n"),
390 (
391 "sql",
392 "-- a\n-- b\n\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
393 ),
394 ];
395
396 fn count_comments(g: &crate::core::code_graph::CodeGraph) -> usize {
397 g.defs().filter(|d| d.kind == b"comment").count()
398 }
399
400 #[test]
401 fn each_language_collapses_three_adjacent_line_comments_into_one_def() {
402 for case in cases() {
403 let src = ADJACENT
404 .iter()
405 .find(|(tag, _)| *tag == case.tag)
406 .expect("adjacent fixture")
407 .1;
408 let g = (case.run)(src);
409 assert_eq!(
410 count_comments(&g),
411 1,
412 "lang={} ({}): three adjacent line comments must collapse to one def",
413 case.tag,
414 case.uri
415 );
416 }
417 }
418
419 #[test]
420 fn each_language_splits_runs_on_blank_line() {
421 for case in cases() {
422 let src = SPLIT_BY_BLANK
423 .iter()
424 .find(|(tag, _)| *tag == case.tag)
425 .expect("blank-line fixture")
426 .1;
427 let g = (case.run)(src);
428 assert_eq!(
429 count_comments(&g),
430 2,
431 "lang={} ({}): blank line must break the run into two defs",
432 case.tag,
433 case.uri
434 );
435 }
436 }
437}