1pub mod build_manifest;
2pub mod c;
3pub mod callable;
4pub mod cs;
5mod document;
6pub mod extractor;
7pub mod go;
8pub mod java;
9pub mod kinds;
10pub mod python;
11pub mod rs;
12pub mod sdk;
13pub mod sql;
14pub mod tree_util;
15pub mod ts;
16
17pub use document::{ParsedDocument, SyntaxInjection};
18#[doc(hidden)]
19pub use extractor::assert_conformance;
20pub use extractor::{ExtractionContext, KindSpec, LangExtractor};
21
22macro_rules! define_languages {
25 ($($(#[$attr:meta])* $variant:ident => $module:ty),* $(,)?) => {
26 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
27 pub enum Lang {
28 $(
29 $(#[$attr])*
30 $variant,
31 )*
32 }
33 impl Lang {
34 pub const ALL: &'static [Lang] = &[
35 $(
36 $(#[$attr])*
37 Self::$variant,
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 pub fn file_root(
95 self,
96 uri: &str,
97 anchor: &$crate::core::moniker::Moniker,
98 ) -> Option<$crate::core::moniker::Moniker> {
99 match self {
100 $(
101 $(#[$attr])*
102 Self::$variant => <$module as $crate::lang::LangExtractor>::file_root(uri, anchor),
103 )*
104 }
105 }
106
107 pub fn parse(self, uri: &str, source: &str) -> $crate::lang::ParsedDocument {
108 match self {
109 $(
110 $(#[$attr])*
111 Self::$variant => <$module as $crate::lang::LangExtractor>::parse(uri, source),
112 )*
113 }
114 }
115 }
116
117 #[cfg(test)]
118 mod _conformance_dispatch {
119 use $crate::lang::LangExtractor;
120
121 pub(crate) fn for_each_language(
124 mut f: impl FnMut(
125 &'static str,
126 &'static [&'static str],
127 &'static [&'static str],
128 &'static [$crate::lang::KindSpec],
129 ),
130 ) {
131 $(
132 $(#[$attr])*
133 f(
134 <$module as LangExtractor>::LANG_TAG,
135 <$module as LangExtractor>::ALLOWED_KINDS,
136 <$module as LangExtractor>::ALLOWED_VISIBILITIES,
137 <$module as LangExtractor>::KIND_SPECS,
138 );
139 )*
140 }
141 }
142 };
143}
144
145define_languages! {
146 Ts => crate::lang::ts::Lang,
147 Rs => crate::lang::rs::Lang,
148 Java => crate::lang::java::Lang,
149 Python => crate::lang::python::Lang,
150 Go => crate::lang::go::Lang,
151 C => crate::lang::c::Lang,
152 Cs => crate::lang::cs::Lang,
153 Sql => crate::lang::sql::Lang,
154}
155
156pub fn parse_source(language: &str, uri: &str, source: &str) -> Option<ParsedDocument> {
161 if language.eq_ignore_ascii_case("plpgsql") {
162 return Some(sql::parse_plpgsql(source));
163 }
164 Lang::from_tag(language).map(|lang| lang.parse(uri, source))
165}
166
167#[cfg(test)]
168pub(crate) use _conformance_dispatch::for_each_language;
169
170#[cfg(test)]
171mod language_registry_tests {
172 use super::for_each_language;
173
174 #[test]
175 fn language_registry_matches_dispatch_table() {
176 let mut visited = 0usize;
177 for_each_language(|tag, kinds, visibilities, _| {
178 visited += 1;
179 let lang = super::Lang::from_tag(tag).expect("dispatch tag must resolve through Lang");
180 assert_eq!(lang.tag(), tag);
181 assert_eq!(lang.allowed_kinds(), kinds);
182 assert_eq!(lang.allowed_visibilities(), visibilities);
183 });
184
185 assert_eq!(
186 visited,
187 super::Lang::ALL.len(),
188 "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",
189 super::Lang::ALL.len()
190 );
191 }
192
193 #[test]
194 fn every_registered_language_parses_on_demand() {
195 for lang in super::Lang::ALL {
196 let document = lang.parse("empty", "");
197 assert!(
198 !document.primary().root_node().kind().is_empty(),
199 "{} returned an empty root kind",
200 lang.tag()
201 );
202 }
203 }
204
205 #[test]
206 fn stateless_parser_accepts_standalone_plpgsql() {
207 let document = super::parse_source(
208 "plpgsql",
209 "snippet.plpgsql",
210 "DECLARE total numeric; BEGIN total := 1; RETURN total; END;",
211 )
212 .expect("PL/pgSQL parser");
213 let root = document.primary().root_node();
214 assert_eq!(root.kind(), "source_file");
215 assert!(!root.has_error());
216 }
217}
218
219#[cfg(test)]
220mod shape_coverage_tests {
221 use super::for_each_language;
222 use crate::core::shape::shape_of;
223
224 #[test]
225 fn every_allowed_kind_has_a_shape() {
226 let mut missing: Vec<(String, String)> = Vec::new();
227 for_each_language(|tag, kinds, _, _| {
228 for k in kinds {
229 if shape_of(k.as_bytes()).is_none() {
230 missing.push((tag.to_string(), (*k).to_string()));
231 }
232 }
233 });
234 assert!(
235 missing.is_empty(),
236 "kinds in ALLOWED_KINDS without an entry in core::shape::SHAPE_TABLE: {missing:?}"
237 );
238 }
239
240 #[test]
241 fn internal_kinds_have_a_shape() {
242 for k in [b"module".as_slice(), b"comment", b"local", b"param"] {
243 assert!(
244 shape_of(k).is_some(),
245 "internal kind {:?} must have a shape entry",
246 std::str::from_utf8(k).unwrap()
247 );
248 }
249 }
250}
251
252#[cfg(test)]
253mod kind_contract_tests {
254 use super::for_each_language;
255 use crate::core::shape::shape_of;
256
257 #[test]
258 fn every_language_kind_spec_matches_allowed_kinds() {
259 for_each_language(|tag, kinds, _, specs| {
260 let spec_ids: Vec<_> = specs.iter().map(|spec| spec.id).collect();
261 assert_eq!(
262 sort(&spec_ids),
263 sort(kinds),
264 "{tag} KIND_SPECS must describe exactly ALLOWED_KINDS"
265 );
266 });
267 }
268
269 #[test]
270 fn every_language_kind_spec_has_stable_semantics() {
271 for_each_language(|tag, _, _, specs| {
272 assert!(!specs.is_empty(), "{tag} must declare kind specs");
273 let mut seen_ids = std::collections::HashSet::new();
274 for spec in specs {
275 assert!(
276 seen_ids.insert(spec.id),
277 "{tag} duplicates kind spec `{}`",
278 spec.id
279 );
280 assert!(
281 !spec.label.is_empty(),
282 "{tag} kind `{}` has no label",
283 spec.id
284 );
285 assert_ne!(spec.order, 0, "{tag} kind `{}` has no order", spec.id);
286 assert_eq!(
287 shape_of(spec.id.as_bytes()),
288 Some(spec.shape),
289 "{tag} kind `{}` shape must stay aligned with core shape taxonomy",
290 spec.id
291 );
292 }
293 });
294 }
295
296 fn sort<'a>(xs: &[&'a str]) -> Vec<&'a str> {
297 let mut v: Vec<&str> = xs.to_vec();
298 v.sort_unstable();
299 v
300 }
301}
302
303#[cfg(test)]
304mod comment_collapse_tests {
305 use crate::core::moniker::MonikerBuilder;
306
307 struct Case {
308 tag: &'static str,
309 uri: &'static str,
310 run: fn(&'static str) -> crate::core::code_graph::CodeGraph,
311 }
312
313 fn anchor() -> crate::core::moniker::Moniker {
314 MonikerBuilder::new().project(b"app").build()
315 }
316
317 fn cases() -> Vec<Case> {
318 vec![
319 Case {
320 tag: "rs",
321 uri: "test.rs",
322 run: |src| {
323 super::rs::extract(
324 "test.rs",
325 src,
326 &anchor(),
327 false,
328 &super::rs::Presets::default(),
329 )
330 },
331 },
332 Case {
333 tag: "ts",
334 uri: "test.ts",
335 run: |src| {
336 super::ts::extract(
337 "test.ts",
338 src,
339 &anchor(),
340 false,
341 &super::ts::Presets::default(),
342 )
343 },
344 },
345 Case {
346 tag: "python",
347 uri: "test.py",
348 run: |src| {
349 super::python::extract(
350 "test.py",
351 src,
352 &anchor(),
353 false,
354 &super::python::Presets::default(),
355 )
356 },
357 },
358 Case {
359 tag: "go",
360 uri: "test.go",
361 run: |src| {
362 super::go::extract(
363 "test.go",
364 src,
365 &anchor(),
366 false,
367 &super::go::Presets::default(),
368 )
369 },
370 },
371 Case {
372 tag: "java",
373 uri: "test.java",
374 run: |src| {
375 super::java::extract(
376 "test.java",
377 src,
378 &anchor(),
379 false,
380 &super::java::Presets::default(),
381 )
382 },
383 },
384 Case {
385 tag: "c",
386 uri: "test.c",
387 run: |src| {
388 super::c::extract(
389 "test.c",
390 src,
391 &anchor(),
392 false,
393 &super::c::Presets::default(),
394 )
395 },
396 },
397 Case {
398 tag: "cs",
399 uri: "test.cs",
400 run: |src| {
401 super::cs::extract(
402 "test.cs",
403 src,
404 &anchor(),
405 false,
406 &super::cs::Presets::default(),
407 )
408 },
409 },
410 Case {
411 tag: "sql",
412 uri: "test.sql",
413 run: |src| {
414 super::sql::extract(
415 "test.sql",
416 src,
417 &anchor(),
418 false,
419 &super::sql::Presets::default(),
420 )
421 },
422 },
423 ]
424 }
425
426 const ADJACENT: &[(&str, &str)] = &[
427 ("rs", "// a\n// b\n// c\nstruct Foo;\n"),
428 ("ts", "// a\n// b\n// c\nclass Foo {}"),
429 ("python", "# a\n# b\n# c\nclass Foo: pass\n"),
430 ("go", "package x\n// a\n// b\n// c\nfunc Foo() {}\n"),
431 ("java", "// a\n// b\n// c\nclass Foo {}\n"),
432 ("c", "// a\n// b\n// c\nint foo(void) { return 0; }\n"),
433 ("cs", "// a\n// b\n// c\nclass Foo {}\n"),
434 (
435 "sql",
436 "-- a\n-- b\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
437 ),
438 ];
439
440 const SPLIT_BY_BLANK: &[(&str, &str)] = &[
441 ("rs", "// a\n// b\n\n// c\nstruct Foo;\n"),
442 ("ts", "// a\n// b\n\n// c\nclass Foo {}"),
443 ("python", "# a\n# b\n\n# c\nclass Foo: pass\n"),
444 ("go", "package x\n// a\n// b\n\n// c\nfunc Foo() {}\n"),
445 ("java", "// a\n// b\n\n// c\nclass Foo {}\n"),
446 ("c", "// a\n// b\n\n// c\nint foo(void) { return 0; }\n"),
447 ("cs", "// a\n// b\n\n// c\nclass Foo {}\n"),
448 (
449 "sql",
450 "-- a\n-- b\n\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
451 ),
452 ];
453
454 fn count_comments(g: &crate::core::code_graph::CodeGraph) -> usize {
455 g.defs().filter(|d| d.kind == b"comment").count()
456 }
457
458 #[test]
459 fn each_language_collapses_three_adjacent_line_comments_into_one_def() {
460 for case in cases() {
461 let src = ADJACENT
462 .iter()
463 .find(|(tag, _)| *tag == case.tag)
464 .expect("adjacent fixture")
465 .1;
466 let g = (case.run)(src);
467 assert_eq!(
468 count_comments(&g),
469 1,
470 "lang={} ({}): three adjacent line comments must collapse to one def",
471 case.tag,
472 case.uri
473 );
474 }
475 }
476
477 #[test]
478 fn each_language_splits_runs_on_blank_line() {
479 for case in cases() {
480 let src = SPLIT_BY_BLANK
481 .iter()
482 .find(|(tag, _)| *tag == case.tag)
483 .expect("blank-line fixture")
484 .1;
485 let g = (case.run)(src);
486 assert_eq!(
487 count_comments(&g),
488 2,
489 "lang={} ({}): blank line must break the run into two defs",
490 case.tag,
491 case.uri
492 );
493 }
494 }
495}