vue_compiler_core/transformer/
process_expression.rs

1// 1. track variables introduced in template
2// currently only v-for and v-slot
3// 2. prefix expression
4use super::collect_entities::is_hoisted_asset;
5use super::{BaseInfo, CorePassExt, Scope, TransformOption};
6use crate::cast;
7use crate::converter::{BindingMetadata, BindingTypes, JsExpr as Js};
8use crate::flags::{RuntimeHelper as RH, StaticLevel};
9use crate::util::{is_global_allow_listed, is_simple_identifier, rslint, VStr};
10
11pub struct ExpressionProcessor<'a, 'b> {
12    pub option: &'b TransformOption,
13    pub binding_metadata: &'b BindingMetadata<'a>,
14}
15
16impl<'a, 'b> CorePassExt<BaseInfo<'a>, Scope<'a>> for ExpressionProcessor<'a, 'b> {
17    fn enter_fn_param(&mut self, p: &mut Js<'a>, shared: &mut Scope<'a>) {
18        self.process_fn_param(p);
19        match p {
20            Js::Param(id) => shared.add_identifier(id),
21            Js::Compound(ids) => {
22                for id in only_param_ids(ids) {
23                    shared.add_identifier(id);
24                }
25            }
26            _ => panic!("only Js::Param is legal"),
27        }
28    }
29    fn exit_fn_param(&mut self, p: &mut Js<'a>, shared: &mut Scope<'a>) {
30        match p {
31            Js::Param(id) => shared.remove_identifier(id),
32            Js::Compound(ids) => {
33                for id in only_param_ids(ids) {
34                    shared.remove_identifier(id);
35                }
36            }
37            _ => panic!("only Js::Param is legal"),
38        };
39    }
40    // only transform expression after its' sub-expression is transformed
41    // e.g. compound/array/call expression
42    fn exit_js_expr(&mut self, e: &mut Js<'a>, shared: &mut Scope<'a>) {
43        self.process_expression(e, shared);
44    }
45}
46
47impl<'a, 'b> ExpressionProcessor<'a, 'b> {
48    // parse expr as function params:
49    fn process_fn_param(&self, p: &mut Js) {
50        if !self.option.prefix_identifier {
51            return;
52        }
53        let raw = *cast!(p, Js::Param);
54        if is_simple_identifier(VStr::raw(raw)) {
55            return;
56        }
57        // 1. breaks down binding pattern e.g. [a, b, c] => identifiers a, b and c
58        // 2. breaks default parameter like v-slot="a = 123" -> (a = 123)
59        let broken_atoms = self.break_down_fn_params(raw);
60        // 3. reunite these 1 and 2 to a compound expression
61        *p = reunite_atoms(raw, broken_atoms, |atom| {
62            let is_param = atom.property;
63            let text = &raw[atom.range];
64            if is_param {
65                Js::Param(text)
66            } else {
67                Js::Simple(VStr::raw(text), StaticLevel::NotStatic)
68            }
69        })
70    }
71    fn process_expression(&self, e: &mut Js<'a>, scope: &Scope) {
72        if !self.option.prefix_identifier {
73            return;
74        }
75        // hoisted component/directive does not need prefixing
76        if is_hoisted_asset(e).is_some() {
77            return;
78        }
79        // complex expr will be handled recursively in transformer
80        if !matches!(e, Js::Simple(..)) {
81            return;
82        }
83        if self.process_expr_fast(e, scope) {
84            return;
85        }
86        self.process_with_js_parser(e, scope);
87    }
88
89    /// prefix _ctx without parsing JS
90    fn process_expr_fast(&self, e: &mut Js<'a>, scope: &Scope) -> bool {
91        let (v, level) = match e {
92            Js::Simple(v, level) => (v, level),
93            _ => return false,
94        };
95        if !is_simple_identifier(*v) {
96            return false;
97        }
98        let raw_exp = v.raw;
99        let is_scope_reference = scope.has_identifier(raw_exp);
100        let is_allowed_global = is_global_allow_listed(raw_exp);
101        let is_literal = matches!(raw_exp, "true" | "false" | "null" | "this");
102        if !is_scope_reference && !is_allowed_global && !is_literal {
103            // const bindings from setup can skip patching but cannot be hoisted
104            // NB: this only applies to simple expression. e.g :prop="constBind()"
105            let bindings = &self.binding_metadata;
106            let lvl = match bindings.get(raw_exp) {
107                Some(BindingTypes::SetupConst) => StaticLevel::CanSkipPatch,
108                _ => *level,
109            };
110            *e = self.rewrite_identifier(*v, lvl, CtxType::NoWrite);
111        } else if !is_scope_reference {
112            *level = if is_literal {
113                StaticLevel::CanStringify
114            } else {
115                StaticLevel::CanHoist
116            };
117        }
118        true
119    }
120
121    fn process_with_js_parser(&self, e: &mut Js<'a>, scope: &Scope) {
122        let (v, level) = match e {
123            Js::Simple(v, level) => (v, level),
124            _ => return,
125        };
126        let raw = v.raw;
127        let (broken_atoms, has_local_ref) = self.break_down_complex_expression(raw, scope);
128        // no prefixed identifier found
129        if broken_atoms.is_empty() {
130            // if expr has no template var nor prefixed var, it can be hoisted as static
131            // NOTE: func call and member access must be bailed for potential side-effect
132            let side_effect = raw.contains('(') || raw.contains('.');
133            *level = if !has_local_ref && !side_effect {
134                StaticLevel::CanStringify
135            } else {
136                StaticLevel::NotStatic
137            };
138            return;
139        }
140        *e = reunite_atoms(raw, broken_atoms, |atom| {
141            let prop = atom.property;
142            let id_str = VStr::raw(&raw[atom.range]);
143            let rewritten = self.rewrite_identifier(id_str, StaticLevel::NotStatic, prop.ctx_type);
144            if prop.is_obj_shorthand {
145                Js::Compound(vec![Js::StrLit(id_str), Js::Src(": "), rewritten])
146            } else {
147                rewritten
148            }
149        });
150    }
151    fn rewrite_identifier(&self, raw: VStr<'a>, level: StaticLevel, ctx: CtxType<'a>) -> Js<'a> {
152        let binding = self.binding_metadata.get(&raw.raw);
153        if let Some(bind) = binding {
154            if self.option.inline {
155                rewrite_inline_identifier(raw, level, bind, ctx)
156            } else {
157                bind.get_js_prop(raw, level)
158            }
159        } else {
160            debug_assert!(level == StaticLevel::NotStatic);
161            Js::simple(*raw.clone().prefix_ctx())
162        }
163    }
164
165    fn break_down_complex_expression(
166        &self,
167        raw: &'a str,
168        scope: &Scope,
169    ) -> (FreeVarAtoms<'a>, bool) {
170        let expr = rslint::parse_js_expr(raw);
171        let expr = match expr {
172            Some(exp) => exp,
173            None => todo!("add error handler"),
174        };
175        let inline = self.option.inline;
176        let mut atoms = vec![];
177        let mut has_local_ref = false;
178        rslint::walk_free_variables(expr, |fv| {
179            let id_text = fv.text();
180            // skip global variable prefixing
181            if is_global_allow_listed(&id_text) || id_text == "require" {
182                return;
183            }
184            let range = fv.range();
185            // skip id defined in the template scope
186            if scope.has_identifier(&raw[range.clone()]) {
187                has_local_ref = true;
188                return;
189            }
190            let ctx_type = if inline { todo!() } else { CtxType::NoWrite };
191            atoms.push(Atom {
192                range,
193                property: FreeVarProp {
194                    ctx_type,
195                    is_obj_shorthand: fv.is_shorthand(),
196                },
197            })
198        });
199        atoms.sort_by_key(|r| r.range.start);
200        (atoms, has_local_ref)
201    }
202
203    /// Atom's property records if it is param identifier
204    fn break_down_fn_params(&self, raw: &'a str) -> Vec<Atom<bool>> {
205        let param = rslint::parse_fn_param(raw);
206        let param = match param {
207            Some(exp) => exp,
208            None => todo!("add error handler"),
209        };
210        // range is offset by -1 due to the wrapping parens when parsed
211        let offset = if raw.starts_with('(') { 0 } else { 1 };
212        let mut atoms = vec![];
213        rslint::walk_param_and_default_arg(param, |range, is_param| {
214            atoms.push(Atom {
215                range: range.start - offset..range.end - offset,
216                property: is_param,
217            });
218        });
219        atoms.sort_by_key(|r| r.range.start);
220        atoms
221    }
222}
223
224// This implementation assumes that broken param expression has only two kinds subexpr:
225// 1. param identifiers represented by Js::Param
226// 2. expression in default binding that has been prefixed
227fn only_param_ids<'a, 'b>(ids: &'b [Js<'a>]) -> impl Iterator<Item = &'a str> + 'b {
228    ids.iter().filter_map(|id| match id {
229        Js::Param(p) => Some(*p),
230        Js::Src(_) => None,
231        Js::Simple(..) => None,
232        Js::Compound(..) => None, // object shorthand
233        _ => panic!("Illegal sub expr kind in param."),
234    })
235}
236
237/// Atom is the atomic identifier text range in the expression.
238/// Property is the additional information for rewriting.
239struct Atom<T> {
240    range: std::ops::Range<usize>,
241    property: T,
242}
243
244struct FreeVarProp<'a> {
245    is_obj_shorthand: bool,
246    ctx_type: CtxType<'a>,
247}
248type FreeVarAtoms<'a> = Vec<Atom<FreeVarProp<'a>>>;
249
250enum CtxType<'a> {
251    /// ref = value, ref += value
252    Assign(Js<'a>),
253    /// ref++, ++ref, ...
254    Update(bool, Js<'a>),
255    /// ({x}) = y
256    Destructure,
257    /// No reactive var writing
258    NoWrite,
259}
260
261fn reunite_atoms<'a, T, F>(raw: &'a str, atoms: Vec<Atom<T>>, mut rewrite: F) -> Js<'a>
262where
263    F: FnMut(Atom<T>) -> Js<'a>,
264{
265    // expr without atoms have specific processing outside
266    debug_assert!(!atoms.is_empty());
267    // the only one ident that spans the text should be handled in fast path
268    debug_assert!(atoms.len() > 1 || atoms[0].range.len() < raw.len());
269    let mut inner = vec![];
270    let mut last = 0;
271    for atom in atoms {
272        let range = &atom.range;
273        if last < range.start {
274            let comp = Js::Src(&raw[last..range.start]);
275            inner.push(comp);
276        }
277        last = range.end;
278        let rewritten = rewrite(atom);
279        inner.push(rewritten);
280    }
281    if last < raw.len() {
282        inner.push(Js::Src(&raw[last..]));
283    }
284    Js::Compound(inner)
285}
286
287fn rewrite_inline_identifier<'a>(
288    raw: VStr<'a>,
289    level: StaticLevel,
290    bind: &BindingTypes,
291    ctx: CtxType<'a>,
292) -> Js<'a> {
293    use BindingTypes as BT;
294    debug_assert!(level == StaticLevel::NotStatic || bind == &BT::SetupConst);
295    let expr = move || Js::Simple(raw, level);
296    let dot_value = Js::Compound(vec![expr(), Js::Src(".value")]);
297    match bind {
298        BT::SetupConst => expr(),
299        BT::SetupRef => dot_value,
300        BT::SetupMaybeRef => {
301            // const binding that may or may not be ref
302            // if it's not a ref, then assignments don't make sense -
303            // so we ignore the non-ref assignment case and generate code
304            // that assumes the value to be a ref for more efficiency
305            if !matches!(ctx, CtxType::NoWrite) {
306                dot_value
307            } else {
308                Js::Call(RH::Unref, vec![expr()])
309            }
310        }
311        BT::SetupLet => rewrite_setup_let(ctx, expr, dot_value),
312        BT::Props => Js::Compound(vec![Js::Src("__props."), expr()]),
313        BT::Data | BT::Options => Js::Compound(vec![Js::Src("_ctx."), expr()]),
314    }
315}
316
317fn rewrite_setup_let<'a, E>(ctx: CtxType<'a>, expr: E, dot_value: Js<'a>) -> Js<'a>
318where
319    E: Fn() -> Js<'a>,
320{
321    match ctx {
322        CtxType::Assign(assign) => Js::Compound(vec![
323            Js::Call(RH::IsRef, vec![expr()]),
324            Js::Src("? "),
325            dot_value,
326            assign.clone(),
327            Js::Src(": "),
328            expr(),
329            assign,
330        ]),
331        CtxType::Update(is_pre, op) => {
332            let mut v = vec![Js::Call(RH::IsRef, vec![expr()])];
333            v.push(Js::Src("? "));
334            let push = |v: &mut Vec<_>, val, op| {
335                if is_pre {
336                    v.extend([op, val]);
337                } else {
338                    v.extend([val, op]);
339                }
340            };
341            push(&mut v, dot_value, op.clone());
342            v.push(Js::Src(": "));
343            push(&mut v, expr(), op);
344            Js::Compound(v)
345        }
346        CtxType::Destructure => {
347            // TODO let binding in a destructure assignment - it's very tricky to
348            // handle both possible cases here without altering the original
349            // structure of the code, so we just assume it's not a ref here for now
350            expr()
351        }
352        CtxType::NoWrite => Js::Call(RH::Unref, vec![expr()]),
353    }
354}
355
356#[cfg(test)]
357mod test {
358    use super::super::{
359        test::{base_convert, transformer_ext},
360        BaseRoot, TransformOption, Transformer,
361    };
362    use super::*;
363    use crate::cast;
364    use crate::converter::{BaseIR, IRNode};
365
366    fn transform(s: &str) -> BaseRoot {
367        let option = TransformOption {
368            prefix_identifier: true,
369            ..Default::default()
370        };
371        let mut ir = base_convert(s);
372        let mut exp = ExpressionProcessor {
373            option: &option,
374            binding_metadata: &Default::default(),
375        };
376        let a: &mut [&mut dyn CorePassExt<_, _>] = &mut [&mut exp];
377        let mut transformer = transformer_ext(a);
378        transformer.transform(&mut ir);
379        ir
380    }
381    fn first_child(ir: BaseRoot) -> BaseIR {
382        ir.body.into_iter().next().unwrap()
383    }
384
385    #[test]
386    fn test_interpolation_prefix() {
387        let ir = transform("{{test}}");
388        let text = cast!(first_child(ir), IRNode::TextCall);
389        let text = match &text.texts[0] {
390            Js::Call(_, r) => &r[0],
391            _ => panic!("wrong interpolation"),
392        };
393        let expr = cast!(text, Js::Simple);
394        assert_eq!(expr.into_string(), "_ctx.test");
395    }
396    #[test]
397    fn test_prop_prefix() {
398        let ir = transform("<p :test='a'/>");
399        let vn = cast!(first_child(ir), IRNode::VNodeCall);
400        let props = vn.props.unwrap();
401        let props = cast!(props, Js::Props);
402        let key = cast!(&props[0].0, Js::StrLit);
403        assert_eq!(key.into_string(), "test");
404        let expr = cast!(&props[0].1, Js::Simple);
405        assert_eq!(expr.into_string(), "_ctx.a");
406    }
407    #[test]
408    fn test_v_bind_prefix() {
409        let ir = transform("<p v-bind='b'/>");
410        let vn = cast!(&ir.body[0], IRNode::VNodeCall);
411        let props = vn.props.as_ref().unwrap();
412        let expr = cast!(props, Js::Simple);
413        assert_eq!(expr.into_string(), "_ctx.b");
414    }
415    #[test]
416    fn test_prefix_v_for() {
417        let ir = transform("<p v-for='a in b'/>");
418        let v_for = cast!(first_child(ir), IRNode::For);
419        let b = cast!(v_for.source, Js::Simple);
420        let a = cast!(v_for.parse_result.value, Js::Param);
421        assert_eq!(a, "a");
422        assert_eq!(b.into_string(), "_ctx.b");
423    }
424    #[test]
425    fn test_complex_expression() {
426        let ir = transform("{{a + b}}");
427        let text = cast!(first_child(ir), IRNode::TextCall);
428        let text = match &text.texts[0] {
429            Js::Call(_, r) => &r[0],
430            _ => panic!("wrong interpolation"),
431        };
432        let expr = cast!(text, Js::Compound);
433        let a = cast!(expr[0], Js::Simple);
434        let b = cast!(expr[2], Js::Simple);
435        assert_eq!(a.into_string(), "_ctx.a");
436        assert_eq!(b.into_string(), "_ctx.b");
437    }
438
439    #[test]
440    fn test_transform_shorthand() {
441        let ir = transform("{{ {a} }}");
442        let text = cast!(first_child(ir), IRNode::TextCall);
443        let text = match &text.texts[0] {
444            Js::Call(_, r) => &r[0],
445            _ => panic!("wrong interpolation"),
446        };
447        let expr = cast!(text, Js::Compound);
448        let prop = cast!(&expr[1], Js::Compound);
449        let key = cast!(prop[0], Js::StrLit);
450        let colon = cast!(prop[1], Js::Src);
451        let val = cast!(prop[2], Js::Simple);
452        assert_eq!(key.into_string(), "a");
453        assert_eq!(colon, ": ");
454        assert_eq!(val.into_string(), "_ctx.a");
455    }
456
457    #[test]
458    fn test_transform_fn_param() {
459        let ir = transform("<p v-for='a=c in b'/>");
460        let v_for = cast!(first_child(ir), IRNode::For);
461        let val = cast!(v_for.parse_result.value, Js::Compound);
462        let a = cast!(val[0], Js::Param);
463        let c = cast!(val[2], Js::Simple);
464        assert_eq!(a, "a");
465        assert_eq!(c.into_string(), "_ctx.c");
466    }
467    #[test]
468    fn test_transform_destruct() {
469        let ir = transform("<p v-for='{a: dd} in b' :yes='a' :not='dd' />");
470        let v_for = cast!(first_child(ir), IRNode::For);
471        let val = cast!(v_for.parse_result.value, Js::Compound);
472        let dd = cast!(val[1], Js::Param);
473        assert_eq!(dd, "dd");
474        let p = cast!(*v_for.child, IRNode::VNodeCall);
475        let props = cast!(p.props.unwrap(), Js::Props);
476        let a = cast!(props[0].1, Js::Simple);
477        let dd = cast!(props[1].1, Js::Simple);
478        assert_eq!(a.into_string(), "_ctx.a");
479        assert_eq!(dd.into_string(), "dd");
480    }
481
482    #[test]
483    fn test_transform_default_shorthand() {
484        let ir = transform("<p v-for='a={c} in b'/>");
485        let v_for = cast!(first_child(ir), IRNode::For);
486        let val = cast!(v_for.parse_result.value, Js::Compound);
487        let c = cast!(&val[2], Js::Compound);
488        let prop = cast!(&c[1], Js::Compound);
489        let key = cast!(prop[0], Js::StrLit);
490        let val = cast!(prop[2], Js::Simple);
491        assert_eq!(key.into_string(), "c");
492        assert_eq!(val.into_string(), "_ctx.c");
493    }
494}