Skip to main content

code_moniker_core/lang/
mod.rs

1pub mod build_manifest;
2pub mod callable;
3pub mod canonical_walker;
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 strategy;
14pub mod tree_util;
15pub mod ts;
16
17#[doc(hidden)]
18pub use extractor::assert_conformance;
19pub use extractor::{KindSpec, LangExtractor};
20
21/// Adding a row registers the language for `Lang::from_tag` / `tag` /
22/// `allowed_kinds` / `allowed_visibilities` and the conformance tests.
23macro_rules! define_languages {
24	($($(#[$attr:meta])* $variant:ident => $module:ty),* $(,)?) => {
25		#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
26		pub enum Lang {
27			$(
28				$(#[$attr])*
29				$variant,
30			)*
31		}
32
33		impl Lang {
34			pub const ALL: &'static [Lang] = &[
35				$(
36					$(#[$attr])*
37					Self::$variant,
38				)*
39			];
40
41			pub fn from_tag(s: &str) -> Option<Self> {
42				$(
43					$(#[$attr])*
44					if s == <$module as $crate::lang::LangExtractor>::LANG_TAG {
45						return Some(Self::$variant);
46					}
47				)*
48				None
49			}
50
51			pub fn tag(self) -> &'static str {
52				match self {
53					$(
54						$(#[$attr])*
55						Self::$variant => <$module as $crate::lang::LangExtractor>::LANG_TAG,
56					)*
57				}
58			}
59
60			pub fn allowed_kinds(self) -> &'static [&'static str] {
61				match self {
62					$(
63						$(#[$attr])*
64						Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_KINDS,
65					)*
66				}
67			}
68
69			pub fn kind_specs(self) -> &'static [$crate::lang::KindSpec] {
70				match self {
71					$(
72						$(#[$attr])*
73						Self::$variant => <$module as $crate::lang::LangExtractor>::KIND_SPECS,
74					)*
75				}
76			}
77
78			pub fn kind_spec(self, id: &str) -> Option<&'static $crate::lang::KindSpec> {
79				self.kind_specs().iter().find(|spec| spec.id == id)
80			}
81
82			pub fn allowed_visibilities(self) -> &'static [&'static str] {
83				match self {
84					$(
85						$(#[$attr])*
86						Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_VISIBILITIES,
87					)*
88				}
89			}
90
91			pub fn ignores_visibility(self) -> bool {
92				self.allowed_visibilities().is_empty()
93			}
94		}
95
96		#[cfg(test)]
97		mod _conformance_dispatch {
98			use $crate::lang::LangExtractor;
99
100			/// Dispatches a closure that takes `(lang_tag, allowed_kinds, allowed_visibilities)`
101			/// over every registered language. Used by language conformance tests.
102			pub(crate) fn for_each_language(
103				mut f: impl FnMut(
104					&'static str,
105					&'static [&'static str],
106					&'static [&'static str],
107					&'static [$crate::lang::KindSpec],
108				),
109			) {
110				$(
111					$(#[$attr])*
112					f(
113						<$module as LangExtractor>::LANG_TAG,
114						<$module as LangExtractor>::ALLOWED_KINDS,
115						<$module as LangExtractor>::ALLOWED_VISIBILITIES,
116						<$module as LangExtractor>::KIND_SPECS,
117					);
118				)*
119			}
120		}
121	};
122}
123
124define_languages! {
125	Ts     => crate::lang::ts::Lang,
126	Rs     => crate::lang::rs::Lang,
127	Java   => crate::lang::java::Lang,
128	Python => crate::lang::python::Lang,
129	Go     => crate::lang::go::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: "cs",
328				uri: "test.cs",
329				run: |src| {
330					super::cs::extract(
331						"test.cs",
332						src,
333						&anchor(),
334						false,
335						&super::cs::Presets::default(),
336					)
337				},
338			},
339			Case {
340				tag: "sql",
341				uri: "test.sql",
342				run: |src| {
343					super::sql::extract(
344						"test.sql",
345						src,
346						&anchor(),
347						false,
348						&super::sql::Presets::default(),
349					)
350				},
351			},
352		]
353	}
354
355	const ADJACENT: &[(&str, &str)] = &[
356		("rs", "// a\n// b\n// c\nstruct Foo;\n"),
357		("ts", "// a\n// b\n// c\nclass Foo {}"),
358		("python", "# a\n# b\n# c\nclass Foo: pass\n"),
359		("go", "package x\n// a\n// b\n// c\nfunc Foo() {}\n"),
360		("java", "// a\n// b\n// c\nclass Foo {}\n"),
361		("cs", "// a\n// b\n// c\nclass Foo {}\n"),
362		(
363			"sql",
364			"-- a\n-- b\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
365		),
366	];
367
368	const SPLIT_BY_BLANK: &[(&str, &str)] = &[
369		("rs", "// a\n// b\n\n// c\nstruct Foo;\n"),
370		("ts", "// a\n// b\n\n// c\nclass Foo {}"),
371		("python", "# a\n# b\n\n# c\nclass Foo: pass\n"),
372		("go", "package x\n// a\n// b\n\n// c\nfunc Foo() {}\n"),
373		("java", "// a\n// b\n\n// c\nclass Foo {}\n"),
374		("cs", "// a\n// b\n\n// c\nclass Foo {}\n"),
375		(
376			"sql",
377			"-- a\n-- b\n\n-- c\nCREATE FUNCTION f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$;\n",
378		),
379	];
380
381	fn count_comments(g: &crate::core::code_graph::CodeGraph) -> usize {
382		g.defs().filter(|d| d.kind == b"comment").count()
383	}
384
385	#[test]
386	fn each_language_collapses_three_adjacent_line_comments_into_one_def() {
387		for case in cases() {
388			let src = ADJACENT
389				.iter()
390				.find(|(tag, _)| *tag == case.tag)
391				.expect("adjacent fixture")
392				.1;
393			let g = (case.run)(src);
394			assert_eq!(
395				count_comments(&g),
396				1,
397				"lang={} ({}): three adjacent line comments must collapse to one def",
398				case.tag,
399				case.uri
400			);
401		}
402	}
403
404	#[test]
405	fn each_language_splits_runs_on_blank_line() {
406		for case in cases() {
407			let src = SPLIT_BY_BLANK
408				.iter()
409				.find(|(tag, _)| *tag == case.tag)
410				.expect("blank-line fixture")
411				.1;
412			let g = (case.run)(src);
413			assert_eq!(
414				count_comments(&g),
415				2,
416				"lang={} ({}): blank line must break the run into two defs",
417				case.tag,
418				case.uri
419			);
420		}
421	}
422}