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::*;
34
35use std::sync::Arc;
36
37use ecow::eco_format;
38use lsp_types::Url;
39use tinymist_project::LspComputeGraph;
40use tinymist_std::error::WithContextUntyped;
41use tinymist_std::{Result, bail};
42use tinymist_world::{EntryReader, EntryState, TaskInputs};
43use typst::diag::{FileError, FileResult, StrResult};
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    /// Checks within package
136    pub fn run_within_package<T>(
137        self,
138        info: &crate::package::PackageInfo,
139        f: impl FnOnce(&mut LocalContextGuard) -> Result<T> + Send + Sync,
140    ) -> Result<T> {
141        let world = self.world();
142
143        let entry: StrResult<EntryState> = Ok(()).and_then(|_| {
144            let toml_id = crate::package::get_manifest_id(info)?;
145            let toml_path = world.path_for_id(toml_id)?.as_path().to_owned();
146            let pkg_root = toml_path
147                .parent()
148                .ok_or_else(|| eco_format!("cannot get package root (parent of {toml_path:?})"))?;
149
150            let manifest = crate::package::get_manifest(world, toml_id)?;
151            let entry_point = toml_id.join(&manifest.package.entrypoint);
152
153            Ok(EntryState::new_rooted_by_id(pkg_root.into(), entry_point))
154        });
155        let entry = entry.context_ut("resolve package entry")?;
156
157        let snap = self.task(TaskInputs {
158            entry: Some(entry),
159            inputs: None,
160        });
161
162        snap.run_analysis(f)?
163    }
164}
165
166#[cfg(test)]
167mod matcher_tests {
168
169    use typst::syntax::LinkedNode;
170    use typst_shim::syntax::LinkedNodeExt;
171
172    use crate::{syntax::classify_def, tests::*};
173
174    #[test]
175    fn test() {
176        snapshot_testing("match_def", &|ctx, path| {
177            let source = ctx.source_by_path(&path).unwrap();
178
179            let pos = ctx
180                .to_typst_pos(find_test_position(&source), &source)
181                .unwrap();
182
183            let root = LinkedNode::new(source.root());
184            let node = root.leaf_at_compat(pos).unwrap();
185
186            let snap = classify_def(node).map(|def| format!("{:?}", def.node().range()));
187            let snap = snap.as_deref().unwrap_or("<nil>");
188
189            assert_snapshot!(snap);
190        });
191    }
192}
193
194#[cfg(test)]
195mod expr_tests {
196
197    use tinymist_std::path::unix_slash;
198    use tinymist_world::vfs::WorkspaceResolver;
199    use typst::syntax::Source;
200
201    use crate::syntax::{Expr, RefExpr};
202    use crate::tests::*;
203
204    trait ShowExpr {
205        fn show_expr(&self, expr: &Expr) -> String;
206    }
207
208    impl ShowExpr for Source {
209        fn show_expr(&self, node: &Expr) -> String {
210            match node {
211                Expr::Decl(decl) => {
212                    let range = self.range(decl.span()).unwrap_or_default();
213                    let fid = if let Some(fid) = decl.file_id() {
214                        let vpath = fid.vpath().as_rooted_path();
215                        match fid.package() {
216                            Some(package) if WorkspaceResolver::is_package_file(fid) => {
217                                format!(" in {package:?}{}", unix_slash(vpath))
218                            }
219                            Some(_) | None => format!(" in {}", unix_slash(vpath)),
220                        }
221                    } else {
222                        "".to_string()
223                    };
224                    format!("{decl:?}@{range:?}{fid}")
225                }
226                _ => format!("{node}"),
227            }
228        }
229    }
230
231    #[test]
232    fn docs() {
233        snapshot_testing("docs", &|ctx, path| {
234            let source = ctx.source_by_path(&path).unwrap();
235
236            let result = ctx.shared_().expr_stage(&source);
237            let mut docstrings = result.docstrings.iter().collect::<Vec<_>>();
238            docstrings.sort_by(|x, y| x.0.cmp(y.0));
239            let mut docstrings = docstrings
240                .into_iter()
241                .map(|(ident, expr)| {
242                    format!(
243                        "{} -> {expr:?}",
244                        source.show_expr(&Expr::Decl(ident.clone())),
245                    )
246                })
247                .collect::<Vec<_>>();
248            let mut snap = vec![];
249            snap.push("= docstings".to_owned());
250            snap.append(&mut docstrings);
251
252            assert_snapshot!(snap.join("\n"));
253        });
254    }
255
256    #[test]
257    fn scope() {
258        snapshot_testing("expr_of", &|ctx, path| {
259            let source = ctx.source_by_path(&path).unwrap();
260
261            let result = ctx.shared_().expr_stage(&source);
262            let mut resolves = result.resolves.iter().collect::<Vec<_>>();
263            resolves.sort_by(|x, y| x.1.decl.cmp(&y.1.decl));
264
265            let mut resolves = resolves
266                .into_iter()
267                .map(|(_, expr)| {
268                    let RefExpr {
269                        decl: ident,
270                        step,
271                        root,
272                        term,
273                    } = expr.as_ref();
274
275                    format!(
276                        "{} -> {}, root {}, val: {term:?}",
277                        source.show_expr(&Expr::Decl(ident.clone())),
278                        step.as_ref()
279                            .map(|expr| source.show_expr(expr))
280                            .unwrap_or_default(),
281                        root.as_ref()
282                            .map(|expr| source.show_expr(expr))
283                            .unwrap_or_default()
284                    )
285                })
286                .collect::<Vec<_>>();
287            let mut exports = result.exports.iter().collect::<Vec<_>>();
288            exports.sort_by(|x, y| x.0.cmp(y.0));
289            let mut exports = exports
290                .into_iter()
291                .map(|(ident, node)| {
292                    let node = source.show_expr(node);
293                    format!("{ident} -> {node}",)
294                })
295                .collect::<Vec<_>>();
296
297            let mut snap = vec![];
298            snap.push("= resolves".to_owned());
299            snap.append(&mut resolves);
300            snap.push("= exports".to_owned());
301            snap.append(&mut exports);
302
303            assert_snapshot!(snap.join("\n"));
304        });
305    }
306}
307
308#[cfg(test)]
309mod module_tests {
310    use serde_json::json;
311    use tinymist_std::path::unix_slash;
312    use typst::syntax::FileId;
313
314    use crate::prelude::*;
315    use crate::syntax::module::*;
316    use crate::tests::*;
317
318    #[test]
319    fn test() {
320        snapshot_testing("modules", &|ctx, _| {
321            fn ids(ids: EcoVec<FileId>) -> Vec<String> {
322                let mut ids: Vec<String> = ids
323                    .into_iter()
324                    .map(|id| unix_slash(id.vpath().as_rooted_path()))
325                    .collect();
326                ids.sort();
327                ids
328            }
329
330            let dependencies = construct_module_dependencies(ctx);
331
332            let mut dependencies = dependencies
333                .into_iter()
334                .map(|(id, v)| {
335                    (
336                        unix_slash(id.vpath().as_rooted_path()),
337                        ids(v.dependencies),
338                        ids(v.dependents),
339                    )
340                })
341                .collect::<Vec<_>>();
342
343            dependencies.sort();
344            // remove /main.typ
345            dependencies.retain(|(path, _, _)| path != "/main.typ");
346
347            let dependencies = dependencies
348                .into_iter()
349                .map(|(id, deps, dependents)| {
350                    let mut mp = serde_json::Map::new();
351                    mp.insert("id".to_string(), json!(id));
352                    mp.insert("dependencies".to_string(), json!(deps));
353                    mp.insert("dependents".to_string(), json!(dependents));
354                    json!(mp)
355                })
356                .collect::<Vec<_>>();
357
358            assert_snapshot!(JsonRepr::new_pure(dependencies));
359        });
360    }
361}
362
363#[cfg(test)]
364mod type_check_tests {
365
366    use core::fmt;
367
368    use typst::syntax::Source;
369
370    use crate::tests::*;
371
372    use super::{Ty, TypeInfo};
373
374    #[test]
375    fn test() {
376        snapshot_testing("type_check", &|ctx, path| {
377            let source = ctx.source_by_path(&path).unwrap();
378
379            let result = ctx.type_check(&source);
380            let result = format!("{:#?}", TypeCheckSnapshot(&source, &result));
381
382            assert_snapshot!(result);
383        });
384    }
385
386    struct TypeCheckSnapshot<'a>(&'a Source, &'a TypeInfo);
387
388    impl fmt::Debug for TypeCheckSnapshot<'_> {
389        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390            let source = self.0;
391            let info = self.1;
392            let mut vars = info
393                .vars
394                .values()
395                .map(|bounds| (bounds.name(), bounds))
396                .collect::<Vec<_>>();
397
398            vars.sort_by(|x, y| x.1.var.strict_cmp(&y.1.var));
399
400            for (name, bounds) in vars {
401                writeln!(f, "{name:?} = {:?}", info.simplify(bounds.as_type(), true))?;
402            }
403
404            writeln!(f, "=====")?;
405            let mut mapping = info
406                .mapping
407                .iter()
408                .map(|pair| (source.range(*pair.0).unwrap_or_default(), pair.1))
409                .collect::<Vec<_>>();
410
411            mapping.sort_by(|x, y| {
412                x.0.start
413                    .cmp(&y.0.start)
414                    .then_with(|| x.0.end.cmp(&y.0.end))
415            });
416
417            for (range, value) in mapping {
418                let ty = Ty::from_types(value.clone().into_iter());
419                writeln!(f, "{range:?} -> {ty:?}")?;
420            }
421
422            Ok(())
423        }
424    }
425}
426
427#[cfg(test)]
428mod post_type_check_tests {
429
430    use typst::syntax::LinkedNode;
431    use typst_shim::syntax::LinkedNodeExt;
432
433    use crate::analysis::*;
434    use crate::tests::*;
435
436    #[test]
437    fn test() {
438        snapshot_testing("post_type_check", &|ctx, path| {
439            let source = ctx.source_by_path(&path).unwrap();
440
441            let pos = ctx
442                .to_typst_pos(find_test_position(&source), &source)
443                .unwrap();
444            let root = LinkedNode::new(source.root());
445            let node = root.leaf_at_compat(pos + 1).unwrap();
446            let text = node.get().clone().into_text();
447
448            let result = ctx.type_check(&source);
449            let post_ty = post_type_check(ctx.shared_(), &result, node);
450
451            with_settings!({
452                description => format!("Check on {text:?} ({pos:?})"),
453            }, {
454                let post_ty = post_ty.map(|ty| format!("{ty:#?}"))
455                    .unwrap_or_else(|| "<nil>".to_string());
456                assert_snapshot!(post_ty);
457            })
458        });
459    }
460}
461
462#[cfg(test)]
463mod type_describe_tests {
464
465    use typst::syntax::LinkedNode;
466    use typst_shim::syntax::LinkedNodeExt;
467
468    use crate::analysis::*;
469    use crate::tests::*;
470
471    #[test]
472    fn test() {
473        snapshot_testing("type_describe", &|ctx, path| {
474            let source = ctx.source_by_path(&path).unwrap();
475
476            let pos = ctx
477                .to_typst_pos(find_test_position(&source), &source)
478                .unwrap();
479            let root = LinkedNode::new(source.root());
480            let node = root.leaf_at_compat(pos + 1).unwrap();
481            let text = node.get().clone().into_text();
482
483            let ti = ctx.type_check(&source);
484            let post_ty = post_type_check(ctx.shared_(), &ti, node);
485
486            with_settings!({
487                description => format!("Check on {text:?} ({pos:?})"),
488            }, {
489                let post_ty = post_ty.and_then(|ty| ty.describe())
490                    .unwrap_or_else(|| "<nil>".into());
491                assert_snapshot!(post_ty);
492            })
493        });
494    }
495}
496
497#[cfg(test)]
498mod signature_tests {
499
500    use core::fmt;
501
502    use typst::syntax::LinkedNode;
503    use typst_shim::syntax::LinkedNodeExt;
504
505    use crate::analysis::{Signature, SignatureTarget, analyze_signature};
506    use crate::syntax::classify_syntax;
507    use crate::tests::*;
508
509    #[test]
510    fn test() {
511        snapshot_testing("signature", &|ctx, path| {
512            let source = ctx.source_by_path(&path).unwrap();
513
514            let pos = ctx
515                .to_typst_pos(find_test_position(&source), &source)
516                .unwrap();
517
518            let root = LinkedNode::new(source.root());
519            let callee_node = root.leaf_at_compat(pos).unwrap();
520            let callee_node = classify_syntax(callee_node, pos).unwrap();
521            let callee_node = callee_node.node();
522
523            let result = analyze_signature(
524                ctx.shared(),
525                SignatureTarget::Syntax(source.clone(), callee_node.span()),
526            );
527
528            assert_snapshot!(SignatureSnapshot(result.as_ref()));
529        });
530    }
531
532    struct SignatureSnapshot<'a>(pub Option<&'a Signature>);
533
534    impl fmt::Display for SignatureSnapshot<'_> {
535        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
536            let Some(sig) = self.0 else {
537                return write!(f, "<nil>");
538            };
539
540            let primary_sig = match sig {
541                Signature::Primary(sig) => sig,
542                Signature::Partial(sig) => {
543                    for w in &sig.with_stack {
544                        write!(f, "with ")?;
545                        for arg in &w.items {
546                            if let Some(name) = &arg.name {
547                                write!(f, "{name}: ")?;
548                            }
549                            let term = arg.term.as_ref();
550                            let term = term.and_then(|v| v.describe()).unwrap_or_default();
551                            write!(f, "{term}, ")?;
552                        }
553                        f.write_str("\n")?;
554                    }
555
556                    &sig.signature
557                }
558            };
559
560            writeln!(f, "fn(")?;
561            for param in primary_sig.pos() {
562                writeln!(f, " {},", param.name)?;
563            }
564            for param in primary_sig.named() {
565                if let Some(expr) = &param.default {
566                    writeln!(f, " {}: {},", param.name, expr)?;
567                } else {
568                    writeln!(f, " {},", param.name)?;
569                }
570            }
571            if let Some(param) = primary_sig.rest() {
572                writeln!(f, " ...{}, ", param.name)?;
573            }
574            write!(f, ")")?;
575
576            Ok(())
577        }
578    }
579}
580
581#[cfg(test)]
582mod call_info_tests {
583
584    use core::fmt;
585
586    use typst::syntax::{LinkedNode, SyntaxKind};
587    use typst_shim::syntax::LinkedNodeExt;
588
589    use crate::analysis::analyze_call;
590    use crate::tests::*;
591
592    use super::CallInfo;
593
594    #[test]
595    fn test() {
596        snapshot_testing("call_info", &|ctx, path| {
597            let source = ctx.source_by_path(&path).unwrap();
598
599            let pos = ctx
600                .to_typst_pos(find_test_position(&source), &source)
601                .unwrap();
602
603            let root = LinkedNode::new(source.root());
604            let mut call_node = root.leaf_at_compat(pos + 1).unwrap();
605
606            while let Some(parent) = call_node.parent() {
607                if call_node.kind() == SyntaxKind::FuncCall {
608                    break;
609                }
610                call_node = parent.clone();
611            }
612
613            let result = analyze_call(ctx, source.clone(), call_node);
614
615            assert_snapshot!(CallSnapshot(result.as_deref()));
616        });
617    }
618
619    struct CallSnapshot<'a>(pub Option<&'a CallInfo>);
620
621    impl fmt::Display for CallSnapshot<'_> {
622        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623            let Some(ci) = self.0 else {
624                return write!(f, "<nil>");
625            };
626
627            let mut w = ci.arg_mapping.iter().collect::<Vec<_>>();
628            w.sort_by(|x, y| x.0.span().into_raw().cmp(&y.0.span().into_raw()));
629
630            for (arg, arg_call_info) in w {
631                writeln!(f, "{} -> {:?}", arg.clone().into_text(), arg_call_info)?;
632            }
633
634            Ok(())
635        }
636    }
637}
638
639#[cfg(test)]
640mod lint_tests {
641    use std::collections::BTreeMap;
642
643    use tinymist_lint::KnownIssues;
644
645    use crate::tests::*;
646
647    #[test]
648    fn test() {
649        snapshot_testing("lint", &|ctx, path| {
650            let source = ctx.source_by_path(&path).unwrap();
651
652            let result = ctx.lint(&source, &KnownIssues::default());
653            let result = crate::diagnostics::DiagWorker::new(ctx).convert_all(result.iter());
654            let result = result
655                .into_iter()
656                .map(|(k, v)| (file_path_(&k), v))
657                .collect::<BTreeMap<_, _>>();
658            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
659        });
660    }
661}