1#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
2pub enum Shape {
3 Namespace,
4 Type,
5 Callable,
6 Value,
7 Annotation,
8 Ref,
9}
10
11impl Shape {
12 pub const ALL: &'static [Shape] = &[
13 Shape::Namespace,
14 Shape::Type,
15 Shape::Callable,
16 Shape::Value,
17 Shape::Annotation,
18 Shape::Ref,
19 ];
20
21 pub fn as_bytes(self) -> &'static [u8] {
22 match self {
23 Shape::Namespace => b"namespace",
24 Shape::Type => b"type",
25 Shape::Callable => b"callable",
26 Shape::Value => b"value",
27 Shape::Annotation => b"annotation",
28 Shape::Ref => b"ref",
29 }
30 }
31
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Shape::Namespace => "namespace",
35 Shape::Type => "type",
36 Shape::Callable => "callable",
37 Shape::Value => "value",
38 Shape::Annotation => "annotation",
39 Shape::Ref => "ref",
40 }
41 }
42
43 pub fn for_kind(kind: &[u8]) -> Shape {
44 shape_of(kind).unwrap_or(Shape::Ref)
45 }
46}
47
48impl std::str::FromStr for Shape {
49 type Err = String;
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 Self::ALL
52 .iter()
53 .copied()
54 .find(|sh| sh.as_str() == s)
55 .ok_or_else(|| format!("unknown shape `{s}`"))
56 }
57}
58
59const SHAPE_TABLE: &[(&[u8], Shape, bool)] = &[
60 (b"module", Shape::Namespace, true),
61 (b"namespace", Shape::Namespace, true),
62 (b"schema", Shape::Namespace, true),
63 (b"impl", Shape::Namespace, true),
64 (b"class", Shape::Type, true),
65 (b"struct", Shape::Type, true),
66 (b"interface", Shape::Type, true),
67 (b"trait", Shape::Type, true),
68 (b"enum", Shape::Type, true),
69 (b"record", Shape::Type, true),
70 (b"annotation_type", Shape::Type, true),
71 (b"table", Shape::Type, true),
72 (b"type", Shape::Type, false),
73 (b"view", Shape::Type, false),
74 (b"delegate", Shape::Type, false),
75 (b"function", Shape::Callable, true),
76 (b"method", Shape::Callable, true),
77 (b"constructor", Shape::Callable, true),
78 (b"fn", Shape::Callable, true),
79 (b"func", Shape::Callable, true),
80 (b"macro", Shape::Callable, true),
81 (b"procedure", Shape::Callable, true),
82 (b"async_function", Shape::Callable, true),
83 (b"test", Shape::Callable, true),
84 (b"field", Shape::Value, false),
85 (b"column", Shape::Value, false),
86 (b"constraint", Shape::Value, false),
87 (b"trigger", Shape::Value, false),
88 (b"property", Shape::Value, false),
89 (b"event", Shape::Value, false),
90 (b"enum_constant", Shape::Value, false),
91 (b"const", Shape::Value, false),
92 (b"static", Shape::Value, false),
93 (b"path", Shape::Value, false),
94 (b"var", Shape::Value, false),
95 (b"param", Shape::Value, false),
96 (b"local", Shape::Value, false),
97 (b"comment", Shape::Annotation, false),
98];
99
100pub fn shape_of(kind: &[u8]) -> Option<Shape> {
101 SHAPE_TABLE
102 .iter()
103 .find(|(k, _, _)| *k == kind)
104 .map(|(_, s, _)| *s)
105}
106
107pub fn opens_scope(kind: &[u8]) -> bool {
108 SHAPE_TABLE
109 .iter()
110 .find(|(k, _, _)| *k == kind)
111 .is_some_and(|(_, _, opens)| *opens)
112}
113
114pub fn known_kinds() -> impl Iterator<Item = &'static [u8]> {
115 SHAPE_TABLE.iter().map(|(k, _, _)| *k)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn shape_table_has_no_duplicate_kind() {
124 let mut seen = std::collections::HashSet::new();
125 for (k, _, _) in SHAPE_TABLE {
126 assert!(seen.insert(*k), "duplicate kind in SHAPE_TABLE: {k:?}");
127 }
128 }
129
130 #[test]
131 fn unknown_kind_has_no_shape() {
132 assert!(shape_of(b"definitely_not_a_kind").is_none());
133 assert!(!opens_scope(b"definitely_not_a_kind"));
134 }
135
136 #[test]
137 fn internal_kinds_are_classified() {
138 assert_eq!(shape_of(b"module"), Some(Shape::Namespace));
139 assert_eq!(shape_of(b"comment"), Some(Shape::Annotation));
140 assert_eq!(shape_of(b"local"), Some(Shape::Value));
141 assert_eq!(shape_of(b"param"), Some(Shape::Value));
142 }
143
144 #[test]
145 fn comment_is_the_only_annotation() {
146 let annotations: Vec<_> = SHAPE_TABLE
147 .iter()
148 .filter(|(_, s, _)| *s == Shape::Annotation)
149 .map(|(k, _, _)| *k)
150 .collect();
151 assert_eq!(annotations, vec![b"comment".as_slice()]);
152 }
153
154 #[test]
155 fn annotation_never_opens_scope() {
156 for (_, shape, opens) in SHAPE_TABLE {
157 if *shape == Shape::Annotation {
158 assert!(!opens, "annotation kind must not open a scope");
159 }
160 }
161 }
162
163 #[test]
164 fn values_never_open_scope() {
165 for (k, shape, opens) in SHAPE_TABLE {
166 if *shape == Shape::Value {
167 assert!(!opens, "value kind {k:?} must not open a scope");
168 }
169 }
170 }
171
172 #[test]
173 fn callables_always_open_scope() {
174 for (k, shape, opens) in SHAPE_TABLE {
175 if *shape == Shape::Callable {
176 assert!(*opens, "callable kind {k:?} must open a scope");
177 }
178 }
179 }
180
181 #[test]
182 fn namespaces_always_open_scope() {
183 for (k, shape, opens) in SHAPE_TABLE {
184 if *shape == Shape::Namespace {
185 assert!(*opens, "namespace kind {k:?} must open a scope");
186 }
187 }
188 }
189
190 #[test]
191 fn type_containers_open_scope_aliases_do_not() {
192 let containers: &[&[u8]] = &[
193 b"class",
194 b"struct",
195 b"interface",
196 b"trait",
197 b"enum",
198 b"record",
199 b"annotation_type",
200 b"table",
201 ];
202 let aliases: &[&[u8]] = &[b"type", b"view", b"delegate"];
203 for k in containers {
204 assert!(opens_scope(k), "type container {k:?} must open a scope");
205 }
206 for k in aliases {
207 assert!(!opens_scope(k), "type alias {k:?} must not open a scope");
208 }
209 }
210
211 #[test]
212 fn shape_str_round_trip_is_lowercase_word() {
213 for shape in [
214 Shape::Namespace,
215 Shape::Type,
216 Shape::Callable,
217 Shape::Value,
218 Shape::Annotation,
219 ] {
220 let s = shape.as_str();
221 assert!(s.chars().all(|c| c.is_ascii_lowercase()));
222 assert!(!s.is_empty());
223 }
224 }
225}