tinymist_query/
analysis.rs

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