1use 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 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 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 let broken_atoms = self.break_down_fn_params(raw);
60 *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 if is_hoisted_asset(e).is_some() {
77 return;
78 }
79 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 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 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 if broken_atoms.is_empty() {
130 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 if is_global_allow_listed(&id_text) || id_text == "require" {
182 return;
183 }
184 let range = fv.range();
185 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 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 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
224fn 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, _ => panic!("Illegal sub expr kind in param."),
234 })
235}
236
237struct 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 Assign(Js<'a>),
253 Update(bool, Js<'a>),
255 Destructure,
257 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 debug_assert!(!atoms.is_empty());
267 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 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 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}