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 crate::lexer::VERBATIM;
14use crate::lexer::string::{CLOSE, ESCAPE, ESCAPES, OPEN, QUOTE};
15use yog_ids::script_problem;
16use yog_script_ids::system::{LOGGER_EXTENSIONS, LOGGER_MESSAGE};
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(
30 &self,
31 target: &Callee,
32 callee: &Expr,
33 args: &[Arg],
34 ) -> Vec<usize> {
35 let ExprKind::Member { name, .. } = &callee.kind else {
36 return Vec::new();
37 };
38 let mut slots: Vec<usize> = Vec::new();
39 let mut note = |at: Option<usize>| {
40 if let Some(i) = at
41 && args.get(i).is_some_and(|a| has_holes(&a.value))
42 && !slots.contains(&i)
43 {
44 slots.push(i);
45 }
46 };
47 let instance = matches!(target, Callee::Own(_));
48 if let Callee::Own(methods) | Callee::Extension(methods) = target {
49 for (m, _) in methods {
50 note(
51 self.template_of(m, &name.text)
52 .map(|p| p - usize::from(!instance)),
53 );
54 }
55 }
56 for (_, m) in self
57 .own
58 .extension_methods
59 .get(&name.text)
60 .into_iter()
61 .flatten()
62 {
63 note(
64 self.template_of(m, &name.text)
65 .and_then(|p| p.checked_sub(1)),
66 );
67 }
68 slots
69 }
70
71 fn template_of(&self, m: &crate::ast::Method, name: &str) -> Option<usize> {
73 let param = self.own.all().find_map(|(_, t)| {
74 let methods = t.methods.get(name)?;
75 let at = methods.iter().position(|x| x.name.span == m.name.span)?;
76 t.templates.get(name)?.get(at).cloned().flatten()
77 })?;
78 m.params.iter().position(|p| p.name.text == param)
79 }
80
81 pub(super) fn templates_chosen(
85 &mut self,
86 used: Option<&MethodUse>,
87 written: &[Arg],
88 slots: &[usize],
89 ) {
90 let chosen = used.and_then(|u| self.chosen_template(u, written.len()));
91 if let Some(u) = used
92 && u.owner.as_deref() == Some(LOGGER_EXTENSIONS)
93 && !matches!(&self.this, Some(super::model::Type::Own(n, _)) if n == LOGGER_EXTENSIONS)
95 && let Some(at) = written.first()
96 {
97 self.report.warning(
98 script_problem::LOG_BY_METHOD,
99 at.value.span,
100 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),
101 );
102 }
103 for &i in slots {
104 let arg = &written[i].value;
105 match chosen {
106 Some((at, values)) if at == i => {
107 let text = template_text(self.report.text(arg.span));
108 if values {
109 for h in holes(arg) {
110 self.expr(h);
111 }
112 }
113 self.types
114 .set_template(arg, MessageTemplate { text, values });
115 }
116 _ => {
117 self.expr(arg);
118 }
119 }
120 }
121 }
122
123 fn chosen_template(&self, used: &MethodUse, written: usize) -> Option<(usize, bool)> {
127 let owner = used.owner.as_ref()?;
128 let own = self.own.of_key(&TypeKey::new(owner, used.owner_arity))?;
129 let m = own.methods.get(&used.name)?.get(used.overload)?;
130 let param = own
131 .templates
132 .get(&used.name)?
133 .get(used.overload)?
134 .as_ref()?;
135 let p = m.params.iter().position(|x| &x.name.text == param)?;
136 let at = p - usize::from(used.extension);
137 let rest_after = m.params.len() == p + 2 && m.params.last().is_some_and(|x| x.params);
138 Some((at, rest_after && written == at + 1))
139 }
140}
141
142fn has_holes(e: &Expr) -> bool {
144 matches!(&e.kind, ExprKind::Str(segs) if segs.iter().any(|s| matches!(s, StrSeg::Expr(_))))
145}
146
147pub fn holes(e: &Expr) -> Vec<&Expr> {
149 let ExprKind::Str(segs) = &e.kind else {
150 return Vec::new();
151 };
152 segs.iter()
153 .filter_map(|s| match s {
154 StrSeg::Expr(h) => Some(hole_value(h)),
155 StrSeg::Text(_) => None,
156 })
157 .collect()
158}
159
160fn hole_value(h: &Expr) -> &Expr {
163 use yog_script_ids::members::{PAD_LEFT, PAD_RIGHT, TO_STRING};
164 let ExprKind::Call { callee, args } = &h.kind else {
165 return h;
166 };
167 let ExprKind::Member { expr, name, .. } = &callee.kind else {
168 return h;
169 };
170 let made = (h.span == expr.span) && h.span == callee.span;
171 match name.text.as_str() {
172 PAD_LEFT | PAD_RIGHT if made => hole_value(expr),
173 TO_STRING if made && args.len() == 1 => expr,
174 _ => h,
175 }
176}
177
178fn template_text(written: &str) -> String {
181 let verbatim = written.strip_prefix(VERBATIM);
182 let quoted = verbatim.unwrap_or(written);
183 let inner = quoted
184 .strip_prefix(QUOTE)
185 .and_then(|s| s.strip_suffix(QUOTE))
186 .unwrap_or(quoted);
187 if verbatim.is_some() {
189 return inner.replace(&format!("{QUOTE}{QUOTE}"), "E.to_string());
190 }
191 let mut out = String::new();
192 let mut chars = inner.chars();
193 while let Some(c) = chars.next() {
194 if c != ESCAPE {
195 out.push(c);
196 continue;
197 }
198 match chars.next() {
199 Some(b @ (OPEN | CLOSE)) => {
200 out.push(b);
201 out.push(b);
202 }
203 Some(e) => out.push(ESCAPES.iter().find(|(k, _)| *k == e).map_or(e, |(_, v)| *v)),
204 None => {}
205 }
206 }
207 out
208}