1use 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;