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