Skip to main content

code_moniker_core/lang/
mod.rs

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
20/// Adding a row registers the language for `Lang::from_tag` / `tag` /
21/// `allowed_kinds` / `allowed_visibilities` and the conformance tests.
22macro_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			/// Dispatches a closure that takes `(lang_tag, allowed_kinds, allowed_visibilities)`
100			/// over every registered language. Used by language conformance tests.
101			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}