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
21macro_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 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}