Skip to main content

sofie_model/
effects.rs

1//! Read from syntax, so a value whose type only inference knows is recorded without one, never guessed.
2
3use std::collections::BTreeMap;
4
5use syn::visit::{self, Visit};
6use syn::{Block, Expr, ExprCall, ExprMethodCall, FnArg, Macro, Pat, Signature};
7
8use crate::code::TypeName;
9use crate::handlers::HandlerScope;
10use crate::syntax::{last_name, line_of, macro_expressions, segments};
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
13pub struct Effects {
14    pub calls: Vec<Call>,
15    pub method_calls: Vec<MethodCall>,
16    pub raised: Vec<Raised>,
17    pub queued: Vec<Queued>,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Call {
22    pub path: Vec<String>,
23    pub line: usize,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct MethodCall {
28    pub receiver: Option<String>,
29    pub method: String,
30    pub line: usize,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Raised {
35    pub event: Option<String>,
36    pub line: usize,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct Queued {
41    pub job: Option<Vec<String>>,
42    pub line: usize,
43}
44
45impl Effects {
46    pub(crate) fn of(
47        signature: &Signature,
48        body: &Block,
49        owner: Option<&str>,
50        scope: &HandlerScope,
51    ) -> Self {
52        let mut visitor = EffectVisitor {
53            typed: typed_parameters(signature, owner),
54            scope,
55            owner,
56            found: Self::default(),
57        };
58        visitor.visit_block(body);
59        visitor.found
60    }
61}
62
63fn typed_parameters(signature: &Signature, owner: Option<&str>) -> BTreeMap<String, String> {
64    let mut typed: BTreeMap<String, String> = signature
65        .inputs
66        .iter()
67        .filter_map(|argument| match argument {
68            FnArg::Typed(typed) => match typed.pat.as_ref() {
69                Pat::Ident(name) => {
70                    Some((name.ident.to_string(), referenced(TypeName::of(&typed.ty))))
71                }
72                _ => None,
73            },
74            FnArg::Receiver(_) => None,
75        })
76        .collect();
77    if let Some(owner) = owner {
78        typed.insert("self".to_owned(), owner.to_owned());
79    }
80    typed
81}
82
83fn referenced(mut ty: TypeName) -> String {
84    while ty.name == "&" && ty.arguments.len() == 1 {
85        ty = ty.arguments.remove(0);
86    }
87    ty.name
88}
89
90struct EffectVisitor<'s> {
91    typed: BTreeMap<String, String>,
92    scope: &'s HandlerScope,
93    owner: Option<&'s str>,
94    found: Effects,
95}
96
97impl EffectVisitor<'_> {
98    fn callee(&self, path: &syn::Path) -> Vec<String> {
99        let written = segments(path);
100        match (written.first().map(String::as_str), self.owner) {
101            (Some("Self"), Some(owner)) => [&[owner.to_owned()], &written[1..]].concat(),
102            _ => self.scope.callee(&written),
103        }
104    }
105
106    fn receiver_type(&self, receiver: &Expr) -> Option<String> {
107        let Expr::Path(path) = receiver else {
108            return None;
109        };
110        let name = path.path.get_ident()?.to_string();
111        self.typed.get(&name).cloned()
112    }
113}
114
115impl Visit<'_> for EffectVisitor<'_> {
116    fn visit_expr_call(&mut self, call: &ExprCall) {
117        if let Expr::Path(function) = call.func.as_ref() {
118            self.found.calls.push(Call {
119                path: self.callee(&function.path),
120                line: line_of(call),
121            });
122        }
123        visit::visit_expr_call(self, call);
124    }
125
126    fn visit_expr_method_call(&mut self, call: &ExprMethodCall) {
127        let line = line_of(&call.method);
128        let first = call.args.first();
129        match call.method.to_string().as_str() {
130            "raise" => self.found.raised.push(Raised {
131                event: first.and_then(event_name),
132                line,
133            }),
134            "queue" => self.found.queued.push(Queued {
135                job: first.and_then(|job| match job {
136                    Expr::Path(path) => Some(self.callee(&path.path)),
137                    _ => None,
138                }),
139                line,
140            }),
141            method => self.found.method_calls.push(MethodCall {
142                receiver: self.receiver_type(&call.receiver),
143                method: method.to_owned(),
144                line,
145            }),
146        }
147        visit::visit_expr_method_call(self, call);
148    }
149
150    fn visit_macro(&mut self, call: &Macro) {
151        for expression in macro_expressions(call) {
152            self.visit_expr(&expression);
153        }
154    }
155}
156
157fn event_name(argument: &Expr) -> Option<String> {
158    match argument {
159        Expr::Struct(literal) => last_name(&literal.path),
160        Expr::Path(path) => last_name(&path.path),
161        _ => None,
162    }
163}