Skip to main content

yog_script_analyze/typeck/
templates.rs

1//! A string written for a message template parameter
2//! (`[StructuredMessageTemplate(nameof(message))]`, `logging`): the template
3//! as written — its holes names, its values the arguments after it
4//! (`"Moved {Count} items", count`); or, none given after it, its holes the
5//! values too (`"Moved {count} items"`: `count`, named `count`). Its holes
6//! aren't checked until the call's method is known: names aren't code.
7
8use 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/// A string written for a message template: its text as written (holes
19/// and all), and whether its holes are its values.
20#[derive(Debug, Clone, PartialEq)]
21pub struct MessageTemplate {
22    pub text: String,
23    pub values: bool,
24}
25
26impl Checker<'_, '_, '_> {
27    /// The written arguments a method `target` (or an extension of its
28    /// name) may take as a message template: strings with holes, there.
29    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    /// The index of method `m`'s (named `name`) template parameter, if it has one.
72    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    /// The method chosen (`used`, if one): each of `slots` it takes as a
82    /// template noted so (its holes checked when they're its values); the
83    /// others checked as the strings they are.
84    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            // Its own methods write through each other.
94            && !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    /// The written index of `used`'s template parameter, and whether a
124    /// template written there gives the values (nothing written after it,
125    /// a `params` one after it).
126    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
142/// Whether `e` is a string written with holes.
143fn has_holes(e: &Expr) -> bool {
144    matches!(&e.kind, ExprKind::Str(segs) if segs.iter().any(|s| matches!(s, StrSeg::Expr(_))))
145}
146
147/// Each hole's value of string `e`, in order.
148pub 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
160/// A hole's value without what its alignment and format made of it
161/// (`{x,5:F2}` is `x.ToString("F2").PadLeft(5)`: `x`; the template keeps them).
162fn 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
178/// A string literal's text as written (`"…"`), its escapes read, its holes
179/// kept: what a template is (a brace escaped, doubled as the template has it).
180fn 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    // `@"…"`: no escapes, a `""` its quote.
188    if verbatim.is_some() {
189        return inner.replace(&format!("{QUOTE}{QUOTE}"), &QUOTE.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}