Skip to main content

code_moniker_core/lang/
mod.rs

1pub mod callable;
2pub mod canonical_walker;
3pub mod cs;
4pub mod extractor;
5pub mod go;
6pub mod java;
7pub mod kinds;
8pub mod python;
9pub mod rs;
10pub mod sql;
11pub mod strategy;
12pub mod tree_util;
13pub mod ts;
14
15pub use extractor::LangExtractor;
16#[cfg(test)]
17pub use extractor::assert_conformance;
18
19/// Single dispatch table for every supported language.
20///
21/// Adding a language is a one-line change here. Each row produces:
22/// - a variant on `Lang`
23/// - participation in `Lang::from_tag` / `Lang::tag` / `Lang::allowed_kinds`
24///   / `Lang::allowed_visibilities` (all consult the trait — no per-language
25///   constant ever lives outside its `LangExtractor` impl)
26/// - dispatch in the conformance test that scans `docs/declare_schema.json`
27///
28/// Forgetting to update one of those callsites is impossible: if a row is
29/// missing, the build fails. If the row is present, every dispatch sees it.
30macro_rules! define_languages {
31	($($(#[$attr:meta])* $variant:ident => $module:ty),* $(,)?) => {
32		#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
33		pub enum Lang {
34			$(
35				$(#[$attr])*
36				$variant,
37			)*
38		}
39
40		impl Lang {
41			pub const ALL: &'static [Lang] = &[
42				$(
43					$(#[$attr])*
44					Self::$variant,
45				)*
46			];
47
48			pub fn from_tag(s: &str) -> Option<Self> {
49				$(
50					$(#[$attr])*
51					if s == <$module as $crate::lang::LangExtractor>::LANG_TAG {
52						return Some(Self::$variant);
53					}
54				)*
55				None
56			}
57
58			pub fn tag(self) -> &'static str {
59				match self {
60					$(
61						$(#[$attr])*
62						Self::$variant => <$module as $crate::lang::LangExtractor>::LANG_TAG,
63					)*
64				}
65			}
66
67			pub fn allowed_kinds(self) -> &'static [&'static str] {
68				match self {
69					$(
70						$(#[$attr])*
71						Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_KINDS,
72					)*
73				}
74			}
75
76			pub fn allowed_visibilities(self) -> &'static [&'static str] {
77				match self {
78					$(
79						$(#[$attr])*
80						Self::$variant => <$module as $crate::lang::LangExtractor>::ALLOWED_VISIBILITIES,
81					)*
82				}
83			}
84
85			pub fn ignores_visibility(self) -> bool {
86				self.allowed_visibilities().is_empty()
87			}
88		}
89
90		#[cfg(test)]
91		mod _conformance_dispatch {
92			use $crate::lang::LangExtractor;
93
94			/// Dispatches a closure that takes `(lang_tag, allowed_kinds, allowed_visibilities)`
95			/// over every registered language. Used by the JSON Schema sync test.
96			pub(crate) fn for_each_language(
97				mut f: impl FnMut(&'static str, &'static [&'static str], &'static [&'static str]),
98			) {
99				$(
100					$(#[$attr])*
101					f(
102						<$module as LangExtractor>::LANG_TAG,
103						<$module as LangExtractor>::ALLOWED_KINDS,
104						<$module as LangExtractor>::ALLOWED_VISIBILITIES,
105					);
106				)*
107			}
108		}
109	};
110}
111
112define_languages! {
113	Ts     => crate::lang::ts::Lang,
114	Rs     => crate::lang::rs::Lang,
115	Java   => crate::lang::java::Lang,
116	Python => crate::lang::python::Lang,
117	Go     => crate::lang::go::Lang,
118	Cs     => crate::lang::cs::Lang,
119	Sql    => crate::lang::sql::Lang,
120}
121
122#[cfg(test)]
123pub(crate) use _conformance_dispatch::for_each_language;
124
125#[cfg(test)]
126mod schema_sync_tests {
127	use super::for_each_language;
128	use serde_json::Value;
129
130	const SCHEMA_JSON: &str = include_str!("../../../../docs/declare_schema.json");
131
132	fn profile_name_for(tag: &str) -> String {
133		let mut chars = tag.chars();
134		let first = chars.next().unwrap().to_uppercase().collect::<String>();
135		format!("{first}{}Profile", chars.as_str())
136	}
137
138	fn enum_at<'a>(schema: &'a Value, profile: &str, field: &str) -> Vec<&'a str> {
139		schema
140			.get("$defs")
141			.and_then(|d| d.get(profile))
142			.and_then(|p| p.get("properties"))
143			.and_then(|p| p.get("symbols"))
144			.and_then(|s| s.get("items"))
145			.and_then(|i| i.get("properties"))
146			.and_then(|p| p.get(field))
147			.and_then(|f| f.get("enum"))
148			.and_then(|e| e.as_array())
149			.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
150			.unwrap_or_default()
151	}
152
153	#[test]
154	fn declare_schema_matches_trait_constants() {
155		let schema: Value =
156			serde_json::from_str(SCHEMA_JSON).expect("docs/declare_schema.json must be valid JSON");
157
158		let mut visited = 0usize;
159		for_each_language(|tag, kinds, visibilities| {
160			visited += 1;
161			let profile = profile_name_for(tag);
162
163			let schema_kinds = enum_at(&schema, &profile, "kind");
164			let trait_kinds: Vec<&str> = kinds.to_vec();
165			assert_eq!(
166				sort(&schema_kinds),
167				sort(&trait_kinds),
168				"declare_schema.json {profile}.kind enum drifted from `{tag}` trait ALLOWED_KINDS"
169			);
170
171			if visibilities.is_empty() {
172				let schema_vis = enum_at(&schema, &profile, "visibility");
173				assert!(
174					schema_vis.is_empty(),
175					"declare_schema.json {profile} declares visibilities but extractor profile is empty"
176				);
177			} else {
178				let schema_vis = enum_at(&schema, &profile, "visibility");
179				let trait_vis: Vec<&str> = visibilities.to_vec();
180				assert_eq!(
181					sort(&schema_vis),
182					sort(&trait_vis),
183					"declare_schema.json {profile}.visibility enum drifted from `{tag}` trait ALLOWED_VISIBILITIES"
184				);
185			}
186		});
187
188		assert_eq!(
189			visited,
190			super::Lang::ALL.len(),
191			"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",
192			super::Lang::ALL.len()
193		);
194	}
195
196	fn sort<'a>(xs: &[&'a str]) -> Vec<&'a str> {
197		let mut v: Vec<&str> = xs.to_vec();
198		v.sort_unstable();
199		v
200	}
201}
202
203#[cfg(test)]
204mod shape_coverage_tests {
205	use super::for_each_language;
206	use crate::core::shape::shape_of;
207
208	#[test]
209	fn every_allowed_kind_has_a_shape() {
210		let mut missing: Vec<(String, String)> = Vec::new();
211		for_each_language(|tag, kinds, _| {
212			for k in kinds {
213				if shape_of(k.as_bytes()).is_none() {
214					missing.push((tag.to_string(), (*k).to_string()));
215				}
216			}
217		});
218		assert!(
219			missing.is_empty(),
220			"kinds in ALLOWED_KINDS without an entry in core::shape::SHAPE_TABLE: {missing:?}"
221		);
222	}
223
224	#[test]
225	fn internal_kinds_have_a_shape() {
226		for k in [b"module".as_slice(), b"comment", b"local", b"param"] {
227			assert!(
228				shape_of(k).is_some(),
229				"internal kind {:?} must have a shape entry",
230				std::str::from_utf8(k).unwrap()
231			);
232		}
233	}
234}