vize_croquis 0.76.0

Croquis - Semantic analysis layer for Vize. Quick sketches of meaning from Vue templates.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Element visiting orchestrator.
//!
//! Two-pass directive processing: first pass collects v-for/v-slot scope
//! info (which must be entered before other directives), second pass
//! processes v-bind, v-if, v-show, v-model, v-on in the correct scope.

use crate::analysis::ComponentUsage;
use crate::scope::{VForScopeData, VSlotScopeData};
use crate::ScopeBinding;
use vize_carton::{profile, smallvec, CompactString, SmallVec};
use vize_relief::ast::{ElementNode, ExpressionNode, PropNode};
use vize_relief::BindingType;

use crate::analyzer::helpers::{
    extract_slot_props, is_builtin_directive, is_component_tag, parse_v_for_expression,
};
use crate::analyzer::Analyzer;

impl Analyzer {
    /// Visit element node.
    ///
    /// Orchestrates directive processing, scope management, and child traversal.
    pub(in crate::analyzer) fn visit_element(
        &mut self,
        el: &ElementNode<'_>,
        scope_vars: &mut Vec<CompactString>,
    ) {
        let tag = el.tag.as_str();
        let is_component = is_component_tag(tag);

        // Track component usage
        if self.options.track_usage && is_component {
            self.summary.used_components.insert(CompactString::new(tag));
        }

        // Collect detailed component usage
        let mut component_usage = if is_component && self.options.track_usage {
            Some(ComponentUsage {
                name: CompactString::new(tag),
                start: el.loc.start.offset,
                end: el.loc.end.offset,
                props: SmallVec::new(),
                events: SmallVec::new(),
                slots: SmallVec::new(),
                has_spread_attrs: false,
                scope_id: crate::scope::ScopeId::ROOT,
                vif_guard: None,
            })
        } else {
            None
        };

        // Collect v-slot scopes
        #[allow(clippy::type_complexity)]
        let mut slot_scope: Option<(
            CompactString,
            vize_carton::SmallVec<[CompactString; 4]>,
            Option<CompactString>,
            u32,
        )> = None;

        // Collect v-for scope
        #[allow(clippy::type_complexity)]
        let mut for_scope: Option<(
            vize_carton::SmallVec<[CompactString; 3]>,
            CompactString,
            u32,
            u32,
            Option<CompactString>,
        )> = None;

        let mut key_expression: Option<CompactString> = None;

        // Collect v-if condition for type narrowing
        let mut vif_condition: Option<CompactString> = None;

        // First pass: collect v-for, v-slot scope info, and :key
        // (need to enter scope before processing other directives)
        profile!("croquis.template.element.first_pass", {
            for prop in &el.props {
                if let PropNode::Directive(dir) = prop {
                    // Track directive usage
                    if self.options.track_usage {
                        let name = dir.name.as_str();
                        if !is_builtin_directive(name) {
                            self.summary
                                .used_directives
                                .insert(CompactString::new(name));
                        }
                    }

                    // Handle v-for
                    if dir.name == "for" && self.options.analyze_template_scopes {
                        if let Some(ref exp) = dir.exp {
                            let content = match exp {
                                ExpressionNode::Simple(s) => s.content.as_str(),
                                ExpressionNode::Compound(c) => c.loc.source.as_str(),
                            };
                            let (vars, source) = profile!(
                                "croquis.template.v_for.parse_expression",
                                parse_v_for_expression(content)
                            );
                            if !vars.is_empty() {
                                for_scope = Some((
                                    vars,
                                    source,
                                    el.loc.start.offset,
                                    el.loc.end.offset,
                                    None,
                                ));
                            }
                        }
                    }
                    // Extract :key for v-for scope (needed before entering scope)
                    else if dir.name == "bind" {
                        if let Some(ref arg) = dir.arg {
                            let arg_name = match arg {
                                ExpressionNode::Simple(s) => s.content.as_str(),
                                ExpressionNode::Compound(c) => c.loc.source.as_str(),
                            };
                            if arg_name == "key" {
                                if let Some(ref exp) = dir.exp {
                                    let content = match exp {
                                        ExpressionNode::Simple(s) => s.content.as_str(),
                                        ExpressionNode::Compound(c) => c.loc.source.as_str(),
                                    };
                                    key_expression = Some(CompactString::new(content));
                                }
                            }
                        }
                    }
                    // Handle v-if (extract condition for type narrowing)
                    else if dir.name == "if" || dir.name == "else-if" {
                        if let Some(ref exp) = dir.exp {
                            let content = match exp {
                                ExpressionNode::Simple(s) => s.content.as_str(),
                                ExpressionNode::Compound(c) => c.loc.source.as_str(),
                            };
                            vif_condition = Some(CompactString::new(content));
                        }
                    }
                    // Handle v-slot
                    else if dir.name == "slot" && self.options.analyze_template_scopes {
                        let slot_name = dir
                            .arg
                            .as_ref()
                            .map(|arg| match arg {
                                ExpressionNode::Simple(s) => CompactString::new(s.content.as_str()),
                                ExpressionNode::Compound(c) => {
                                    CompactString::new(c.loc.source.as_str())
                                }
                            })
                            .unwrap_or_else(|| CompactString::const_new("default"));

                        let (prop_names, props_pattern) = if let Some(ref exp) = dir.exp {
                            let content = match exp {
                                ExpressionNode::Simple(s) => s.content.as_str(),
                                ExpressionNode::Compound(c) => c.loc.source.as_str(),
                            };
                            (
                                profile!(
                                    "croquis.template.v_slot.extract_props",
                                    extract_slot_props(content)
                                ),
                                Some(CompactString::new(content)),
                            )
                        } else {
                            (smallvec![], None)
                        };

                        slot_scope =
                            Some((slot_name, prop_names, props_pattern, dir.loc.start.offset));
                    }
                }
            }
        });

        // Enter v-slot scope if present
        let slot_vars_count =
            if let Some((slot_name, prop_names, props_pattern, offset)) = slot_scope {
                let count = prop_names.len();

                if count > 0 || self.options.analyze_template_scopes {
                    self.summary.scopes.enter_v_slot_scope(
                        VSlotScopeData {
                            name: slot_name,
                            props_pattern,
                            prop_names: prop_names.iter().cloned().collect(),
                        },
                        offset,
                        el.loc.end.offset,
                    );

                    for name in prop_names {
                        scope_vars.push(name);
                    }
                }

                count
            } else {
                0
            };

        // Enter v-for scope if present
        let for_vars_count = if let Some((vars, source, start, end, _)) = for_scope {
            let count = vars.len();

            if count > 0 {
                let value_alias = vars
                    .first()
                    .cloned()
                    .unwrap_or_else(|| CompactString::const_new("_"));

                self.summary.scopes.enter_v_for_scope(
                    VForScopeData {
                        value_alias,
                        key_alias: vars.get(1).cloned(),
                        index_alias: vars.get(2).cloned(),
                        source,
                        key_expression,
                    },
                    start,
                    end,
                );

                for var in &vars {
                    self.summary
                        .scopes
                        .add_binding(var.clone(), ScopeBinding::new(BindingType::SetupConst, 0));
                    scope_vars.push(var.clone());
                }
            }

            count
        } else {
            0
        };

        // Capture scope_id for component usage after entering v-for/v-slot scopes
        if let Some(ref mut usage) = component_usage {
            usage.scope_id = self.summary.scopes.current_id();
        }

        // Push v-if / v-else-if guard before processing same-element directives
        // so bindings and handlers on the same element are narrowed too.
        let vif_guard_pushed = if let Some(ref cond) = vif_condition {
            self.vif_guard_stack.push(cond.clone());
            true
        } else {
            false
        };

        if let Some(ref mut usage) = component_usage {
            usage.vif_guard = self.current_vif_guard();
        }

        // Second pass: process other directives AFTER entering v-for/v-slot scopes
        // This ensures expressions like `:todo="todo"` in v-for are in the correct scope
        profile!("croquis.template.element.second_pass", {
            for prop in &el.props {
                if let PropNode::Directive(dir) = prop {
                    // Handle v-bind (key_expression already extracted in first pass)
                    if dir.name == "bind" {
                        profile!(
                            "croquis.template.directive.v_bind",
                            self.handle_v_bind_directive(dir, el, scope_vars)
                        );
                    }
                    // Handle v-if/v-else-if
                    else if dir.name == "if" || dir.name == "else-if" {
                        if self.options.collect_template_expressions {
                            if let Some(ref exp) = dir.exp {
                                let content = match exp {
                                    ExpressionNode::Simple(s) => s.content.as_str(),
                                    ExpressionNode::Compound(c) => c.loc.source.as_str(),
                                };
                                let loc = exp.loc();
                                let scope_id = self.summary.scopes.current_id();
                                self.summary.template_expressions.push(
                                    crate::analysis::TemplateExpression {
                                        content: CompactString::new(content),
                                        kind: crate::analysis::TemplateExpressionKind::VIf,
                                        start: loc.start.offset,
                                        end: loc.end.offset,
                                        scope_id,
                                        vif_guard: self.current_vif_guard(),
                                    },
                                );
                            }
                        }
                    }
                    // Handle v-show
                    else if dir.name == "show" {
                        if self.options.collect_template_expressions {
                            if let Some(ref exp) = dir.exp {
                                let content = match exp {
                                    ExpressionNode::Simple(s) => s.content.as_str(),
                                    ExpressionNode::Compound(c) => c.loc.source.as_str(),
                                };
                                let loc = exp.loc();
                                let scope_id = self.summary.scopes.current_id();
                                self.summary.template_expressions.push(
                                    crate::analysis::TemplateExpression {
                                        content: CompactString::new(content),
                                        kind: crate::analysis::TemplateExpressionKind::VShow,
                                        start: loc.start.offset,
                                        end: loc.end.offset,
                                        scope_id,
                                        vif_guard: self.current_vif_guard(),
                                    },
                                );
                            }
                        }
                    }
                    // Handle v-model
                    else if dir.name == "model" {
                        if self.options.collect_template_expressions {
                            if let Some(ref exp) = dir.exp {
                                let content = match exp {
                                    ExpressionNode::Simple(s) => s.content.as_str(),
                                    ExpressionNode::Compound(c) => c.loc.source.as_str(),
                                };
                                let loc = exp.loc();
                                let scope_id = self.summary.scopes.current_id();
                                self.summary.template_expressions.push(
                                    crate::analysis::TemplateExpression {
                                        content: CompactString::new(content),
                                        kind: crate::analysis::TemplateExpressionKind::VModel,
                                        start: loc.start.offset,
                                        end: loc.end.offset,
                                        scope_id,
                                        vif_guard: self.current_vif_guard(),
                                    },
                                );
                            }
                        }
                    }
                    // Handle v-on
                    else if dir.name == "on" && self.options.analyze_template_scopes {
                        let target_component = if is_component {
                            Some(CompactString::new(tag))
                        } else {
                            None
                        };
                        profile!(
                            "croquis.template.directive.v_on",
                            self.handle_v_on_directive(dir, scope_vars, target_component)
                        );
                    }
                }
            }
        });

        // Check directive expressions for undefined refs
        profile!("croquis.template.element.undefined_refs", {
            if self.options.detect_undefined && self.script_analyzed {
                for prop in &el.props {
                    if let PropNode::Directive(dir) = prop {
                        if let Some(ref exp) = dir.exp {
                            if dir.name != "for" && dir.name != "on" && dir.name != "bind" {
                                self.check_expression_refs(exp, scope_vars, dir.loc.start.offset);
                            }
                        }
                    }
                }
            }
        });

        // Visit children
        profile!("croquis.template.element.children", {
            for child in el.children.iter() {
                self.visit_template_child(child, scope_vars);
            }
        });

        // Pop v-if guard after visiting children
        if vif_guard_pushed {
            self.vif_guard_stack.pop();
        }

        // Exit v-for scope
        if for_vars_count > 0 {
            for _ in 0..for_vars_count {
                scope_vars.pop();
            }
            self.summary.scopes.exit_scope();
        }

        // Exit v-slot scope
        if slot_vars_count > 0 {
            for _ in 0..slot_vars_count {
                scope_vars.pop();
            }
            self.summary.scopes.exit_scope();
        }

        // Collect props and events
        if let Some(ref mut usage) = component_usage {
            profile!(
                "croquis.template.component.props_events",
                self.collect_component_props_events(el, usage)
            );
        }

        // Add component usage
        if let Some(usage) = component_usage {
            self.summary.component_usages.push(usage);
        }

        // Collect element IDs for cross-file analysis
        profile!("croquis.template.element_ids", self.collect_element_ids(el));
    }
}