tinymist_query/
analysis.rs

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