tinymist_query/
package.rs

1//! Package management tools.
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::fmt::{self, Write as _};
6use std::ops::Range;
7use std::path::PathBuf;
8
9use ecow::eco_format;
10#[cfg(feature = "local-registry")]
11use ecow::{EcoVec, eco_vec};
12// use reflexo_typst::typst::prelude::*;
13use rustc_hash::{FxHashMap, FxHashSet};
14use serde::{Deserialize, Serialize};
15use tinymist_world::package::registry::PackageIndexEntry;
16use tinymist_world::package::{PackageSpec, PackageSpecExt};
17use typst::World;
18use typst::diag::{EcoString, StrResult};
19use typst::syntax::package::PackageManifest;
20use typst::syntax::{
21    FileId, LinkedNode, RootedPath, Source, Span, SyntaxKind, VirtualPath, VirtualRoot, ast,
22};
23use typst_shim::syntax::{resolve_path_from_id, source_range};
24
25use crate::LocalContext;
26use crate::analysis::{SharedContext, TypeInfo};
27use crate::syntax::{DeclExpr, Expr, LexicalScope, Pattern, PatternSig};
28use crate::ty::Ty;
29
30/// Information about a package.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PackageInfo {
33    /// The path to the package if any.
34    pub path: PathBuf,
35    /// The namespace the package lives in.
36    pub namespace: EcoString,
37    /// The name of the package within its namespace.
38    pub name: EcoString,
39    /// The package's version.
40    pub version: String,
41}
42
43impl From<PackageIndexEntry> for PackageInfo {
44    fn from(entry: PackageIndexEntry) -> Self {
45        let spec = entry.spec();
46        Self {
47            path: entry.path.unwrap_or_default(),
48            namespace: spec.namespace,
49            name: spec.name,
50            version: spec.version.to_string(),
51        }
52    }
53}
54
55/// Parses a package import from a string literal node in an import statement.
56/// Returns the PackageSpec if it's a valid package import.
57pub fn parse_package_import(node: &LinkedNode) -> Option<PackageSpec> {
58    if !matches!(node.kind(), SyntaxKind::Str) {
59        return None;
60    }
61
62    let import_node = node.parent()?.cast::<ast::ModuleImport>()?;
63
64    let ast::Expr::Str(str_node) = import_node.source() else {
65        return None;
66    };
67    let import_str = str_node.get();
68    if import_str.starts_with('@') {
69        import_str.parse().ok()
70    } else {
71        None
72    }
73}
74
75/// Finds the package entry for a given package spec, and also the latest
76/// version entry.
77pub fn find_package_and_latest<'a>(
78    ctx: &'a SharedContext,
79    package_spec: &PackageSpec,
80) -> (
81    Option<Cow<'a, PackageIndexEntry>>,
82    Option<Cow<'a, PackageIndexEntry>>,
83) {
84    let versionless_spec = package_spec.versionless();
85
86    if package_spec.is_preview() {
87        let packages = ctx.world().packages();
88
89        let current = packages.iter().find(|it| it.matches(package_spec));
90        let latest = packages
91            .iter()
92            .filter(|it| it.matches_versionless(&versionless_spec))
93            .max_by_key(|entry| entry.package.version);
94
95        (current.map(Cow::Borrowed), latest.map(Cow::Borrowed))
96    } else if cfg!(feature = "local-registry") {
97        let local_packages = ctx.non_preview_packages();
98
99        let current = local_packages.iter().find(|it| it.matches(package_spec));
100        let latest = local_packages
101            .iter()
102            .filter(|it| it.matches_versionless(&versionless_spec))
103            .max_by_key(|entry| entry.package.version);
104
105        (
106            current.map(|p| Cow::Owned(p.clone())),
107            latest.map(|p| Cow::Owned(p.clone())),
108        )
109    } else {
110        (None, None)
111    }
112}
113
114/// Parses the manifest of the package located at `package_path`.
115pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
116    Ok(FileId::new(RootedPath::new(
117        VirtualRoot::Package(PackageSpec {
118            namespace: spec.namespace.clone(),
119            name: spec.name.clone(),
120            version: spec.version.parse()?,
121        }),
122        VirtualPath::new("typst.toml").expect("valid manifest path"),
123    )))
124}
125
126/// Parses the manifest of the package located at `package_path`.
127pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
128    let toml_data = world
129        .file(toml_id)
130        .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
131
132    let string = std::str::from_utf8(&toml_data)
133        .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
134
135    toml::from_str(string)
136        .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
137}
138
139pub(crate) fn package_entrypoint_id(manifest_id: FileId, entrypoint: &str) -> FileId {
140    resolve_path_from_id(manifest_id, entrypoint)
141        .expect("valid package entrypoint")
142        .intern()
143}
144
145/// Check Package.
146pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
147    let toml_id = get_manifest_id(spec)?;
148    let manifest = ctx.get_manifest(toml_id)?;
149
150    let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
151
152    ctx.preload_package(entry_point);
153    Ok(())
154}
155
156/// Dumps package scopes together with type-checker results.
157pub fn package_tyck_scope(
158    ctx: &mut LocalContext,
159    spec: &PackageInfo,
160    options: PackageTyckDumpOptions,
161) -> StrResult<PackageTyckDump> {
162    let toml_id = get_manifest_id(spec)?;
163    let manifest = ctx.get_manifest(toml_id)?;
164    let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
165    let files = collect_package_tyck_files(ctx, entry_point, options)?;
166
167    Ok(PackageTyckDump {
168        schema: 1,
169        package: DumpPackageInfo {
170            namespace: spec.namespace.to_string(),
171            name: spec.name.to_string(),
172            version: spec.version.clone(),
173            spec: format!("@{}/{}:{}", spec.namespace, spec.name, spec.version),
174            path: spec.path.to_string_lossy().into_owned(),
175            entrypoint: manifest.package.entrypoint.to_string(),
176        },
177        entrypoint: dump_file_id(entry_point),
178        files,
179    })
180}
181
182fn collect_package_tyck_files(
183    ctx: &mut LocalContext,
184    entry_point: FileId,
185    options: PackageTyckDumpOptions,
186) -> StrResult<Vec<DumpFile>> {
187    let package_root = entry_point.root().clone();
188    let mut files = vec![];
189    let mut seen = FxHashSet::default();
190    let mut queue = VecDeque::from([entry_point]);
191
192    while let Some(fid) = queue.pop_front() {
193        if !seen.insert(fid) {
194            continue;
195        }
196
197        let source = match ctx.source_by_id(fid) {
198            Ok(source) => source,
199            Err(err) if fid == entry_point => {
200                return Err(eco_format!(
201                    "failed to read package entrypoint {fid:?}: {err}"
202                ));
203            }
204            Err(err) => {
205                log::warn!("skipping unreadable package source {fid:?}: {err}");
206                continue;
207            }
208        };
209        let expr_info = ctx.expr_stage(&source);
210        let type_info = ctx.type_check(&source);
211
212        let mut imported_files = expr_info
213            .imports
214            .keys()
215            .copied()
216            .map(dump_file_id)
217            .collect::<Vec<_>>();
218        imported_files.sort_by(|left, right| left.file_id.cmp(&right.file_id));
219
220        for imported in expr_info.imports.keys().copied() {
221            if imported.root() == &package_root && !seen.contains(&imported) {
222                queue.push_back(imported);
223            }
224        }
225
226        files.push(dump_file_scope(
227            &source,
228            &expr_info.exports,
229            &expr_info.root,
230            &type_info,
231            imported_files,
232            options,
233        ));
234    }
235
236    files.sort_by(|left, right| left.file_id.cmp(&right.file_id));
237    Ok(files)
238}
239
240/// Options for dumping package scope and type-checker information.
241#[derive(Debug, Clone, Copy, Default)]
242pub struct PackageTyckDumpOptions {
243    /// Maximum characters kept for each dumped type string.
244    ///
245    /// Set to `None` to keep full type strings. Very large inferred types can
246    /// otherwise make the JSON dump impractical for downstream scripts.
247    pub max_type_chars: Option<usize>,
248}
249
250/// Package scope and type-checker dump.
251#[derive(Debug, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct PackageTyckDump {
254    schema: u32,
255    package: DumpPackageInfo,
256    entrypoint: DumpFileId,
257    files: Vec<DumpFile>,
258}
259
260#[derive(Debug, Serialize)]
261#[serde(rename_all = "camelCase")]
262struct DumpPackageInfo {
263    namespace: String,
264    name: String,
265    version: String,
266    spec: String,
267    path: String,
268    entrypoint: String,
269}
270
271#[derive(Debug, Clone, Serialize)]
272#[serde(rename_all = "camelCase")]
273struct DumpFileId {
274    file_id: String,
275    root: String,
276    path: String,
277}
278
279#[derive(Debug, Serialize)]
280#[serde(rename_all = "camelCase")]
281struct DumpFile {
282    file_id: String,
283    root: String,
284    path: String,
285    imports: Vec<DumpFileId>,
286    scopes: Vec<DumpScope>,
287    type_mappings: Vec<DumpTypeMapping>,
288}
289
290#[derive(Debug, Serialize)]
291#[serde(rename_all = "camelCase")]
292struct DumpScope {
293    kind: &'static str,
294    name: String,
295    declaration: Option<DumpDecl>,
296    variables: Vec<DumpVariable>,
297}
298
299#[derive(Debug, Serialize)]
300#[serde(rename_all = "camelCase")]
301struct DumpVariable {
302    name: String,
303    kind: String,
304    source: &'static str,
305    exported: bool,
306    declaration: DumpDecl,
307    expression: Option<String>,
308    ty: Option<DumpType>,
309}
310
311#[derive(Debug, Clone, Copy)]
312struct DumpVariableOrigin {
313    source: &'static str,
314    exported: bool,
315}
316
317#[derive(Debug, Serialize)]
318#[serde(rename_all = "camelCase")]
319struct DumpDecl {
320    debug: String,
321    kind: String,
322    file_id: Option<String>,
323    range: Option<DumpRange>,
324}
325
326#[derive(Clone, Debug, Serialize)]
327#[serde(rename_all = "camelCase")]
328struct DumpType {
329    debug: EcoString,
330    describe: Option<EcoString>,
331    repr: Option<EcoString>,
332}
333
334struct TypeDumper<'a> {
335    type_info: &'a TypeInfo,
336    options: PackageTyckDumpOptions,
337    cache: FxHashMap<Ty, DumpType>,
338}
339
340impl<'a> TypeDumper<'a> {
341    fn new(type_info: &'a TypeInfo, options: PackageTyckDumpOptions) -> Self {
342        Self {
343            type_info,
344            options,
345            cache: FxHashMap::default(),
346        }
347    }
348
349    fn dump(&mut self, source: Ty) -> DumpType {
350        if let Some(dump) = self.cache.get(&source) {
351            return dump.clone();
352        }
353
354        let ty = self.type_info.simplify(source.clone(), true);
355        let display_ty = if contains_signature_binders(&ty) {
356            self.type_info.simplify(source.clone(), false)
357        } else {
358            ty.clone()
359        };
360        let dump = DumpType {
361            debug: format_debug_dump(&ty, self.options.max_type_chars).into(),
362            describe: display_ty.describe().map(|text| {
363                truncate_dump_string(text.to_string(), self.options.max_type_chars).into()
364            }),
365            repr: display_ty.repr().map(|text| {
366                truncate_dump_string(text.to_string(), self.options.max_type_chars).into()
367            }),
368        };
369        self.cache.insert(source, dump.clone());
370        dump
371    }
372}
373
374#[derive(Debug, Serialize)]
375#[serde(rename_all = "camelCase")]
376struct DumpTypeMapping {
377    range: DumpRange,
378    ty: DumpType,
379}
380
381#[derive(Debug, Serialize)]
382#[serde(rename_all = "camelCase")]
383struct DumpRange {
384    start: usize,
385    end: usize,
386}
387
388fn dump_file_scope(
389    source: &Source,
390    exports: &LexicalScope,
391    root_expr: &Expr,
392    type_info: &TypeInfo,
393    imports: Vec<DumpFileId>,
394    options: PackageTyckDumpOptions,
395) -> DumpFile {
396    let fid = source.id();
397    let file = dump_file_id(fid);
398    let mut types = TypeDumper::new(type_info, options);
399
400    let file_scope = DumpScope {
401        kind: "file",
402        name: file.path.clone(),
403        declaration: None,
404        variables: dump_scope_variables(source, &mut types, exports, "export", true),
405    };
406
407    let mut scopes = vec![file_scope];
408    collect_function_scopes(source, &mut types, root_expr, &mut scopes);
409
410    DumpFile {
411        file_id: file.file_id,
412        root: file.root,
413        path: file.path,
414        imports,
415        scopes,
416        type_mappings: dump_type_mappings(source, &mut types),
417    }
418}
419
420fn dump_scope_variables(
421    source: &Source,
422    types: &mut TypeDumper,
423    scope: &LexicalScope,
424    var_source: &'static str,
425    exported: bool,
426) -> Vec<DumpVariable> {
427    let origin = DumpVariableOrigin {
428        source: var_source,
429        exported,
430    };
431    let mut vars = scope
432        .iter()
433        .filter_map(|(name, expr)| {
434            let decl = expr_decl(expr)?;
435            Some(dump_variable(
436                source,
437                types,
438                name.as_ref(),
439                decl,
440                Some(expr),
441                origin,
442            ))
443        })
444        .collect::<Vec<_>>();
445
446    vars.sort_by(variable_cmp);
447    vars.dedup_by(|left, right| {
448        left.name == right.name && left.declaration.debug == right.declaration.debug
449    });
450    vars
451}
452
453fn collect_function_scopes(
454    source: &Source,
455    types: &mut TypeDumper,
456    expr: &Expr,
457    scopes: &mut Vec<DumpScope>,
458) {
459    if let Expr::Func(func) = expr {
460        let mut variables = vec![];
461        collect_pattern_sig_variables(source, types, &func.params, "parameter", &mut variables);
462        collect_local_variables(source, types, &func.body, &mut variables);
463        variables.sort_by(variable_cmp);
464        variables.dedup_by(|left, right| {
465            left.name == right.name && left.declaration.debug == right.declaration.debug
466        });
467
468        scopes.push(DumpScope {
469            kind: "function",
470            name: scope_name(&func.decl),
471            declaration: Some(dump_decl(source, &func.decl)),
472            variables,
473        });
474
475        collect_function_scopes(source, types, &func.body, scopes);
476        return;
477    }
478
479    walk_expr_children(expr, &mut |child| {
480        collect_function_scopes(source, types, child, scopes);
481    });
482}
483
484fn collect_local_variables(
485    source: &Source,
486    types: &mut TypeDumper,
487    expr: &Expr,
488    variables: &mut Vec<DumpVariable>,
489) {
490    match expr {
491        Expr::Func(_) => {}
492        Expr::Let(let_expr) => {
493            collect_pattern_variables(source, types, &let_expr.pattern, "local", variables);
494            if let Some(body) = &let_expr.body {
495                collect_local_variables(source, types, body, variables);
496            }
497        }
498        Expr::ForLoop(for_loop) => {
499            collect_pattern_variables(source, types, &for_loop.pattern, "local", variables);
500            collect_local_variables(source, types, &for_loop.iter, variables);
501            collect_local_variables(source, types, &for_loop.body, variables);
502        }
503        _ => {
504            walk_expr_children(expr, &mut |child| {
505                collect_local_variables(source, types, child, variables);
506            });
507        }
508    }
509}
510
511fn collect_pattern_sig_variables(
512    source: &Source,
513    types: &mut TypeDumper,
514    sig: &PatternSig,
515    var_source: &'static str,
516    variables: &mut Vec<DumpVariable>,
517) {
518    for pattern in &sig.pos {
519        collect_pattern_variables(source, types, pattern, var_source, variables);
520    }
521    for (decl, pattern) in &sig.named {
522        variables.push(dump_variable(
523            source,
524            types,
525            decl.name().as_ref(),
526            decl,
527            None,
528            DumpVariableOrigin {
529                source: var_source,
530                exported: false,
531            },
532        ));
533        collect_pattern_variables(source, types, pattern, var_source, variables);
534    }
535    for (decl, pattern) in sig.spread_left.iter().chain(sig.spread_right.iter()) {
536        variables.push(dump_variable(
537            source,
538            types,
539            decl.name().as_ref(),
540            decl,
541            None,
542            DumpVariableOrigin {
543                source: var_source,
544                exported: false,
545            },
546        ));
547        collect_pattern_variables(source, types, pattern, var_source, variables);
548    }
549}
550
551fn collect_pattern_variables(
552    source: &Source,
553    types: &mut TypeDumper,
554    pattern: &Pattern,
555    var_source: &'static str,
556    variables: &mut Vec<DumpVariable>,
557) {
558    match pattern {
559        Pattern::Expr(expr) => collect_local_variables(source, types, expr, variables),
560        Pattern::Simple(decl) => {
561            variables.push(dump_variable(
562                source,
563                types,
564                decl.name().as_ref(),
565                decl,
566                None,
567                DumpVariableOrigin {
568                    source: var_source,
569                    exported: false,
570                },
571            ));
572        }
573        Pattern::Sig(sig) => {
574            collect_pattern_sig_variables(source, types, sig, var_source, variables);
575        }
576    }
577}
578
579fn walk_expr_children(expr: &Expr, f: &mut impl FnMut(&Expr)) {
580    match expr {
581        Expr::Block(exprs) => exprs.iter().for_each(f),
582        Expr::Array(args) | Expr::Dict(args) | Expr::Args(args) => {
583            walk_args(args.args.iter(), f);
584        }
585        Expr::Pattern(pattern) => walk_pattern(pattern, f),
586        Expr::Element(elem) => elem.content.iter().for_each(f),
587        Expr::Unary(unary) => f(&unary.lhs),
588        Expr::Binary(binary) => {
589            f(&binary.operands.0);
590            f(&binary.operands.1);
591        }
592        Expr::Apply(apply) => {
593            f(&apply.callee);
594            f(&apply.args);
595        }
596        Expr::Func(func) => {
597            walk_pattern_sig(&func.params, f);
598            f(&func.body);
599        }
600        Expr::Let(let_expr) => {
601            walk_pattern(&let_expr.pattern, f);
602            if let Some(body) = &let_expr.body {
603                f(body);
604            }
605        }
606        Expr::Show(show) => {
607            if let Some(selector) = &show.selector {
608                f(selector);
609            }
610            f(&show.edit);
611        }
612        Expr::Set(set) => {
613            f(&set.target);
614            f(&set.args);
615            if let Some(cond) = &set.cond {
616                f(cond);
617            }
618        }
619        Expr::Ref(ref_expr) => {
620            if let Some(step) = &ref_expr.step {
621                f(step);
622            }
623            if let Some(root) = &ref_expr.root {
624                f(root);
625            }
626        }
627        Expr::ContentRef(content_ref) => {
628            if let Some(body) = &content_ref.body {
629                f(body);
630            }
631        }
632        Expr::Select(select) => f(&select.lhs),
633        Expr::Import(import) => {
634            f(&import.source);
635        }
636        Expr::Include(include) => {
637            f(&include.source);
638        }
639        Expr::Contextual(inner) => f(inner),
640        Expr::Conditional(cond) => {
641            f(&cond.cond);
642            f(&cond.then);
643            f(&cond.else_);
644        }
645        Expr::WhileLoop(while_loop) => {
646            f(&while_loop.cond);
647            f(&while_loop.body);
648        }
649        Expr::ForLoop(for_loop) => {
650            walk_pattern(&for_loop.pattern, f);
651            f(&for_loop.iter);
652            f(&for_loop.body);
653        }
654        Expr::Type(_) | Expr::Decl(_) | Expr::Star => {}
655    }
656}
657
658fn walk_args<'a>(
659    args: impl Iterator<Item = &'a crate::syntax::ArgExpr>,
660    f: &mut impl FnMut(&Expr),
661) {
662    for arg in args {
663        match arg {
664            crate::syntax::ArgExpr::Pos(expr) | crate::syntax::ArgExpr::Spread(expr) => f(expr),
665            crate::syntax::ArgExpr::Named(pair) => f(&pair.1),
666            crate::syntax::ArgExpr::NamedRt(pair) => {
667                f(&pair.0);
668                f(&pair.1);
669            }
670        }
671    }
672}
673
674fn walk_pattern(pattern: &Pattern, f: &mut impl FnMut(&Expr)) {
675    match pattern {
676        Pattern::Expr(expr) => f(expr),
677        Pattern::Simple(_) => {}
678        Pattern::Sig(sig) => walk_pattern_sig(sig, f),
679    }
680}
681
682fn walk_pattern_sig(sig: &PatternSig, f: &mut impl FnMut(&Expr)) {
683    for pattern in &sig.pos {
684        walk_pattern(pattern, f);
685    }
686    for (_, pattern) in &sig.named {
687        walk_pattern(pattern, f);
688    }
689    for (_, pattern) in sig.spread_left.iter().chain(sig.spread_right.iter()) {
690        walk_pattern(pattern, f);
691    }
692}
693
694fn expr_decl(expr: &Expr) -> Option<&DeclExpr> {
695    match expr {
696        Expr::Decl(decl) => Some(decl),
697        Expr::Ref(ref_expr) => ref_expr
698            .root
699            .as_ref()
700            .and_then(expr_decl)
701            .or(Some(&ref_expr.decl)),
702        _ => None,
703    }
704}
705
706fn dump_variable(
707    source: &Source,
708    types: &mut TypeDumper,
709    name: &str,
710    decl: &DeclExpr,
711    expr: Option<&Expr>,
712    origin: DumpVariableOrigin,
713) -> DumpVariable {
714    let ty = types
715        .type_info
716        .vars
717        .get(decl)
718        .map(|bounds| bounds.as_type())
719        .map(|ty| types.dump(ty));
720
721    DumpVariable {
722        name: name.to_owned(),
723        kind: decl.kind().to_string(),
724        source: origin.source,
725        exported: origin.exported,
726        declaration: dump_decl(source, decl),
727        expression: expr.map(ToString::to_string),
728        ty,
729    }
730}
731
732fn dump_decl(source: &Source, decl: &DeclExpr) -> DumpDecl {
733    DumpDecl {
734        debug: format!("{decl:?}"),
735        kind: decl.kind().to_string(),
736        file_id: decl.file_id().map(|fid| dump_file_id(fid).file_id),
737        range: dump_span_range(source, decl.span()),
738    }
739}
740
741fn contains_signature_binders(ty: &Ty) -> bool {
742    contains_signature_binders_inner(ty, &mut FxHashSet::default(), &mut FxHashSet::default())
743}
744
745#[allow(clippy::mutable_key_type)]
746fn contains_signature_binders_inner(
747    ty: &Ty,
748    traversed: &mut FxHashSet<Ty>,
749    type_var_traversed: &mut FxHashSet<Ty>,
750) -> bool {
751    if !traversed.insert(ty.clone()) {
752        return false;
753    }
754
755    match ty {
756        Ty::Func(sig) | Ty::Pattern(sig) => {
757            sig.inputs()
758                .any(|ty| contains_type_var(ty, type_var_traversed))
759                || sig
760                    .inputs()
761                    .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
762                || sig.body.as_ref().is_some_and(|ty| {
763                    contains_signature_binders_inner(ty, traversed, type_var_traversed)
764                })
765        }
766        Ty::Args(sig) => {
767            sig.inputs()
768                .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
769                || sig.body.as_ref().is_some_and(|ty| {
770                    contains_signature_binders_inner(ty, traversed, type_var_traversed)
771                })
772        }
773        Ty::With(with) => {
774            contains_signature_binders_inner(&with.sig, traversed, type_var_traversed)
775                || with
776                    .with
777                    .inputs()
778                    .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed))
779                || with.with.body.as_ref().is_some_and(|ty| {
780                    contains_signature_binders_inner(ty, traversed, type_var_traversed)
781                })
782        }
783        Ty::Param(param) => {
784            contains_signature_binders_inner(&param.ty, traversed, type_var_traversed)
785        }
786        Ty::Union(types) | Ty::Tuple(types) => types
787            .iter()
788            .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
789        Ty::Let(bounds) => bounds
790            .lbs
791            .iter()
792            .chain(&bounds.ubs)
793            .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
794        Ty::Dict(record) => record
795            .types
796            .iter()
797            .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
798        Ty::Array(elem) => contains_signature_binders_inner(elem, traversed, type_var_traversed),
799        Ty::Select(select) => {
800            contains_signature_binders_inner(&select.ty, traversed, type_var_traversed)
801        }
802        Ty::Unary(unary) => {
803            contains_signature_binders_inner(&unary.lhs, traversed, type_var_traversed)
804        }
805        Ty::Binary(binary) => binary
806            .operands()
807            .iter()
808            .any(|ty| contains_signature_binders_inner(ty, traversed, type_var_traversed)),
809        Ty::If(if_ty) => {
810            contains_signature_binders_inner(&if_ty.cond, traversed, type_var_traversed)
811                || contains_signature_binders_inner(&if_ty.then, traversed, type_var_traversed)
812                || contains_signature_binders_inner(&if_ty.else_, traversed, type_var_traversed)
813        }
814        Ty::Var(_) | Ty::Any | Ty::Boolean(_) | Ty::Builtin(_) | Ty::Value(_) => false,
815    }
816}
817
818#[allow(clippy::mutable_key_type)]
819fn contains_type_var(ty: &Ty, traversed: &mut FxHashSet<Ty>) -> bool {
820    if !traversed.insert(ty.clone()) {
821        return false;
822    }
823
824    match ty {
825        Ty::Var(_) => true,
826        Ty::Func(sig) | Ty::Args(sig) | Ty::Pattern(sig) => {
827            sig.inputs().any(|ty| contains_type_var(ty, traversed))
828                || sig
829                    .body
830                    .as_ref()
831                    .is_some_and(|ty| contains_type_var(ty, traversed))
832        }
833        Ty::With(with) => {
834            contains_type_var(&with.sig, traversed)
835                || with
836                    .with
837                    .inputs()
838                    .any(|ty| contains_type_var(ty, traversed))
839                || with
840                    .with
841                    .body
842                    .as_ref()
843                    .is_some_and(|ty| contains_type_var(ty, traversed))
844        }
845        Ty::Param(param) => contains_type_var(&param.ty, traversed),
846        Ty::Union(types) | Ty::Tuple(types) => {
847            types.iter().any(|ty| contains_type_var(ty, traversed))
848        }
849        Ty::Let(bounds) => bounds
850            .lbs
851            .iter()
852            .chain(&bounds.ubs)
853            .any(|ty| contains_type_var(ty, traversed)),
854        Ty::Dict(record) => record
855            .types
856            .iter()
857            .any(|ty| contains_type_var(ty, traversed)),
858        Ty::Array(elem) => contains_type_var(elem, traversed),
859        Ty::Select(select) => contains_type_var(&select.ty, traversed),
860        Ty::Unary(unary) => contains_type_var(&unary.lhs, traversed),
861        Ty::Binary(binary) => binary
862            .operands()
863            .iter()
864            .any(|ty| contains_type_var(ty, traversed)),
865        Ty::If(if_ty) => {
866            contains_type_var(&if_ty.cond, traversed)
867                || contains_type_var(&if_ty.then, traversed)
868                || contains_type_var(&if_ty.else_, traversed)
869        }
870        Ty::Any | Ty::Boolean(_) | Ty::Builtin(_) | Ty::Value(_) => false,
871    }
872}
873
874fn dump_type_mappings(source: &Source, types: &mut TypeDumper) -> Vec<DumpTypeMapping> {
875    let type_info = types.type_info;
876    let mut mappings = type_info
877        .mapping
878        .iter()
879        .filter_map(|(span, mapped_types)| {
880            let range = dump_span_range(source, *span)?;
881            let ty = Ty::from_types(mapped_types.clone().into_iter());
882            Some(DumpTypeMapping {
883                range,
884                ty: types.dump(ty),
885            })
886        })
887        .collect::<Vec<_>>();
888    mappings.sort_by(|left, right| {
889        left.range
890            .start
891            .cmp(&right.range.start)
892            .then_with(|| left.range.end.cmp(&right.range.end))
893    });
894    mappings
895}
896
897struct TruncatingString {
898    text: String,
899    remaining_chars: usize,
900    truncated: bool,
901}
902
903impl fmt::Write for TruncatingString {
904    fn write_str(&mut self, text: &str) -> fmt::Result {
905        if self.remaining_chars == 0 {
906            self.truncated = true;
907            return Err(fmt::Error);
908        }
909
910        if let Some((byte_idx, _)) = text.char_indices().nth(self.remaining_chars) {
911            self.text.push_str(&text[..byte_idx]);
912            self.remaining_chars = 0;
913            self.truncated = true;
914            Err(fmt::Error)
915        } else {
916            self.remaining_chars -= text.chars().count();
917            self.text.push_str(text);
918            Ok(())
919        }
920    }
921}
922
923fn format_debug_dump<T: fmt::Debug>(value: &T, max_chars: Option<usize>) -> String {
924    let Some(max_chars) = max_chars else {
925        return format!("{value:#?}");
926    };
927
928    let mut out = TruncatingString {
929        text: String::new(),
930        remaining_chars: max_chars,
931        truncated: false,
932    };
933    let _ = write!(&mut out, "{value:#?}");
934    if out.truncated {
935        out.text.push_str(" ... truncated ...");
936        out.text.shrink_to_fit();
937    }
938    out.text
939}
940
941fn truncate_dump_string(mut text: String, max_chars: Option<usize>) -> String {
942    let Some(max_chars) = max_chars else {
943        return text;
944    };
945    if let Some((byte_idx, _)) = text.char_indices().nth(max_chars) {
946        text.truncate(byte_idx);
947        text.push_str(" ... truncated ...");
948        text.shrink_to_fit();
949    }
950    text
951}
952
953fn dump_span_range(source: &Source, span: Span) -> Option<DumpRange> {
954    if span.id()? != source.id() {
955        return None;
956    }
957
958    let Range { start, end } = source_range(source, span)?;
959    Some(DumpRange { start, end })
960}
961
962fn dump_file_id(fid: FileId) -> DumpFileId {
963    let root = match fid.root() {
964        VirtualRoot::Project => "$project".to_owned(),
965        VirtualRoot::Package(spec) => spec.to_string(),
966    };
967    let path = fid.vpath().get_without_slash().to_owned();
968    let file_id = if matches!(fid.root(), VirtualRoot::Package(_)) {
969        format!("{root}/{}", fid.vpath().get_without_slash())
970    } else {
971        fid.vpath().get_with_slash().to_owned()
972    };
973
974    DumpFileId {
975        file_id,
976        root,
977        path,
978    }
979}
980
981fn scope_name(decl: &DeclExpr) -> String {
982    let name = decl.name().as_ref();
983    if name.is_empty() {
984        format!("{decl:?}")
985    } else {
986        name.to_owned()
987    }
988}
989
990fn variable_cmp(left: &DumpVariable, right: &DumpVariable) -> std::cmp::Ordering {
991    left.declaration
992        .range
993        .as_ref()
994        .map(|range| (range.start, range.end))
995        .cmp(
996            &right
997                .declaration
998                .range
999                .as_ref()
1000                .map(|range| (range.start, range.end)),
1001        )
1002        .then_with(|| left.name.cmp(&right.name))
1003        .then_with(|| left.kind.cmp(&right.kind))
1004}
1005
1006/// A filter for packages.
1007#[cfg(feature = "local-registry")]
1008pub enum PackageFilter {
1009    /// Filter for packages that match the given namespace.
1010    For(EcoString),
1011    /// Filter for packages that do not match the given namespace.
1012    ExceptFor(EcoString),
1013    /// Filter that matches all packages.
1014    All,
1015}
1016
1017#[cfg(feature = "local-registry")]
1018/// Get the packages in namespaces and their descriptions.
1019pub fn list_package(
1020    world: &tinymist_project::LspWorld,
1021    filter: PackageFilter,
1022) -> EcoVec<PackageIndexEntry> {
1023    trait IsDirFollowLinks {
1024        fn is_dir_follow_links(&self) -> bool;
1025    }
1026
1027    impl IsDirFollowLinks for PathBuf {
1028        fn is_dir_follow_links(&self) -> bool {
1029            // Although `canonicalize` is heavy, we must use it because `symlink_metadata`
1030            // is not reliable.
1031            self.canonicalize()
1032                .map(|meta| meta.is_dir())
1033                .unwrap_or(false)
1034        }
1035    }
1036
1037    let registry = &world.registry;
1038
1039    // search packages locally. We only search in the data
1040    // directory and not the cache directory, because the latter is not
1041    // intended for storage of local packages.
1042    let mut packages = eco_vec![];
1043
1044    let paths = registry.paths();
1045    log::info!("searching for packages in paths {paths:?}");
1046
1047    let mut search_in_dir = |local_path: PathBuf, ns: EcoString| {
1048        if !local_path.exists() || !local_path.is_dir_follow_links() {
1049            return;
1050        }
1051        // namespace/package_name/version
1052        // 2. package_name
1053        let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
1054        else {
1055            return;
1056        };
1057        for package in package_names {
1058            let Some(package) = once_log(package, "read package name") else {
1059                continue;
1060            };
1061            let package_name = EcoString::from(package.file_name().to_string_lossy());
1062            if package_name.starts_with('.') {
1063                continue;
1064            }
1065
1066            let package_path = package.path();
1067            if !package_path.is_dir_follow_links() {
1068                continue;
1069            }
1070            // 3. version
1071            let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
1072            else {
1073                continue;
1074            };
1075            for version in versions {
1076                let Some(version_entry) = once_log(version, "read package version") else {
1077                    continue;
1078                };
1079                if version_entry.file_name().to_string_lossy().starts_with('.') {
1080                    continue;
1081                }
1082                let package_version_path = version_entry.path();
1083                if !package_version_path.is_dir_follow_links() {
1084                    continue;
1085                }
1086                let Some(version) = once_log(
1087                    version_entry.file_name().to_string_lossy().parse(),
1088                    "parse package version",
1089                ) else {
1090                    continue;
1091                };
1092                let spec = PackageSpec {
1093                    namespace: ns.clone(),
1094                    name: package_name.clone(),
1095                    version,
1096                };
1097                let manifest_id = typst::syntax::FileId::new(typst::syntax::RootedPath::new(
1098                    typst::syntax::VirtualRoot::Package(spec.clone()),
1099                    typst::syntax::VirtualPath::new("typst.toml").expect("valid manifest path"),
1100                ));
1101                let Some(manifest) =
1102                    once_log(get_manifest(world, manifest_id), "read package manifest")
1103                else {
1104                    continue;
1105                };
1106                packages.push(PackageIndexEntry {
1107                    namespace: ns.clone(),
1108                    package: manifest.package,
1109                    template: manifest.template,
1110                    updated_at: None,
1111                    path: Some(package_version_path),
1112                });
1113            }
1114        }
1115    };
1116
1117    for dir in paths {
1118        let matching_ns = match &filter {
1119            PackageFilter::For(ns) => {
1120                let local_path = dir.join(ns.as_str());
1121                search_in_dir(local_path, ns.clone());
1122
1123                continue;
1124            }
1125            PackageFilter::ExceptFor(ns) => Some(ns),
1126            PackageFilter::All => None,
1127        };
1128
1129        let Some(namespaces) = once_log(std::fs::read_dir(dir), "read package directory") else {
1130            continue;
1131        };
1132        for dir in namespaces {
1133            let Some(dir) = once_log(dir, "read ns directory") else {
1134                continue;
1135            };
1136            let ns = dir.file_name();
1137            let ns = ns.to_string_lossy();
1138            if let Some(matching_ns) = &matching_ns
1139                && matching_ns.as_str() == ns.as_ref()
1140            {
1141                continue;
1142            }
1143            let local_path = dir.path();
1144            search_in_dir(local_path, ns.into());
1145        }
1146    }
1147
1148    packages
1149}
1150
1151#[cfg(feature = "local-registry")]
1152fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
1153    use std::collections::HashSet;
1154    use std::sync::OnceLock;
1155
1156    use parking_lot::Mutex;
1157
1158    let err = match result {
1159        Ok(value) => return Some(value),
1160        Err(err) => err,
1161    };
1162
1163    static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
1164    let mut once = ONCE.get_or_init(Default::default).lock();
1165    if once.insert(site) {
1166        log::error!("failed to perform {site}: {err}");
1167    }
1168
1169    None
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use std::str::FromStr;
1175
1176    use typst::syntax::package::PackageSpec;
1177
1178    use super::*;
1179    use crate::syntax::Decl;
1180    use crate::tests::{run_with_ctx, run_with_sources};
1181    use crate::ty::{SigTy, TypeVar};
1182
1183    fn manifest_id() -> FileId {
1184        FileId::new(RootedPath::new(
1185            VirtualRoot::Package(
1186                PackageSpec::from_str("@preview/example:0.1.0").expect("valid package spec"),
1187            ),
1188            VirtualPath::new("typst.toml").expect("valid manifest path"),
1189        ))
1190    }
1191
1192    #[test]
1193    fn package_entrypoint_id_resolves_relative_to_manifest_parent() {
1194        let manifest_id = manifest_id();
1195        let entrypoint = package_entrypoint_id(manifest_id, "src/lib.typ");
1196
1197        assert_eq!(entrypoint.root(), manifest_id.root());
1198        assert_eq!(entrypoint.vpath().get_with_slash(), "/src/lib.typ");
1199    }
1200
1201    #[test]
1202    fn package_entrypoint_id_resolves_absolute_path_in_package_root() {
1203        let manifest_id = manifest_id();
1204        let entrypoint = package_entrypoint_id(manifest_id, "/lib.typ");
1205
1206        assert_eq!(entrypoint.root(), manifest_id.root());
1207        assert_eq!(entrypoint.vpath().get_with_slash(), "/lib.typ");
1208    }
1209
1210    #[test]
1211    #[allow(clippy::mutable_key_type)]
1212    fn signature_binder_detection_visits_shared_type_once() {
1213        const DEPTH: usize = 16;
1214
1215        let mut shared = Ty::Any;
1216        for _ in 0..DEPTH {
1217            shared = Ty::Tuple(vec![shared.clone(), shared].into());
1218        }
1219        let mut traversed = FxHashSet::default();
1220        assert!(!contains_signature_binders_inner(
1221            &shared,
1222            &mut traversed,
1223            &mut FxHashSet::default(),
1224        ));
1225        assert_eq!(traversed.len(), DEPTH + 1);
1226
1227        let binder = TypeVar::new("input".into(), Decl::lit("input").into());
1228        let binder_ty = Ty::Var(binder);
1229        assert!(contains_signature_binders(&Ty::Func(SigTy::unary(
1230            binder_ty,
1231            Ty::Any,
1232        ))));
1233    }
1234
1235    #[test]
1236    fn type_dumper_reuses_dumped_types() {
1237        let info = TypeInfo::default();
1238        let mut dumper = TypeDumper::new(
1239            &info,
1240            PackageTyckDumpOptions {
1241                max_type_chars: Some(128),
1242            },
1243        );
1244
1245        let first = dumper.dump(Ty::Any);
1246        let second = dumper.dump(Ty::Any);
1247
1248        assert_eq!(dumper.cache.len(), 1);
1249        assert_eq!(first.debug, second.debug);
1250        assert_eq!(first.describe, second.describe);
1251        assert_eq!(first.repr, second.repr);
1252    }
1253
1254    #[test]
1255    #[allow(clippy::mutable_key_type)]
1256    fn principal_dump_preserves_type_guard_branches() {
1257        run_with_sources(
1258            r#"
1259#let auto-cast(pat) = {
1260  if type(pat) == dictionary {
1261    pat
1262  } else if type(pat) == array {
1263    pat.map(auto-cast)
1264  }
1265}
1266"#,
1267            |verse, path| {
1268                run_with_ctx(verse, path, &|ctx, path| {
1269                    let source = ctx.source_by_path(&path).unwrap();
1270                    let info = ctx.type_check(&source);
1271                    let mapped = info
1272                        .mapping
1273                        .iter()
1274                        .find_map(|(span, mapped)| {
1275                            let range = source_range(&source, *span)?;
1276                            (source.text().get(range) == Some("pat.map")).then_some(mapped)
1277                        })
1278                        .expect("pat.map must have a mapped type");
1279                    let source_ty = Ty::from_types(mapped.clone().into_iter());
1280                    let mut dumper = TypeDumper::new(
1281                        &info,
1282                        PackageTyckDumpOptions {
1283                            max_type_chars: Some(1024),
1284                        },
1285                    );
1286                    let dumped = dumper.dump(source_ty);
1287
1288                    assert!(dumped.debug.contains("Type(array)"));
1289                    assert!(dumped.debug.contains("Type(dictionary)"));
1290                    assert!(dumped.debug.contains(".map"));
1291                });
1292            },
1293        );
1294    }
1295
1296    #[test]
1297    fn package_tyck_files_skip_unreadable_imports() {
1298        run_with_sources(
1299            r#"
1300// path: present.typ
1301#let present = true
1302-----
1303// path: main.typ
1304#import "present.typ"
1305#import "missing.typ"
1306"#,
1307            |verse, path| {
1308                run_with_ctx(verse, path, &|ctx, path| {
1309                    let entrypoint = ctx.source_by_path(&path).unwrap().id();
1310                    let files = collect_package_tyck_files(ctx, entrypoint, Default::default())
1311                        .expect("missing imports should not abort a package scan");
1312
1313                    assert_eq!(
1314                        files
1315                            .iter()
1316                            .map(|file| file.path.as_str())
1317                            .collect::<Vec<_>>(),
1318                        ["main.typ", "present.typ"]
1319                    );
1320                    let main = files
1321                        .iter()
1322                        .find(|file| file.path == "main.typ")
1323                        .expect("entrypoint must be dumped");
1324                    assert_eq!(
1325                        main.imports
1326                            .iter()
1327                            .map(|import| import.path.as_str())
1328                            .collect::<Vec<_>>(),
1329                        ["missing.typ", "present.typ"]
1330                    );
1331                });
1332            },
1333        );
1334    }
1335}