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 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/// 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(&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    /// The index of method `m`'s (named `name`) template parameter, if it has one.
55    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    /// The method chosen (`used`, if one): each of `slots` it takes as a
68    /// template noted so (its holes checked when they're its values); the
69    /// others checked as the strings they are.
70    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            // Its own methods write through each other.
75            && !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    /// The written index of `used`'s template parameter, and whether a
104    /// template written there gives the values (nothing written after it,
105    /// a `params` one after it).
106    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
118/// Whether `e` is a string written with holes.
119fn has_holes(e: &Expr) -> bool {
120    matches!(&e.kind, ExprKind::Str(segs) if segs.iter().any(|s| matches!(s, StrSeg::Expr(_))))
121}
122
123/// Each hole's value of string `e`, in order.
124pub 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
136/// A hole's value without what its alignment and format made of it
137/// (`{x,5:F2}` is `x.ToString("F2").PadLeft(5)`: `x`; the template keeps them).
138fn 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
154/// A string literal's text as written (`"…"`), its escapes read, its holes
155/// kept: what a template is (a brace escaped, doubled as the template has it).
156fn 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    // `@"…"`: no escapes, a `""` its quote.
161    if verbatim.is_some() {
162        return inner.replace(&format!("{QUOTE}{QUOTE}"), &QUOTE.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}