Skip to main content

presolve_compiler/
template_semantics.rs

1use crate::semantic_id::{SemanticId, SemanticOwner};
2use crate::semantic_provenance::SourceProvenance;
3use crate::template_graph::{
4    AttributeValue, ConditionalNode, ElementNode, FragmentNode, ListNode, TemplateChild,
5    TemplateNode,
6};
7
8/// Typed semantic entities lowered from authored template descendants.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TemplateSemanticEntity {
11    pub id: SemanticId,
12    pub owner: SemanticOwner,
13    pub kind: TemplateSemanticKind,
14    pub scope: TemplateSemanticScope,
15    pub tag_name: Option<String>,
16    pub tag_name_provenance: Option<SourceProvenance>,
17    pub attribute_name: Option<String>,
18    pub list_item_variable: Option<String>,
19    pub list_index_variable: Option<String>,
20    pub expression: Option<String>,
21    pub provenance: SourceProvenance,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TemplateSemanticKind {
26    Element,
27    Fragment,
28    Text,
29    Binding,
30    Attribute,
31    AttributeBinding,
32    EventAttribute,
33    Conditional,
34    List,
35}
36
37/// The lexical template scope in which an authored template entity appears.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TemplateSemanticScope {
40    Render,
41    ListItem,
42}
43
44#[must_use]
45pub fn build_template_semantic_entities(templates: &[TemplateNode]) -> Vec<TemplateSemanticEntity> {
46    let mut entities = Vec::new();
47
48    for template in templates {
49        if let Some(root) = &template.root {
50            collect_element(
51                root,
52                template,
53                "root",
54                TemplateSemanticScope::Render,
55                &mut entities,
56            );
57        }
58        if let Some(root) = &template.root_fragment {
59            collect_fragment(
60                root,
61                template,
62                "root",
63                TemplateSemanticScope::Render,
64                &mut entities,
65            );
66        }
67    }
68
69    entities
70}
71
72fn collect_element(
73    element: &ElementNode,
74    template: &TemplateNode,
75    path: &str,
76    scope: TemplateSemanticScope,
77    entities: &mut Vec<TemplateSemanticEntity>,
78) {
79    push_entity(
80        entities,
81        template,
82        TemplateSemanticKind::Element,
83        scope,
84        "element",
85        path,
86        None,
87        element.span,
88    );
89    entities
90        .last_mut()
91        .expect("element semantic entity was just inserted")
92        .tag_name = Some(element.tag_name.clone());
93    entities
94        .last_mut()
95        .expect("element semantic entity was just inserted")
96        .tag_name_provenance = Some(SourceProvenance::new(
97        &template.provenance.path,
98        element.tag_name_span,
99    ));
100
101    for attribute in &element.attributes {
102        let Some(span) = attribute.span else {
103            continue;
104        };
105        let attribute_path = format!("{path}.{}", attribute.name);
106        let (kind, expression) = match &attribute.value {
107            AttributeValue::Binding { expression, .. } => (
108                TemplateSemanticKind::AttributeBinding,
109                Some(expression.clone()),
110            ),
111            AttributeValue::EventHandler { handler, .. } => {
112                (TemplateSemanticKind::EventAttribute, Some(handler.clone()))
113            }
114            AttributeValue::BindingList(_) => continue,
115            AttributeValue::Boolean | AttributeValue::Static(_) => {
116                (TemplateSemanticKind::Attribute, None)
117            }
118        };
119        push_entity(
120            entities,
121            template,
122            kind,
123            scope,
124            match kind {
125                TemplateSemanticKind::AttributeBinding => "attribute-binding",
126                TemplateSemanticKind::EventAttribute => "event-attribute",
127                _ => "attribute",
128            },
129            &attribute_path,
130            expression,
131            span,
132        );
133        entities
134            .last_mut()
135            .expect("attribute semantic entity was just inserted")
136            .attribute_name = Some(attribute.name.clone());
137    }
138
139    collect_children(&element.children, template, path, scope, entities);
140}
141
142fn collect_fragment(
143    fragment: &FragmentNode,
144    template: &TemplateNode,
145    path: &str,
146    scope: TemplateSemanticScope,
147    entities: &mut Vec<TemplateSemanticEntity>,
148) {
149    push_entity(
150        entities,
151        template,
152        TemplateSemanticKind::Fragment,
153        scope,
154        "fragment",
155        path,
156        None,
157        fragment.span,
158    );
159    collect_children(&fragment.children, template, path, scope, entities);
160}
161
162fn collect_children(
163    children: &[TemplateChild],
164    template: &TemplateNode,
165    parent_path: &str,
166    scope: TemplateSemanticScope,
167    entities: &mut Vec<TemplateSemanticEntity>,
168) {
169    for (index, child) in children.iter().enumerate() {
170        let path = format!("{parent_path}.{index}");
171        match child {
172            TemplateChild::Text { span, .. } => push_entity(
173                entities,
174                template,
175                TemplateSemanticKind::Text,
176                scope,
177                "text",
178                &path,
179                None,
180                *span,
181            ),
182            TemplateChild::Binding {
183                expression, span, ..
184            } => push_entity(
185                entities,
186                template,
187                TemplateSemanticKind::Binding,
188                scope,
189                "binding",
190                &path,
191                Some(expression.clone()),
192                *span,
193            ),
194            TemplateChild::Element(element) => {
195                collect_element(element, template, &path, scope, entities);
196            }
197            TemplateChild::Fragment(fragment) => {
198                collect_fragment(fragment, template, &path, scope, entities);
199            }
200            TemplateChild::Conditional(conditional) => {
201                collect_conditional(conditional, template, &path, scope, entities);
202            }
203            TemplateChild::List(list) => collect_list(list, template, &path, scope, entities),
204        }
205    }
206}
207
208fn collect_conditional(
209    conditional: &ConditionalNode,
210    template: &TemplateNode,
211    path: &str,
212    scope: TemplateSemanticScope,
213    entities: &mut Vec<TemplateSemanticEntity>,
214) {
215    push_entity(
216        entities,
217        template,
218        TemplateSemanticKind::Conditional,
219        scope,
220        "conditional",
221        path,
222        Some(conditional.condition.clone()),
223        conditional.span,
224    );
225    collect_children(
226        &conditional.when_true,
227        template,
228        &format!("{path}.true"),
229        scope,
230        entities,
231    );
232    collect_children(
233        &conditional.when_false,
234        template,
235        &format!("{path}.false"),
236        scope,
237        entities,
238    );
239}
240
241fn collect_list(
242    list: &ListNode,
243    template: &TemplateNode,
244    path: &str,
245    scope: TemplateSemanticScope,
246    entities: &mut Vec<TemplateSemanticEntity>,
247) {
248    push_entity(
249        entities,
250        template,
251        TemplateSemanticKind::List,
252        scope,
253        "list",
254        path,
255        Some(list.iterable.clone()),
256        list.span,
257    );
258    let entity = entities
259        .last_mut()
260        .expect("list semantic entity was just inserted");
261    entity.list_item_variable = Some(list.item_variable.clone());
262    entity.list_index_variable.clone_from(&list.index_variable);
263    collect_children(
264        &list.item_template,
265        template,
266        &format!("{path}.item"),
267        TemplateSemanticScope::ListItem,
268        entities,
269    );
270}
271
272#[allow(clippy::too_many_arguments)]
273fn push_entity(
274    entities: &mut Vec<TemplateSemanticEntity>,
275    template: &TemplateNode,
276    kind: TemplateSemanticKind,
277    scope: TemplateSemanticScope,
278    id_kind: &str,
279    path: &str,
280    expression: Option<String>,
281    span: presolve_parser::SourceSpan,
282) {
283    entities.push(TemplateSemanticEntity {
284        id: template.id.template_entity(id_kind, path),
285        owner: SemanticOwner::entity(template.id.clone()),
286        kind,
287        scope,
288        tag_name: None,
289        tag_name_provenance: None,
290        attribute_name: None,
291        list_item_variable: None,
292        list_index_variable: None,
293        expression,
294        provenance: SourceProvenance::new(&template.provenance.path, span),
295    });
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{build_template_semantic_entities, TemplateSemanticKind};
301    use crate::{
302        build_component_graph_for_module, build_template_graph, SemanticId, SemanticOwner,
303    };
304
305    #[test]
306    fn lowers_authored_template_descendants_and_bindings() {
307        let parsed = presolve_parser::parse_file(
308            "src/Panel.tsx",
309            r#"
310@component("x-panel")
311class Panel extends Component {
312  enabled = state(true);
313
314  render() {
315    return <section title="Panel" hidden={this.enabled}>{this.enabled}</section>;
316  }
317}
318"#,
319        );
320        let graph = build_component_graph_for_module(&parsed);
321        let templates = build_template_graph(&graph);
322        let entities = build_template_semantic_entities(&templates.templates);
323        let template =
324            SemanticId::component_in_module("src/Panel.tsx", Some("x-panel"), "Panel").template();
325
326        let attribute_binding = entities
327            .iter()
328            .find(|entity| {
329                entity
330                    .id
331                    .as_str()
332                    .ends_with("attribute-binding:root.hidden")
333            })
334            .expect("attribute binding entity");
335        let text_binding = entities
336            .iter()
337            .find(|entity| entity.id.as_str().ends_with("binding:root.0"))
338            .expect("text binding entity");
339
340        assert!(entities.iter().any(|entity| {
341            entity.kind == TemplateSemanticKind::Element
342                && entity.id.as_str().ends_with("element:root")
343        }));
344        assert_eq!(
345            attribute_binding.kind,
346            TemplateSemanticKind::AttributeBinding
347        );
348        assert_eq!(
349            attribute_binding.expression.as_deref(),
350            Some("this.enabled")
351        );
352        assert_eq!(attribute_binding.provenance.span.line, 7);
353        assert_eq!(text_binding.kind, TemplateSemanticKind::Binding);
354        assert_eq!(text_binding.owner, SemanticOwner::entity(template));
355    }
356}