Skip to main content

code_moniker_core/lang/
mod.rs

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
22/// Adding a row registers the language for `Lang::from_tag` / `tag` /
23/// `allowed_kinds` / `allowed_visibilities` and the conformance tests.
24macro_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			/// Dispatches a closure that takes `(lang_tag, allowed_kinds, allowed_visibilities)`
122			/// over every registered language. Used by language conformance tests.
123			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
156/// Parse source text without indexing it.
157///
158/// Canonical language tags are the tags returned by [`Lang::tag`]. PL/pgSQL
159/// is also accepted as a standalone embedded language.
160pub 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}