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