tinymist_query/
analysis.rs

1//! Semantic static and dynamic analysis of the source code.
2
3mod bib;
4
5pub(crate) use bib::*;
6pub mod call;
7pub use call::*;
8pub mod completion;
9pub use completion::*;
10pub mod code_action;
11pub use code_action::*;
12pub mod color_expr;
13pub use color_expr::*;
14pub mod doc_highlight;
15pub use doc_highlight::*;
16pub mod link_expr;
17pub use link_expr::*;
18pub mod stats;
19pub use stats::*;
20pub mod definition;
21pub use definition::*;
22pub mod signature;
23pub use signature::*;
24pub mod semantic_tokens;
25pub use semantic_tokens::*;
26mod post_tyck;
27mod tyck;
28pub(crate) use crate::ty::*;
29pub(crate) use post_tyck::*;
30pub(crate) use tyck::*;
31mod prelude;
32
33mod global;
34pub use global::*;
35
36use std::sync::Arc;
37
38use ecow::eco_format;
39use lsp_types::Url;
40use tinymist_project::LspComputeGraph;
41use tinymist_std::{Result, bail};
42use tinymist_world::{EntryReader, TaskInputs};
43use typst::diag::{FileError, FileResult};
44use typst::foundations::{Func, Value};
45use typst::syntax::FileId;
46
47use crate::{CompilerQueryResponse, SemanticRequest, StatefulRequest, path_res_to_url};
48
49pub(crate) trait ToFunc {
50    fn to_func(&self) -> Option<Func>;
51}
52
53impl ToFunc for Value {
54    fn to_func(&self) -> Option<Func> {
55        match self {
56            Value::Func(func) => Some(func.clone()),
57            Value::Type(ty) => ty.constructor().ok(),
58            _ => None,
59        }
60    }
61}
62
63/// Extension trait for `typst::World`.
64pub trait LspWorldExt {
65    /// Resolve the uri for a file id.
66    fn uri_for_id(&self, fid: FileId) -> FileResult<Url>;
67}
68
69impl LspWorldExt for tinymist_project::LspWorld {
70    fn uri_for_id(&self, fid: FileId) -> Result<Url, FileError> {
71        let res = path_res_to_url(self.path_for_id(fid)?);
72
73        crate::log_debug_ct!("uri_for_id: {fid:?} -> {res:?}");
74        res.map_err(|err| FileError::Other(Some(eco_format!("convert to url: {err:?}"))))
75    }
76}
77
78/// A snapshot for LSP queries.
79pub struct LspQuerySnapshot {
80    /// The using snapshot.
81    pub snap: LspComputeGraph,
82    /// The global shared analysis data.
83    analysis: Arc<Analysis>,
84    /// The revision lock for the analysis (cache).
85    rev_lock: AnalysisRevLock,
86}
87
88impl std::ops::Deref for LspQuerySnapshot {
89    type Target = LspComputeGraph;
90
91    fn deref(&self) -> &Self::Target {
92        &self.snap
93    }
94}
95
96impl LspQuerySnapshot {
97    /// Runs a query for another task.
98    pub fn task(mut self, inputs: TaskInputs) -> Self {
99        self.snap = self.snap.task(inputs);
100        self
101    }
102
103    /// Runs a stateful query.
104    pub fn run_stateful<T: StatefulRequest>(
105        self,
106        query: T,
107        wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
108    ) -> Result<CompilerQueryResponse> {
109        let graph = self.snap.clone();
110        self.run_analysis(|ctx| query.request(ctx, graph))
111            .map(wrapper)
112    }
113
114    /// Runs a semantic query.
115    pub fn run_semantic<T: SemanticRequest>(
116        self,
117        query: T,
118        wrapper: fn(Option<T::Response>) -> CompilerQueryResponse,
119    ) -> Result<CompilerQueryResponse> {
120        self.run_analysis(|ctx| query.request(ctx)).map(wrapper)
121    }
122
123    /// Runs a query.
124    pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> Result<T> {
125        let world = self.snap.world().clone();
126        let Some(..) = world.main_id() else {
127            log::error!("Project: main file is not set");
128            bail!("main file is not set");
129        };
130
131        let mut ctx = self.analysis.enter_(world, self.rev_lock);
132        Ok(f(&mut ctx))
133    }
134}
135
136#[cfg(test)]
137mod matcher_tests {
138
139    use typst::syntax::LinkedNode;
140    use typst_shim::syntax::LinkedNodeExt;
141
142    use crate::{syntax::classify_def, tests::*};
143
144    #[test]
145    fn test() {
146        snapshot_testing("match_def", &|ctx, path| {
147            let source = ctx.source_by_path(&path).unwrap();
148
149            let pos = ctx
150                .to_typst_pos(find_test_position(&source), &source)
151                .unwrap();
152
153            let root = LinkedNode::new(source.root());
154            let node = root.leaf_at_compat(pos).unwrap();
155
156            let snap = classify_def(node).map(|def| format!("{:?}", def.node().range()));
157            let snap = snap.as_deref().unwrap_or("<nil>");
158
159            assert_snapshot!(snap);
160        });
161    }
162}
163
164#[cfg(test)]
165mod expr_tests {
166
167    use tinymist_std::path::unix_slash;
168    use tinymist_world::vfs::WorkspaceResolver;
169    use typst::syntax::Source;
170
171    use crate::syntax::{Expr, RefExpr};
172    use crate::tests::*;
173
174    trait ShowExpr {
175        fn show_expr(&self, expr: &Expr) -> String;
176    }
177
178    impl ShowExpr for Source {
179        fn show_expr(&self, node: &Expr) -> String {
180            match node {
181                Expr::Decl(decl) => {
182                    let range = self.range(decl.span()).unwrap_or_default();
183                    let fid = if let Some(fid) = decl.file_id() {
184                        let vpath = fid.vpath().as_rooted_path();
185                        match fid.package() {
186                            Some(package) if WorkspaceResolver::is_package_file(fid) => {
187                                format!(" in {package:?}{}", unix_slash(vpath))
188                            }
189                            Some(_) | None => format!(" in {}", unix_slash(vpath)),
190                        }
191                    } else {
192                        "".to_string()
193                    };
194                    format!("{decl:?}@{range:?}{fid}")
195                }
196                _ => format!("{node}"),
197            }
198        }
199    }
200
201    #[test]
202    fn docs() {
203        snapshot_testing("docs", &|ctx, path| {
204            let source = ctx.source_by_path(&path).unwrap();
205
206            let result = ctx.shared_().expr_stage(&source);
207            let mut docstrings = result.docstrings.iter().collect::<Vec<_>>();
208            docstrings.sort_by(|x, y| x.0.cmp(y.0));
209            let mut docstrings = docstrings
210                .into_iter()
211                .map(|(ident, expr)| {
212                    format!(
213                        "{} -> {expr:?}",
214                        source.show_expr(&Expr::Decl(ident.clone())),
215                    )
216                })
217                .collect::<Vec<_>>();
218            let mut snap = vec![];
219            snap.push("= docstings".to_owned());
220            snap.append(&mut docstrings);
221
222            assert_snapshot!(snap.join("\n"));
223        });
224    }
225
226    #[test]
227    fn scope() {
228        snapshot_testing("expr_of", &|ctx, path| {
229            let source = ctx.source_by_path(&path).unwrap();
230
231            let result = ctx.shared_().expr_stage(&source);
232            let mut resolves = result.resolves.iter().collect::<Vec<_>>();
233            resolves.sort_by(|x, y| x.1.decl.cmp(&y.1.decl));
234
235            let mut resolves = resolves
236                .into_iter()
237                .map(|(_, expr)| {
238                    let RefExpr {
239                        decl: ident,
240                        step,
241                        root,
242                        term,
243                    } = expr.as_ref();
244
245                    format!(
246                        "{} -> {}, root {}, val: {term:?}",
247                        source.show_expr(&Expr::Decl(ident.clone())),
248                        step.as_ref()
249                            .map(|expr| source.show_expr(expr))
250                            .unwrap_or_default(),
251                        root.as_ref()
252                            .map(|expr| source.show_expr(expr))
253                            .unwrap_or_default()
254                    )
255                })
256                .collect::<Vec<_>>();
257            let mut exports = result.exports.iter().collect::<Vec<_>>();
258            exports.sort_by(|x, y| x.0.cmp(y.0));
259            let mut exports = exports
260                .into_iter()
261                .map(|(ident, node)| {
262                    let node = source.show_expr(node);
263                    format!("{ident} -> {node}",)
264                })
265                .collect::<Vec<_>>();
266
267            let mut snap = vec![];
268            snap.push("= resolves".to_owned());
269            snap.append(&mut resolves);
270            snap.push("= exports".to_owned());
271            snap.append(&mut exports);
272
273            assert_snapshot!(snap.join("\n"));
274        });
275    }
276}
277
278#[cfg(test)]
279mod module_tests {
280    use serde_json::json;
281    use tinymist_std::path::unix_slash;
282    use typst::syntax::FileId;
283
284    use crate::prelude::*;
285    use crate::syntax::module::*;
286    use crate::tests::*;
287
288    #[test]
289    fn test() {
290        snapshot_testing("modules", &|ctx, _| {
291            fn ids(ids: EcoVec<FileId>) -> Vec<String> {
292                let mut ids: Vec<String> = ids
293                    .into_iter()
294                    .map(|id| unix_slash(id.vpath().as_rooted_path()))
295                    .collect();
296                ids.sort();
297                ids
298            }
299
300            let dependencies = construct_module_dependencies(ctx);
301
302            let mut dependencies = dependencies
303                .into_iter()
304                .map(|(id, v)| {
305                    (
306                        unix_slash(id.vpath().as_rooted_path()),
307                        ids(v.dependencies),
308                        ids(v.dependents),
309                    )
310                })
311                .collect::<Vec<_>>();
312
313            dependencies.sort();
314            // remove /main.typ
315            dependencies.retain(|(path, _, _)| path != "/main.typ");
316
317            let dependencies = dependencies
318                .into_iter()
319                .map(|(id, deps, dependents)| {
320                    let mut mp = serde_json::Map::new();
321                    mp.insert("id".to_string(), json!(id));
322                    mp.insert("dependencies".to_string(), json!(deps));
323                    mp.insert("dependents".to_string(), json!(dependents));
324                    json!(mp)
325                })
326                .collect::<Vec<_>>();
327
328            assert_snapshot!(JsonRepr::new_pure(dependencies));
329        });
330    }
331}
332
333#[cfg(test)]
334mod type_check_tests {
335
336    use core::fmt;
337
338    use typst::syntax::Source;
339
340    use crate::tests::*;
341
342    use super::{Ty, TypeInfo};
343
344    #[test]
345    fn test() {
346        snapshot_testing("type_check", &|ctx, path| {
347            let source = ctx.source_by_path(&path).unwrap();
348
349            let result = ctx.type_check(&source);
350            let result = format!("{:#?}", TypeCheckSnapshot(&source, &result));
351
352            assert_snapshot!(result);
353        });
354    }
355
356    struct TypeCheckSnapshot<'a>(&'a Source, &'a TypeInfo);
357
358    impl fmt::Debug for TypeCheckSnapshot<'_> {
359        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360            let source = self.0;
361            let info = self.1;
362            let mut vars = info
363                .vars
364                .values()
365                .map(|bounds| (bounds.name(), bounds))
366                .collect::<Vec<_>>();
367
368            vars.sort_by(|x, y| x.1.var.strict_cmp(&y.1.var));
369
370            for (name, bounds) in vars {
371                writeln!(f, "{name:?} = {:?}", info.simplify(bounds.as_type(), true))?;
372            }
373
374            writeln!(f, "=====")?;
375            let mut mapping = info
376                .mapping
377                .iter()
378                .map(|pair| (source.range(*pair.0).unwrap_or_default(), pair.1))
379                .collect::<Vec<_>>();
380
381            mapping.sort_by(|x, y| {
382                x.0.start
383                    .cmp(&y.0.start)
384                    .then_with(|| x.0.end.cmp(&y.0.end))
385            });
386
387            for (range, value) in mapping {
388                let ty = Ty::from_types(value.clone().into_iter());
389                writeln!(f, "{range:?} -> {ty:?}")?;
390            }
391
392            Ok(())
393        }
394    }
395}
396
397#[cfg(test)]
398mod post_type_check_tests {
399
400    use typst::syntax::LinkedNode;
401    use typst_shim::syntax::LinkedNodeExt;
402
403    use crate::analysis::*;
404    use crate::tests::*;
405
406    #[test]
407    fn test() {
408        snapshot_testing("post_type_check", &|ctx, path| {
409            let source = ctx.source_by_path(&path).unwrap();
410
411            let pos = ctx
412                .to_typst_pos(find_test_position(&source), &source)
413                .unwrap();
414            let root = LinkedNode::new(source.root());
415            let node = root.leaf_at_compat(pos + 1).unwrap();
416            let text = node.get().clone().into_text();
417
418            let result = ctx.type_check(&source);
419            let post_ty = post_type_check(ctx.shared_(), &result, node);
420
421            with_settings!({
422                description => format!("Check on {text:?} ({pos:?})"),
423            }, {
424                let post_ty = post_ty.map(|ty| format!("{ty:#?}"))
425                    .unwrap_or_else(|| "<nil>".to_string());
426                assert_snapshot!(post_ty);
427            })
428        });
429    }
430}
431
432#[cfg(test)]
433mod type_describe_tests {
434
435    use typst::syntax::LinkedNode;
436    use typst_shim::syntax::LinkedNodeExt;
437
438    use crate::analysis::*;
439    use crate::tests::*;
440
441    #[test]
442    fn test() {
443        snapshot_testing("type_describe", &|ctx, path| {
444            let source = ctx.source_by_path(&path).unwrap();
445
446            let pos = ctx
447                .to_typst_pos(find_test_position(&source), &source)
448                .unwrap();
449            let root = LinkedNode::new(source.root());
450            let node = root.leaf_at_compat(pos + 1).unwrap();
451            let text = node.get().clone().into_text();
452
453            let ti = ctx.type_check(&source);
454            let post_ty = post_type_check(ctx.shared_(), &ti, node);
455
456            with_settings!({
457                description => format!("Check on {text:?} ({pos:?})"),
458            }, {
459                let post_ty = post_ty.and_then(|ty| ty.describe())
460                    .unwrap_or_else(|| "<nil>".into());
461                assert_snapshot!(post_ty);
462            })
463        });
464    }
465}
466
467#[cfg(test)]
468mod signature_tests {
469
470    use core::fmt;
471
472    use typst::syntax::LinkedNode;
473    use typst_shim::syntax::LinkedNodeExt;
474
475    use crate::analysis::{Signature, SignatureTarget, analyze_signature};
476    use crate::syntax::classify_syntax;
477    use crate::tests::*;
478
479    #[test]
480    fn test() {
481        snapshot_testing("signature", &|ctx, path| {
482            let source = ctx.source_by_path(&path).unwrap();
483
484            let pos = ctx
485                .to_typst_pos(find_test_position(&source), &source)
486                .unwrap();
487
488            let root = LinkedNode::new(source.root());
489            let callee_node = root.leaf_at_compat(pos).unwrap();
490            let callee_node = classify_syntax(callee_node, pos).unwrap();
491            let callee_node = callee_node.node();
492
493            let result = analyze_signature(
494                ctx.shared(),
495                SignatureTarget::Syntax(source.clone(), callee_node.span()),
496            );
497
498            assert_snapshot!(SignatureSnapshot(result.as_ref()));
499        });
500    }
501
502    struct SignatureSnapshot<'a>(pub Option<&'a Signature>);
503
504    impl fmt::Display for SignatureSnapshot<'_> {
505        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506            let Some(sig) = self.0 else {
507                return write!(f, "<nil>");
508            };
509
510            let primary_sig = match sig {
511                Signature::Primary(sig) => sig,
512                Signature::Partial(sig) => {
513                    for w in &sig.with_stack {
514                        write!(f, "with ")?;
515                        for arg in &w.items {
516                            if let Some(name) = &arg.name {
517                                write!(f, "{name}: ")?;
518                            }
519                            let term = arg.term.as_ref();
520                            let term = term.and_then(|v| v.describe()).unwrap_or_default();
521                            write!(f, "{term}, ")?;
522                        }
523                        f.write_str("\n")?;
524                    }
525
526                    &sig.signature
527                }
528            };
529
530            writeln!(f, "fn(")?;
531            for param in primary_sig.pos() {
532                writeln!(f, " {},", param.name)?;
533            }
534            for param in primary_sig.named() {
535                if let Some(expr) = &param.default {
536                    writeln!(f, " {}: {},", param.name, expr)?;
537                } else {
538                    writeln!(f, " {},", param.name)?;
539                }
540            }
541            if let Some(param) = primary_sig.rest() {
542                writeln!(f, " ...{}, ", param.name)?;
543            }
544            write!(f, ")")?;
545
546            Ok(())
547        }
548    }
549}
550
551#[cfg(test)]
552mod call_info_tests {
553
554    use core::fmt;
555
556    use typst::syntax::{LinkedNode, SyntaxKind};
557    use typst_shim::syntax::LinkedNodeExt;
558
559    use crate::analysis::analyze_call;
560    use crate::tests::*;
561
562    use super::CallInfo;
563
564    #[test]
565    fn test() {
566        snapshot_testing("call_info", &|ctx, path| {
567            let source = ctx.source_by_path(&path).unwrap();
568
569            let pos = ctx
570                .to_typst_pos(find_test_position(&source), &source)
571                .unwrap();
572
573            let root = LinkedNode::new(source.root());
574            let mut call_node = root.leaf_at_compat(pos + 1).unwrap();
575
576            while let Some(parent) = call_node.parent() {
577                if call_node.kind() == SyntaxKind::FuncCall {
578                    break;
579                }
580                call_node = parent.clone();
581            }
582
583            let result = analyze_call(ctx, source.clone(), call_node);
584
585            assert_snapshot!(CallSnapshot(result.as_deref()));
586        });
587    }
588
589    struct CallSnapshot<'a>(pub Option<&'a CallInfo>);
590
591    impl fmt::Display for CallSnapshot<'_> {
592        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593            let Some(ci) = self.0 else {
594                return write!(f, "<nil>");
595            };
596
597            let mut w = ci.arg_mapping.iter().collect::<Vec<_>>();
598            w.sort_by(|x, y| x.0.span().into_raw().cmp(&y.0.span().into_raw()));
599
600            for (arg, arg_call_info) in w {
601                writeln!(f, "{} -> {:?}", arg.clone().into_text(), arg_call_info)?;
602            }
603
604            Ok(())
605        }
606    }
607}
608
609#[cfg(test)]
610mod lint_tests {
611    use std::collections::BTreeMap;
612
613    use crate::tests::*;
614
615    #[test]
616    fn test() {
617        snapshot_testing("lint", &|ctx, path| {
618            let source = ctx.source_by_path(&path).unwrap();
619
620            let result = ctx.lint(&source);
621            let result = crate::diagnostics::DiagWorker::new(ctx).convert_all(result.iter());
622            let result = result
623                .into_iter()
624                .map(|(k, v)| (file_path_(&k), v))
625                .collect::<BTreeMap<_, _>>();
626            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
627        });
628    }
629}