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