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// todo: cache call
40/// Analyzes a function call.
41#[typst_macros::time(span = node.span())]
42pub fn analyze_call(
43    ctx: &mut LocalContext,
44    source: Source,
45    node: LinkedNode,
46) -> Option<Arc<CallInfo>> {
47    log::trace!("func call found: {node:?}");
48    let call = node.cast::<ast::FuncCall>()?;
49
50    let callee = call.callee();
51    // todo: reduce many such patterns
52    if !callee.hash() && !matches!(callee, ast::Expr::MathIdent(_)) {
53        return None;
54    }
55
56    let callee_node = node.find(callee.span())?;
57    Some(Arc::new(analyze_call_no_cache(
58        ctx,
59        source,
60        callee_node,
61        call.args(),
62    )?))
63}
64
65/// Analyzes a function call without caching the result.
66// todo: testing
67pub fn analyze_call_no_cache(
68    ctx: &mut LocalContext,
69    source: Source,
70    callee_node: LinkedNode,
71    args: ast::Args<'_>,
72) -> Option<CallInfo> {
73    let signature = analyze_signature(
74        ctx.shared(),
75        SignatureTarget::SyntaxFast(source, callee_node.span()),
76    )?;
77    log::trace!("got signature {signature:?}");
78
79    let mut info = CallInfo {
80        arg_mapping: HashMap::new(),
81        signature: signature.clone(),
82    };
83
84    enum PosState {
85        Init,
86        Pos(usize),
87        Variadic,
88        Final,
89    }
90
91    struct PosBuilder {
92        state: PosState,
93        out_of_arg_list: bool,
94        signature: Arc<PrimarySignature>,
95    }
96
97    impl PosBuilder {
98        fn advance(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
99            let (kind, param) = match self.state {
100                PosState::Init => {
101                    if !self.signature.pos().is_empty() {
102                        self.state = PosState::Pos(0);
103                    } else if self.signature.has_spread_right() {
104                        self.state = PosState::Variadic;
105                    } else {
106                        self.state = PosState::Final;
107                    }
108
109                    return;
110                }
111                PosState::Pos(pos) => {
112                    if pos + 1 < self.signature.pos_size() {
113                        self.state = PosState::Pos(pos + 1);
114                    } else if self.signature.has_spread_right() {
115                        self.state = PosState::Variadic;
116                    } else {
117                        self.state = PosState::Final;
118                    }
119
120                    (ParamKind::Positional, self.signature.get_pos(pos).unwrap())
121                }
122                PosState::Variadic => (ParamKind::Rest, self.signature.rest().unwrap()),
123                PosState::Final => return,
124            };
125
126            if let Some(arg) = arg {
127                let is_content_block =
128                    self.out_of_arg_list && arg.kind() == SyntaxKind::ContentBlock;
129                info.arg_mapping.insert(
130                    arg,
131                    CallParamInfo {
132                        kind,
133                        is_content_block,
134                        param_name: param.name.clone(),
135                    },
136                );
137            }
138        }
139
140        fn advance_rest(&mut self, info: &mut CallInfo, arg: Option<SyntaxNode>) {
141            match self.state {
142                PosState::Init => unreachable!(),
143                // todo: not precise
144                PosState::Pos(..) => {
145                    if self.signature.has_spread_right() {
146                        self.state = PosState::Variadic;
147                    } else {
148                        self.state = PosState::Final;
149                    }
150                }
151                PosState::Variadic => {}
152                PosState::Final => return,
153            };
154
155            let Some(rest) = self.signature.rest() else {
156                return;
157            };
158
159            if let Some(arg) = arg {
160                let is_content_block =
161                    self.out_of_arg_list && arg.kind() == SyntaxKind::ContentBlock;
162                info.arg_mapping.insert(
163                    arg,
164                    CallParamInfo {
165                        kind: ParamKind::Rest,
166                        is_content_block,
167                        param_name: rest.name.clone(),
168                    },
169                );
170            }
171        }
172
173        fn set_out_of_arg_list(&mut self, o: bool) {
174            self.out_of_arg_list = o;
175        }
176    }
177
178    let mut pos_builder = PosBuilder {
179        state: PosState::Init,
180        out_of_arg_list: true,
181        signature: signature.primary().clone(),
182    };
183    pos_builder.advance(&mut info, None);
184
185    for args in signature.bindings().iter().rev() {
186        for _arg in args.items.iter().filter(|arg| arg.name.is_none()) {
187            pos_builder.advance(&mut info, None);
188        }
189    }
190
191    for node in args.to_untyped().children() {
192        match node.kind() {
193            SyntaxKind::LeftParen => {
194                pos_builder.set_out_of_arg_list(false);
195                continue;
196            }
197            SyntaxKind::RightParen => {
198                pos_builder.set_out_of_arg_list(true);
199                continue;
200            }
201            _ => {}
202        }
203        let arg_tag = node.clone();
204        let Some(arg) = node.cast::<ast::Arg>() else {
205            continue;
206        };
207
208        match arg {
209            ast::Arg::Named(named) => {
210                let n = named.name().get().into();
211
212                if let Some(param) = signature.primary().get_named(&n) {
213                    info.arg_mapping.insert(
214                        arg_tag,
215                        CallParamInfo {
216                            kind: ParamKind::Named,
217                            is_content_block: false,
218                            param_name: param.name.clone(),
219                        },
220                    );
221                }
222            }
223            ast::Arg::Pos(..) => {
224                pos_builder.advance(&mut info, Some(arg_tag));
225            }
226            ast::Arg::Spread(..) => pos_builder.advance_rest(&mut info, Some(arg_tag)),
227        }
228    }
229
230    Some(info)
231}