tinymist_query/analysis/
call.rs

1//! Hybrid analysis for function calls.
2
3use super::Signature;
4use super::prelude::*;
5use crate::analysis::{PrimarySignature, SignatureTarget, analyze_signature};
6
7/// Describes kind of a parameter.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ParamKind {
10    /// A positional parameter.
11    Positional,
12    /// A named parameter.
13    Named,
14    /// A rest (spread) parameter.
15    Rest,
16}
17
18/// Describes a function call parameter.
19#[derive(Debug, Clone)]
20pub struct CallParamInfo {
21    /// The parameter's kind.
22    pub kind: ParamKind,
23    /// Whether the parameter is a content block.
24    pub is_content_block: bool,
25    /// The name of the parameter.
26    pub param_name: StrRef,
27}
28
29/// Describes a function call.
30#[derive(Debug, Clone)]
31pub struct CallInfo {
32    /// The called function's signature.
33    pub signature: Signature,
34    /// The mapping of arguments syntax nodes to their respective parameter
35    /// info.
36    pub arg_mapping: HashMap<SyntaxNode, CallParamInfo>,
37}
38
39/// Gets the callee and argument nodes for a normal or math call.
40pub fn call_parts<'a>(node: &LinkedNode<'a>) -> Option<(LinkedNode<'a>, LinkedNode<'a>)> {
41    match node.cast::<ast::Expr>()? {
42        ast::Expr::FuncCall(call) => {
43            let callee = call.callee();
44            // todo: reduce many such patterns
45            if !callee.hash() && !matches!(callee, ast::Expr::MathIdent(_)) {
46                return None;
47            }
48
49            let callee_node = node.find(callee.span())?;
50            let args_node = node.find(call.args().span())?;
51            Some((callee_node, args_node))
52        }
53        ast::Expr::MathCall(call) => {
54            let callee_node = node.find(call.callee().to_untyped().span())?;
55            let args_node = node.find(call.args().span())?;
56            Some((callee_node, args_node))
57        }
58        _ => None,
59    }
60}
61
62// todo: cache call
63/// Analyzes a function call.
64#[typst_macros::time(span = node.span())]
65pub fn analyze_call(
66    ctx: &mut LocalContext,
67    source: Source,
68    node: LinkedNode,
69) -> Option<Arc<CallInfo>> {
70    log::trace!("func call found: {node:?}");
71    let (callee_node, args_node) = call_parts(&node)?;
72    Some(Arc::new(analyze_call_no_cache(
73        ctx,
74        source,
75        callee_node,
76        args_node,
77    )?))
78}
79
80/// Analyzes a function call without caching the result.
81// todo: testing
82pub fn analyze_call_no_cache(
83    ctx: &mut LocalContext,
84    source: Source,
85    callee_node: LinkedNode,
86    args_node: LinkedNode,
87) -> Option<CallInfo> {
88    let signature = analyze_signature(
89        ctx.shared(),
90        SignatureTarget::SyntaxFast(source, callee_node.span()),
91    )?;
92    log::trace!("got signature {signature:?}");
93
94    let mut info = CallInfo {
95        arg_mapping: HashMap::new(),
96        signature: signature.clone(),
97    };
98
99    enum PosState {
100        Init,
101        Pos(usize),
102        Variadic,
103        Final,
104    }
105
106    struct PosBuilder {
107        state: PosState,
108        out_of_arg_list: bool,
109        signature: Arc<PrimarySignature>,
110    }
111
112    impl PosBuilder {
113        fn advance(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
114            let (kind, param) = match self.state {
115                PosState::Init => {
116                    if !self.signature.pos().is_empty() {
117                        self.state = PosState::Pos(0);
118                    } else if self.signature.has_spread_right() {
119                        self.state = PosState::Variadic;
120                    } else {
121                        self.state = PosState::Final;
122                    }
123
124                    return;
125                }
126                PosState::Pos(pos) => {
127                    if pos + 1 < self.signature.pos_size() {
128                        self.state = PosState::Pos(pos + 1);
129                    } else if self.signature.has_spread_right() {
130                        self.state = PosState::Variadic;
131                    } else {
132                        self.state = PosState::Final;
133                    }
134
135                    (ParamKind::Positional, self.signature.get_pos(pos).unwrap())
136                }
137                PosState::Variadic => (ParamKind::Rest, self.signature.rest().unwrap()),
138                PosState::Final => return,
139            };
140
141            if let Some(arg) = arg {
142                let is_content_block =
143                    self.out_of_arg_list && arg.kind() == SyntaxKind::ContentBlock;
144                info.arg_mapping.insert(
145                    arg,
146                    CallParamInfo {
147                        kind,
148                        is_content_block,
149                        param_name: param.name.clone(),
150                    },
151                );
152            }
153        }
154
155        fn advance_rest(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
156            match self.state {
157                PosState::Init => unreachable!(),
158                // todo: not precise
159                PosState::Pos(..) => {
160                    if self.signature.has_spread_right() {
161                        self.state = PosState::Variadic;
162                    } else {
163                        self.state = PosState::Final;
164                    }
165                }
166                PosState::Variadic => {}
167                PosState::Final => return,
168            };
169
170            let Some(rest) = self.signature.rest() else {
171                return;
172            };
173
174            if let Some(arg) = arg {
175                let is_content_block =
176                    self.out_of_arg_list && arg.kind() == SyntaxKind::ContentBlock;
177                info.arg_mapping.insert(
178                    arg,
179                    CallParamInfo {
180                        kind: ParamKind::Rest,
181                        is_content_block,
182                        param_name: rest.name.clone(),
183                    },
184                );
185            }
186        }
187
188        fn set_out_of_arg_list(&mut self, o: bool) {
189            self.out_of_arg_list = o;
190        }
191    }
192
193    let mut pos_builder = PosBuilder {
194        state: PosState::Init,
195        out_of_arg_list: true,
196        signature: signature.primary().clone(),
197    };
198    pos_builder.advance(&mut info, None);
199
200    for args in signature.bindings().iter().rev() {
201        for _arg in args.items.iter().filter(|arg| arg.name.is_none()) {
202            pos_builder.advance(&mut info, None);
203        }
204    }
205
206    for node in args_node.children() {
207        match node.kind() {
208            SyntaxKind::LeftParen => {
209                pos_builder.set_out_of_arg_list(false);
210                continue;
211            }
212            SyntaxKind::RightParen => {
213                pos_builder.set_out_of_arg_list(true);
214                continue;
215            }
216            _ => {}
217        }
218        let arg_tag = node.get().clone();
219        let Some(arg) = node.cast::<ast::Arg>() else {
220            continue;
221        };
222
223        match arg {
224            ast::Arg::Named(named) => {
225                let n = named.name().get().into();
226
227                if let Some(param) = signature.primary().get_named(&n) {
228                    info.arg_mapping.insert(
229                        arg_tag,
230                        CallParamInfo {
231                            kind: ParamKind::Named,
232                            is_content_block: false,
233                            param_name: param.name.clone(),
234                        },
235                    );
236                }
237            }
238            ast::Arg::Pos(..) => {
239                pos_builder.advance(&mut info, Some(arg_tag));
240            }
241            ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)),
242        }
243    }
244
245    Some(info)
246}