Skip to main content

code_moniker_core/lang/
extractor.rs

1//! Per-language extractor contract.
2//!
3//! Every supported language exposes a zero-sized `Lang` type implementing
4//! `LangExtractor`. The trait carries no dispatch overhead — it is the
5//! formal contract every extractor must satisfy.
6//!
7//! `assert_conformance::<Lang>(graph, anchor)` validates that a graph
8//! produced by an extractor respects the contract. Each extractor's
9//! `#[cfg(test)] extract_default` helper invokes it on every fixture.
10//!
11use crate::core::code_graph::CodeGraph;
12use crate::core::moniker::Moniker;
13use crate::core::shape::Shape;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
16pub struct KindSpec {
17	pub id: &'static str,
18	pub shape: Shape,
19	pub order: u16,
20	pub label: &'static str,
21}
22
23impl KindSpec {
24	pub const fn new(id: &'static str, shape: Shape, order: u16, label: &'static str) -> Self {
25		Self {
26			id,
27			shape,
28			order,
29			label,
30		}
31	}
32}
33
34pub trait LangExtractor {
35	type Presets: Default;
36
37	const LANG_TAG: &'static str;
38
39	const ALLOWED_KINDS: &'static [&'static str];
40
41	const KIND_SPECS: &'static [KindSpec];
42
43	const ALLOWED_VISIBILITIES: &'static [&'static str];
44
45	fn extract(
46		uri: &str,
47		source: &str,
48		anchor: &Moniker,
49		deep: bool,
50		presets: &Self::Presets,
51	) -> CodeGraph;
52}
53
54mod conformance {
55	use super::LangExtractor;
56	use crate::core::code_graph::{CodeGraph, assert_local_refs_closed};
57	use crate::core::kinds::{
58		BIND_IMPORT, BIND_INJECT, BIND_LOCAL, BIND_NONE, KIND_COMMENT, KIND_LOCAL, KIND_MODULE,
59		KIND_PARAM, ORIGIN_EXTRACTED, REF_ANNOTATES, REF_CALLS, REF_DI_REGISTER, REF_DI_REQUIRE,
60		REF_EXTENDS, REF_IMPLEMENTS, REF_IMPORTS_MODULE, REF_IMPORTS_SYMBOL, REF_INSTANTIATES,
61		REF_METHOD_CALL, REF_READS, REF_REEXPORTS, REF_RETURNS_TYPE, REF_USES_TYPE, VIS_NONE,
62	};
63	use crate::core::moniker::Moniker;
64
65	const INTERNAL_KINDS: &[&[u8]] = &[KIND_MODULE, KIND_LOCAL, KIND_PARAM, KIND_COMMENT];
66
67	pub fn assert_conformance<E: LangExtractor>(graph: &CodeGraph, anchor: &Moniker) {
68		assert_root_under_anchor::<E>(graph, anchor);
69		for d in graph.defs() {
70			assert_kind_in_profile::<E>(d.moniker.as_encoded(), &d.kind);
71			assert_visibility_in_profile::<E>(d.moniker.as_encoded(), &d.visibility);
72			assert_kind_matches_moniker_last_segment(&d.moniker, &d.kind);
73			assert_origin_extracted(&d.moniker, &d.origin);
74		}
75		for r in graph.refs() {
76			assert_ref_binding_consistent(&r.kind, &r.binding);
77		}
78		assert_local_refs_closed(graph);
79	}
80
81	fn assert_root_under_anchor<E: LangExtractor>(graph: &CodeGraph, anchor: &Moniker) {
82		let root = graph.root();
83		let root_view = root.as_view();
84		assert!(
85			anchor.as_view().is_ancestor_of(&root_view) || root.as_encoded() == anchor.as_encoded(),
86			"contract violation: root {root:?} is not anchored under {anchor:?}"
87		);
88		let lang = root_view.lang_segment().unwrap_or_else(|| {
89			panic!(
90				"contract violation: root {:?} has no `lang:` segment (lang={:?} expected)",
91				root,
92				E::LANG_TAG
93			)
94		});
95		assert_eq!(
96			lang,
97			E::LANG_TAG.as_bytes(),
98			"contract violation: root carries lang:{} but extractor LANG_TAG={}",
99			String::from_utf8_lossy(lang),
100			E::LANG_TAG
101		);
102	}
103
104	fn assert_kind_in_profile<E: LangExtractor>(moniker_bytes: &[u8], kind: &[u8]) {
105		if INTERNAL_KINDS.contains(&kind) {
106			return;
107		}
108		let kind_str = std::str::from_utf8(kind).unwrap_or_else(|_| {
109			panic!("contract violation: def kind is not UTF-8 ({kind:?})");
110		});
111		assert!(
112			E::ALLOWED_KINDS.contains(&kind_str),
113			"contract violation: def kind `{}` is not in {} profile (moniker bytes: {:?})",
114			kind_str,
115			E::LANG_TAG,
116			moniker_bytes
117		);
118	}
119
120	fn assert_visibility_in_profile<E: LangExtractor>(moniker_bytes: &[u8], vis: &[u8]) {
121		if vis == VIS_NONE {
122			return;
123		}
124		let vis_str = std::str::from_utf8(vis).unwrap_or_else(|_| {
125			panic!("contract violation: def visibility is not UTF-8 ({vis:?})");
126		});
127		assert!(
128			E::ALLOWED_VISIBILITIES.contains(&vis_str),
129			"contract violation: def visibility `{}` is not in {} profile (moniker bytes: {:?})",
130			vis_str,
131			E::LANG_TAG,
132			moniker_bytes
133		);
134	}
135
136	fn assert_kind_matches_moniker_last_segment(moniker: &Moniker, kind: &[u8]) {
137		if INTERNAL_KINDS.contains(&kind) {
138			return;
139		}
140		let last_kind = moniker.last_kind().unwrap_or_else(|| {
141			panic!("contract violation: def has no segments (kind={kind:?})");
142		});
143		assert_eq!(
144			last_kind.as_slice(),
145			kind,
146			"contract violation: def.kind {kind:?} does not match moniker last segment kind {last_kind:?}"
147		);
148	}
149
150	fn assert_origin_extracted(moniker: &Moniker, origin: &[u8]) {
151		assert_eq!(
152			origin, ORIGIN_EXTRACTED,
153			"contract violation: extractor produced def with origin={origin:?} (must be `extracted`); moniker={moniker:?}"
154		);
155	}
156
157	fn assert_ref_binding_consistent(kind: &[u8], binding: &[u8]) {
158		let expected: &[u8] =
159			if kind == REF_IMPORTS_SYMBOL || kind == REF_IMPORTS_MODULE || kind == REF_REEXPORTS {
160				BIND_IMPORT
161			} else if kind == REF_DI_REGISTER || kind == REF_DI_REQUIRE {
162				BIND_INJECT
163			} else if kind == REF_CALLS
164				|| kind == REF_METHOD_CALL
165				|| kind == REF_READS
166				|| kind == REF_USES_TYPE
167				|| kind == REF_RETURNS_TYPE
168				|| kind == REF_INSTANTIATES
169				|| kind == REF_EXTENDS
170				|| kind == REF_IMPLEMENTS
171				|| kind == REF_ANNOTATES
172			{
173				BIND_LOCAL
174			} else {
175				BIND_NONE
176			};
177		assert_eq!(
178			binding,
179			expected,
180			"contract violation: ref kind={:?} got binding={:?} (expected {:?})",
181			std::str::from_utf8(kind).unwrap_or("<non-utf8>"),
182			std::str::from_utf8(binding).unwrap_or("<non-utf8>"),
183			std::str::from_utf8(expected).unwrap_or("<non-utf8>"),
184		);
185	}
186}
187
188#[doc(hidden)]
189pub use conformance::assert_conformance;