yog_script_analyze/typeck/
templates.rs1use super::Checker;
9use super::members::Callee;
10use super::own::TypeKey;
11use super::types::MethodUse;
12use crate::ast::{Arg, Expr, ExprKind, StrSeg};
13use yog_ids::script_problem;
14use yog_script_ids::system::{LOGGER_EXTENSIONS, LOGGER_MESSAGE};
15use crate::lexer::VERBATIM;
16use crate::lexer::string::{CLOSE, ESCAPE, ESCAPES, OPEN, QUOTE};
17
18#[derive(Debug, Clone, PartialEq)]
21pub struct MessageTemplate {
22 pub text: String,
23 pub values: bool,
24}
25
26impl Checker<'_, '_, '_> {
27 pub(super) fn template_slots(&self, target: &Callee, callee: &Expr, args: &[Arg]) -> Vec<usize> {
30 let ExprKind::Member { name, .. } = &callee.kind else {
31 return Vec::new();
32 };
33 let mut slots: Vec<usize> = Vec::new();
34 let mut note = |at: Option<usize>| {
35 if let Some(i) = at
36 && args.get(i).is_some_and(|a| has_holes(&a.value))
37 && !slots.contains(&i)
38 {
39 slots.push(i);
40 }
41 };
42 let instance = matches!(target, Callee::Own(_));
43 if let Callee::Own(methods) | Callee::Extension(methods) = target {
44 for (m, _) in methods {
45 note(self.template_of(m, &name.text).map(|p| p - usize::from(!instance)));
46 }
47 }
48 for (_, m) in self.own.extension_methods.get(&name.text).into_iter().flatten() {
49 note(self.template_of(m, &name.text).and_then(|p| p.checked_sub(1)));
50 }
51 slots
52 }
53
54 fn template_of(&self, m: &crate::ast::Method, name: &str) -> Option<usize> {
56 let param = self
57 .own
58 .all()
59 .find_map(|(_, t)| {
60 let methods = t.methods.get(name)?;
61 let at = methods.iter().position(|x| x.name.span == m.name.span)?;
62 t.templates.get(name)?.get(at).cloned().flatten()
63 })?;
64 m.params.iter().position(|p| p.name.text == param)
65 }
66
67 pub(super) fn templates_chosen(&mut self, used: Option<&MethodUse>, written: &[Arg], slots: &[usize]) {
71 let chosen = used.and_then(|u| self.chosen_template(u, written.len()));
72 if let Some(u) = used
73 && u.owner.as_deref() == Some(LOGGER_EXTENSIONS)
74 && !matches!(&self.this, Some(super::model::Type::Own(n, _)) if n == LOGGER_EXTENSIONS)
76 && let Some(at) = written.first()
77 {
78 self.report.warning(
79 script_problem::LOG_BY_METHOD,
80 at.value.span,
81 format!("`{}` reads its template and boxes its values at each entry: a `[{LOGGER_MESSAGE}]` method checks the level first and formats by compiled code", u.name),
82 );
83 }
84 for &i in slots {
85 let arg = &written[i].value;
86 match chosen {
87 Some((at, values)) if at == i => {
88 let text = template_text(self.report.text(arg.span));
89 if values {
90 for h in holes(arg) {
91 self.expr(h);
92 }
93 }
94 self.types.set_template(arg, MessageTemplate { text, values });
95 }
96 _ => {
97 self.expr(arg);
98 }
99 }
100 }
101 }
102
103 fn chosen_template(&self, used: &MethodUse, written: usize) -> Option<(usize, bool)> {
107 let owner = used.owner.as_ref()?;
108 let own = self.own.of_key(&TypeKey::new(owner, used.owner_arity))?;
109 let m = own.methods.get(&used.name)?.get(used.overload)?;
110 let param = own.templates.get(&used.name)?.get(used.overload)?.as_ref()?;
111 let p = m.params.iter().position(|x| &x.name.text == param)?;
112 let at = p - usize::from(used.extension);
113 let rest_after = m.params.len() == p + 2 && m.params.last().is_some_and(|x| x.params);
114 Some((at, rest_after && written == at + 1))
115 }
116}
117
118fn has_holes(e: &Expr) -> bool {
120 matches!(&e.kind, ExprKind::Str(segs) if segs.iter().any(|s| matches!(s, StrSeg::Expr(_))))
121}
122
123pub fn holes(e: &Expr) -> Vec<&Expr> {
125 let ExprKind::Str(segs) = &e.kind else {
126 return Vec::new();
127 };
128 segs.iter()
129 .filter_map(|s| match s {
130 StrSeg::Expr(h) => Some(hole_value(h)),
131 StrSeg::Text(_) => None,
132 })
133 .collect()
134}
135
136fn hole_value(h: &Expr) -> &Expr {
139 use yog_script_ids::members::{PAD_LEFT, PAD_RIGHT, TO_STRING};
140 let ExprKind::Call { callee, args } = &h.kind else {
141 return h;
142 };
143 let ExprKind::Member { expr, name, .. } = &callee.kind else {
144 return h;
145 };
146 let made = (h.span == expr.span) && h.span == callee.span;
147 match name.text.as_str() {
148 PAD_LEFT | PAD_RIGHT if made => hole_value(expr),
149 TO_STRING if made && args.len() == 1 => expr,
150 _ => h,
151 }
152}
153
154fn template_text(written: &str) -> String {
157 let verbatim = written.strip_prefix(VERBATIM);
158 let quoted = verbatim.unwrap_or(written);
159 let inner = quoted.strip_prefix(QUOTE).and_then(|s| s.strip_suffix(QUOTE)).unwrap_or(quoted);
160 if verbatim.is_some() {
162 return inner.replace(&format!("{QUOTE}{QUOTE}"), "E.to_string());
163 }
164 let mut out = String::new();
165 let mut chars = inner.chars();
166 while let Some(c) = chars.next() {
167 if c != ESCAPE {
168 out.push(c);
169 continue;
170 }
171 match chars.next() {
172 Some(b @ (OPEN | CLOSE)) => {
173 out.push(b);
174 out.push(b);
175 }
176 Some(e) => out.push(ESCAPES.iter().find(|(k, _)| *k == e).map_or(e, |(_, v)| *v)),
177 None => {}
178 }
179 }
180 out
181}