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
15use super::ParsedDocument;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
18pub struct KindSpec {
19	pub id: &'static str,
20	pub shape: Shape,
21	pub order: u16,
22	pub label: &'static str,
23}
24
25impl KindSpec {
26	pub const fn new(id: &'static str, shape: Shape, order: u16, label: &'static str) -> Self {
27		Self {
28			id,
29			shape,
30			order,
31			label,
32		}
33	}
34}
35
36pub struct ExtractionContext<'a, P> {
37	pub uri: &'a str,
38	pub source: &'a str,
39	pub anchor: &'a Moniker,
40	pub deep: bool,
41	pub presets: &'a P,
42}
43
44impl<'a, P> ExtractionContext<'a, P> {
45	pub fn new(
46		uri: &'a str,
47		source: &'a str,
48		anchor: &'a Moniker,
49		deep: bool,
50		presets: &'a P,
51	) -> Self {
52		Self {
53			uri,
54			source,
55			anchor,
56			deep,
57			presets,
58		}
59	}
60}
61
62pub trait LangExtractor {
63	type Presets: Default;
64
65	const LANG_TAG: &'static str;
66
67	const ALLOWED_KINDS: &'static [&'static str];
68
69	const KIND_SPECS: &'static [KindSpec];
70
71	const ALLOWED_VISIBILITIES: &'static [&'static str];
72
73	fn file_root(_uri: &str, _anchor: &Moniker) -> Option<Moniker> {
74		None
75	}
76
77	fn parse(uri: &str, source: &str) -> ParsedDocument;
78
79	fn extract_parsed(
80		context: ExtractionContext<'_, Self::Presets>,
81		document: &ParsedDocument,
82	) -> CodeGraph;
83
84	fn extract(
85		uri: &str,
86		source: &str,
87		anchor: &Moniker,
88		deep: bool,
89		presets: &Self::Presets,
90	) -> CodeGraph {
91		let document = Self::parse(uri, source);
92		let context = ExtractionContext::new(uri, source, anchor, deep, presets);
93		Self::extract_parsed(context, &document)
94	}
95}
96
97mod conformance {
98	use super::LangExtractor;
99	use crate::core::code_graph::{CodeGraph, assert_local_refs_closed};
100	use crate::core::kinds::{
101		BIND_IMPORT, BIND_INJECT, BIND_LOCAL, BIND_NONE, KIND_COMMENT, KIND_LOCAL, KIND_MODULE,
102		KIND_PARAM, ORIGIN_EXTRACTED, REF_ANNOTATES, REF_CALLS, REF_DI_REGISTER, REF_DI_REQUIRE,
103		REF_EXTENDS, REF_IMPLEMENTS, REF_IMPORTS_MODULE, REF_IMPORTS_SYMBOL, REF_INSTANTIATES,
104		REF_METHOD_CALL, REF_READS, REF_REEXPORTS, REF_REFERENCES, REF_RETURNS_TYPE, REF_USES_TYPE,
105		REF_WRITES, VIS_NONE,
106	};
107	use crate::core::moniker::Moniker;
108
109	const INTERNAL_KINDS: &[&[u8]] = &[KIND_MODULE, KIND_LOCAL, KIND_PARAM, KIND_COMMENT];
110
111	pub fn assert_conformance<E: LangExtractor>(graph: &CodeGraph, anchor: &Moniker) {
112		assert_root_under_anchor::<E>(graph, anchor);
113		for d in graph.defs() {
114			assert_kind_in_profile::<E>(d.moniker.as_encoded(), &d.kind);
115			assert_visibility_in_profile::<E>(d.moniker.as_encoded(), &d.visibility);
116			assert_kind_matches_moniker_last_segment(&d.moniker, &d.kind);
117			assert_origin_extracted(&d.moniker, &d.origin);
118		}
119		for r in graph.refs() {
120			assert_ref_binding_consistent(&r.kind, &r.binding);
121		}
122		assert_local_refs_closed(graph);
123	}
124
125	fn assert_root_under_anchor<E: LangExtractor>(graph: &CodeGraph, anchor: &Moniker) {
126		let root = graph.root();
127		let root_view = root.as_view();
128		assert!(
129			anchor.as_view().is_ancestor_of(&root_view) || root.as_encoded() == anchor.as_encoded(),
130			"contract violation: root {root:?} is not anchored under {anchor:?}"
131		);
132		let lang = root_view.lang_segment().unwrap_or_else(|| {
133			panic!(
134				"contract violation: root {:?} has no `lang:` segment (lang={:?} expected)",
135				root,
136				E::LANG_TAG
137			)
138		});
139		assert_eq!(
140			lang,
141			E::LANG_TAG.as_bytes(),
142			"contract violation: root carries lang:{} but extractor LANG_TAG={}",
143			String::from_utf8_lossy(lang),
144			E::LANG_TAG
145		);
146	}
147
148	fn assert_kind_in_profile<E: LangExtractor>(moniker_bytes: &[u8], kind: &[u8]) {
149		if INTERNAL_KINDS.contains(&kind) {
150			return;
151		}
152		let kind_str = std::str::from_utf8(kind).unwrap_or_else(|_| {
153			panic!("contract violation: def kind is not UTF-8 ({kind:?})");
154		});
155		assert!(
156			E::ALLOWED_KINDS.contains(&kind_str),
157			"contract violation: def kind `{}` is not in {} profile (moniker bytes: {:?})",
158			kind_str,
159			E::LANG_TAG,
160			moniker_bytes
161		);
162	}
163
164	fn assert_visibility_in_profile<E: LangExtractor>(moniker_bytes: &[u8], vis: &[u8]) {
165		if vis == VIS_NONE {
166			return;
167		}
168		let vis_str = std::str::from_utf8(vis).unwrap_or_else(|_| {
169			panic!("contract violation: def visibility is not UTF-8 ({vis:?})");
170		});
171		assert!(
172			E::ALLOWED_VISIBILITIES.contains(&vis_str),
173			"contract violation: def visibility `{}` is not in {} profile (moniker bytes: {:?})",
174			vis_str,
175			E::LANG_TAG,
176			moniker_bytes
177		);
178	}
179
180	fn assert_kind_matches_moniker_last_segment(moniker: &Moniker, kind: &[u8]) {
181		if INTERNAL_KINDS.contains(&kind) {
182			return;
183		}
184		let last_kind = moniker.last_kind().unwrap_or_else(|| {
185			panic!("contract violation: def has no segments (kind={kind:?})");
186		});
187		assert_eq!(
188			last_kind.as_slice(),
189			kind,
190			"contract violation: def.kind {kind:?} does not match moniker last segment kind {last_kind:?}"
191		);
192	}
193
194	fn assert_origin_extracted(moniker: &Moniker, origin: &[u8]) {
195		assert_eq!(
196			origin, ORIGIN_EXTRACTED,
197			"contract violation: extractor produced def with origin={origin:?} (must be `extracted`); moniker={moniker:?}"
198		);
199	}
200
201	fn assert_ref_binding_consistent(kind: &[u8], binding: &[u8]) {
202		let expected: &[u8] =
203			if kind == REF_IMPORTS_SYMBOL || kind == REF_IMPORTS_MODULE || kind == REF_REEXPORTS {
204				BIND_IMPORT
205			} else if kind == REF_DI_REGISTER || kind == REF_DI_REQUIRE {
206				BIND_INJECT
207			} else if kind == REF_CALLS
208				|| kind == REF_METHOD_CALL
209				|| kind == REF_READS
210				|| kind == REF_WRITES
211				|| kind == REF_REFERENCES
212				|| kind == REF_USES_TYPE
213				|| kind == REF_RETURNS_TYPE
214				|| kind == REF_INSTANTIATES
215				|| kind == REF_EXTENDS
216				|| kind == REF_IMPLEMENTS
217				|| kind == REF_ANNOTATES
218			{
219				BIND_LOCAL
220			} else {
221				BIND_NONE
222			};
223		assert_eq!(
224			binding,
225			expected,
226			"contract violation: ref kind={:?} got binding={:?} (expected {:?})",
227			std::str::from_utf8(kind).unwrap_or("<non-utf8>"),
228			std::str::from_utf8(binding).unwrap_or("<non-utf8>"),
229			std::str::from_utf8(expected).unwrap_or("<non-utf8>"),
230		);
231	}
232}
233
234#[doc(hidden)]
235pub use conformance::assert_conformance;