Skip to main content

presolve_compiler/
binding_table.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use crate::compilation_unit::CompilationUnit;
5use crate::module_graph::{ModuleEdgeKind, ModuleGraph, ModuleTarget};
6use crate::semantic_package::{
7    SemanticPackageFormSubmission, SemanticPackageKind, SemanticPackageOpaqueTerminal,
8    SemanticPackagePureOperation, SemanticPackageResolutionTable, SemanticPackageResourceEndpoint,
9    SemanticPackageRouteLoader, SemanticPackageServerAction,
10};
11use crate::symbol_table::{ModuleSymbol, SymbolKind, SymbolTable};
12
13/// Resolved local exports and relative-module imports.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct BindingTable {
16    pub modules: Vec<ModuleBindingTable>,
17    pub diagnostics: Vec<BindingDiagnostic>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ModuleBindingTable {
22    pub path: PathBuf,
23    pub exports: BTreeMap<String, ExportBinding>,
24    pub imports: BTreeMap<String, ImportBinding>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ExportBinding {
29    pub exported_name: String,
30    pub local_name: String,
31    pub symbol: ModuleSymbol,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ImportBinding {
36    pub local_name: String,
37    pub imported_name: String,
38    pub source_module: PathBuf,
39    pub target: ImportBindingTarget,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ImportBindingTarget {
44    Symbol(ModuleSymbol),
45    Namespace {
46        module: PathBuf,
47        exports: BTreeMap<String, ModuleSymbol>,
48    },
49    SemanticPackage {
50        package: String,
51        version: String,
52        integrity: String,
53        export: String,
54        kind: SemanticPackageKind,
55        type_signature: String,
56        runtime_module: String,
57        resume_policy: String,
58        pure_operation: Option<SemanticPackagePureOperation>,
59        resource_endpoint: Option<SemanticPackageResourceEndpoint>,
60        route_loader: Option<SemanticPackageRouteLoader>,
61        server_action: Option<SemanticPackageServerAction>,
62        form_submission: Option<SemanticPackageFormSubmission>,
63        opaque_terminal: Option<SemanticPackageOpaqueTerminal>,
64    },
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct BindingDiagnostic {
69    pub code: String,
70    pub module: PathBuf,
71    pub name: String,
72    pub message: String,
73}
74
75impl BindingTable {
76    #[must_use]
77    pub fn module(&self, path: impl AsRef<Path>) -> Option<&ModuleBindingTable> {
78        self.modules
79            .iter()
80            .find(|module| module.path == path.as_ref())
81    }
82
83    #[must_use]
84    pub fn resolve_import(
85        &self,
86        module_path: impl AsRef<Path>,
87        local_name: &str,
88    ) -> Option<&ImportBinding> {
89        self.module(module_path)
90            .and_then(|module| module.imports.get(local_name))
91    }
92}
93
94#[must_use]
95pub fn build_binding_table(
96    unit: &CompilationUnit,
97    symbols: &SymbolTable,
98    modules: &ModuleGraph,
99) -> BindingTable {
100    build_binding_table_with_packages(
101        unit,
102        symbols,
103        modules,
104        &SemanticPackageResolutionTable::default(),
105    )
106}
107
108#[must_use]
109pub fn build_binding_table_with_packages(
110    unit: &CompilationUnit,
111    symbols: &SymbolTable,
112    modules: &ModuleGraph,
113    packages: &SemanticPackageResolutionTable,
114) -> BindingTable {
115    let mut tables = unit
116        .files()
117        .iter()
118        .map(|file| ModuleBindingTable {
119            path: file.path.clone(),
120            exports: BTreeMap::new(),
121            imports: BTreeMap::new(),
122        })
123        .collect::<Vec<_>>();
124    let mut diagnostics = Vec::new();
125
126    collect_local_exports(unit, symbols, &mut tables, &mut diagnostics);
127    resolve_relative_reexports(unit, modules, &mut tables, &mut diagnostics);
128    collect_identity_collisions(symbols, &mut diagnostics);
129
130    let exports_by_path = tables
131        .iter()
132        .map(|table| (table.path.clone(), table.exports.clone()))
133        .collect::<BTreeMap<_, _>>();
134
135    resolve_relative_imports(
136        unit,
137        modules,
138        &exports_by_path,
139        &mut tables,
140        &mut diagnostics,
141        packages,
142    );
143
144    BindingTable {
145        modules: tables,
146        diagnostics,
147    }
148}
149
150fn collect_local_exports(
151    unit: &CompilationUnit,
152    symbols: &SymbolTable,
153    tables: &mut [ModuleBindingTable],
154    diagnostics: &mut Vec<BindingDiagnostic>,
155) {
156    for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
157        let Some(module_symbols) = symbols.module(&file.path) else {
158            continue;
159        };
160
161        for export in file.exports.iter().filter(|export| export.source.is_none()) {
162            for specifier in &export.specifiers {
163                let Some(local_name) = &specifier.local else {
164                    diagnostics.push(BindingDiagnostic {
165                        code: "PSBIND1001".to_string(),
166                        module: file.path.clone(),
167                        name: specifier.exported.clone(),
168                        message: format!(
169                            "export `{}` has no local declaration to bind",
170                            specifier.exported
171                        ),
172                    });
173                    continue;
174                };
175                let Some(symbol) = module_symbols.symbols.get(local_name) else {
176                    diagnostics.push(BindingDiagnostic {
177                        code: "PSBIND1001".to_string(),
178                        module: file.path.clone(),
179                        name: specifier.exported.clone(),
180                        message: format!(
181                            "export `{}` refers to unsupported or missing local symbol `{local_name}`",
182                            specifier.exported
183                        ),
184                    });
185                    continue;
186                };
187
188                insert_export(
189                    table,
190                    diagnostics,
191                    ExportBinding {
192                        exported_name: specifier.exported.clone(),
193                        local_name: local_name.clone(),
194                        symbol: symbol.clone(),
195                    },
196                );
197            }
198        }
199    }
200}
201
202fn resolve_relative_imports(
203    unit: &CompilationUnit,
204    modules: &ModuleGraph,
205    exports_by_path: &BTreeMap<PathBuf, BTreeMap<String, ExportBinding>>,
206    tables: &mut [ModuleBindingTable],
207    diagnostics: &mut Vec<BindingDiagnostic>,
208    packages: &SemanticPackageResolutionTable,
209) {
210    for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
211        for import in &file.imports {
212            let target = module_target(modules, &file.path, import.span, ModuleEdgeKind::Import);
213            if matches!(target, ModuleTarget::External) {
214                resolve_semantic_package_import(import, packages, table, diagnostics, &file.path);
215                continue;
216            }
217            let ModuleTarget::Resolved(target_path) = target else {
218                if matches!(target, ModuleTarget::Unresolved) {
219                    diagnostics.push(BindingDiagnostic {
220                        code: "PSBIND1002".to_string(),
221                        module: file.path.clone(),
222                        name: import.source.clone(),
223                        message: format!(
224                            "relative import `{}` does not resolve to a module",
225                            import.source
226                        ),
227                    });
228                }
229                continue;
230            };
231            let target_exports = exports_by_path
232                .get(&target_path)
233                .cloned()
234                .unwrap_or_default();
235
236            for specifier in &import.specifiers {
237                let binding = resolve_import_binding(specifier, &target_path, &target_exports);
238
239                let Some(binding) = binding else {
240                    diagnostics.push(BindingDiagnostic {
241                        code: "PSBIND1003".to_string(),
242                        module: file.path.clone(),
243                        name: specifier.local.clone(),
244                        message: format!(
245                            "import `{}` is not exported by `{}`",
246                            specifier.imported,
247                            target_path.display()
248                        ),
249                    });
250                    continue;
251                };
252
253                insert_import(table, diagnostics, binding);
254            }
255        }
256    }
257}
258
259fn resolve_semantic_package_import(
260    import: &presolve_parser::ParsedImport,
261    packages: &SemanticPackageResolutionTable,
262    table: &mut ModuleBindingTable,
263    diagnostics: &mut Vec<BindingDiagnostic>,
264    module: &Path,
265) {
266    // `presolve` is the public declaration-only authoring vocabulary. Its
267    // imports select compiler intrinsics that are already recognized by the
268    // parser/component model; unlike third-party packages, they have no
269    // runtime module, integrity coordinate, or semantic capability to bind.
270    if import.source == "presolve" {
271        return;
272    }
273    let Some(contract) = packages.contract(&import.source) else {
274        diagnostics.push(BindingDiagnostic {
275            code: "PSBIND1009".into(),
276            module: module.to_path_buf(),
277            name: import.source.clone(),
278            message: format!(
279                "external import `{}` has no semantic package contract",
280                import.source
281            ),
282        });
283        return;
284    };
285    for specifier in &import.specifiers {
286        let Some(export) = contract.exports.get(&specifier.imported) else {
287            diagnostics.push(BindingDiagnostic {
288                code: "PSBIND1010".into(),
289                module: module.to_path_buf(),
290                name: specifier.imported.clone(),
291                message: format!(
292                    "semantic package `{}` does not declare export `{}`",
293                    import.source, specifier.imported
294                ),
295            });
296            continue;
297        };
298        insert_import(
299            table,
300            diagnostics,
301            ImportBinding {
302                local_name: specifier.local.clone(),
303                imported_name: specifier.imported.clone(),
304                source_module: PathBuf::from(&import.source),
305                target: ImportBindingTarget::SemanticPackage {
306                    package: contract.package.clone(),
307                    version: contract.version.clone(),
308                    integrity: contract.integrity.clone(),
309                    export: specifier.imported.clone(),
310                    kind: export.kind.clone(),
311                    type_signature: export.type_signature.clone(),
312                    runtime_module: export.runtime_module.clone(),
313                    resume_policy: export.resume_policy.clone(),
314                    pure_operation: export.pure_operation,
315                    resource_endpoint: export.resource_endpoint.clone(),
316                    route_loader: export.route_loader.clone(),
317                    server_action: export.server_action.clone(),
318                    form_submission: export.form_submission.clone(),
319                    opaque_terminal: export.opaque_terminal.clone(),
320                },
321            },
322        );
323    }
324}
325
326fn resolve_relative_reexports(
327    unit: &CompilationUnit,
328    modules: &ModuleGraph,
329    tables: &mut [ModuleBindingTable],
330    diagnostics: &mut Vec<BindingDiagnostic>,
331) {
332    for _ in 0..unit.files().len() {
333        let exports_by_path = tables
334            .iter()
335            .map(|table| (table.path.clone(), table.exports.clone()))
336            .collect::<BTreeMap<_, _>>();
337        let mut changed = false;
338
339        for (file, table) in unit.files().iter().zip(tables.iter_mut()) {
340            for export in file.exports.iter().filter(|export| export.source.is_some()) {
341                let edge_kind = match export.kind {
342                    presolve_parser::ParsedExportKind::Named => ModuleEdgeKind::NamedReExport,
343                    presolve_parser::ParsedExportKind::All => ModuleEdgeKind::ExportAll,
344                    presolve_parser::ParsedExportKind::Default => continue,
345                };
346                let target = module_target(modules, &file.path, export.span, edge_kind);
347                let ModuleTarget::Resolved(target_path) = target else {
348                    if matches!(target, ModuleTarget::Unresolved) {
349                        push_diagnostic_once(
350                            diagnostics,
351                            BindingDiagnostic {
352                                code: "PSBIND1006".to_string(),
353                                module: file.path.clone(),
354                                name: export.source.clone().unwrap_or_default(),
355                                message: "relative re-export does not resolve to a module"
356                                    .to_string(),
357                            },
358                        );
359                    }
360                    continue;
361                };
362                let target_exports = exports_by_path
363                    .get(&target_path)
364                    .cloned()
365                    .unwrap_or_default();
366
367                match export.kind {
368                    presolve_parser::ParsedExportKind::Named => {
369                        for specifier in &export.specifiers {
370                            let Some(local_name) = &specifier.local else {
371                                continue;
372                            };
373                            let Some(target) = target_exports.get(local_name) else {
374                                continue;
375                            };
376                            changed |= insert_reexport(
377                                table,
378                                ExportBinding {
379                                    exported_name: specifier.exported.clone(),
380                                    local_name: local_name.clone(),
381                                    symbol: target.symbol.clone(),
382                                },
383                            );
384                        }
385                    }
386                    presolve_parser::ParsedExportKind::All if export.specifiers.is_empty() => {
387                        for (name, target) in target_exports {
388                            if name != "default" {
389                                changed |= insert_reexport(
390                                    table,
391                                    ExportBinding {
392                                        exported_name: name.clone(),
393                                        local_name: target.local_name,
394                                        symbol: target.symbol,
395                                    },
396                                );
397                            }
398                        }
399                    }
400                    presolve_parser::ParsedExportKind::All => {
401                        push_diagnostic_once(
402                            diagnostics,
403                            BindingDiagnostic {
404                                code: "PSBIND1007".to_string(),
405                                module: file.path.clone(),
406                                name: export.source.clone().unwrap_or_default(),
407                                message: "namespace re-export bindings are not supported yet"
408                                    .to_string(),
409                            },
410                        );
411                    }
412                    presolve_parser::ParsedExportKind::Default => {}
413                }
414            }
415        }
416
417        if !changed {
418            break;
419        }
420    }
421}
422
423fn insert_reexport(table: &mut ModuleBindingTable, binding: ExportBinding) -> bool {
424    if table.exports.contains_key(&binding.exported_name) {
425        return false;
426    }
427
428    table.exports.insert(binding.exported_name.clone(), binding);
429    true
430}
431
432fn collect_identity_collisions(symbols: &SymbolTable, diagnostics: &mut Vec<BindingDiagnostic>) {
433    let mut paths_by_id = BTreeMap::<String, Vec<PathBuf>>::new();
434
435    for module in &symbols.modules {
436        for symbol in module
437            .symbols
438            .values()
439            .filter(|symbol| symbol.kind == SymbolKind::Component)
440        {
441            paths_by_id
442                .entry(symbol.id.as_str().to_string())
443                .or_default()
444                .push(module.path.clone());
445        }
446    }
447
448    for paths in paths_by_id.values_mut() {
449        paths.sort();
450        paths.dedup();
451    }
452
453    for (id, paths) in paths_by_id.into_iter().filter(|(_, paths)| paths.len() > 1) {
454        diagnostics.push(BindingDiagnostic {
455            code: "PSBIND1008".to_string(),
456            module: paths[0].clone(),
457            name: id.clone(),
458            message: format!(
459                "semantic identity `{id}` collides across modules: {}",
460                paths
461                    .iter()
462                    .map(|path| path.display().to_string())
463                    .collect::<Vec<_>>()
464                    .join(", ")
465            ),
466        });
467    }
468}
469
470fn push_diagnostic_once(diagnostics: &mut Vec<BindingDiagnostic>, diagnostic: BindingDiagnostic) {
471    if diagnostics.iter().any(|existing| existing == &diagnostic) {
472        return;
473    }
474
475    diagnostics.push(diagnostic);
476}
477
478fn resolve_import_binding(
479    specifier: &presolve_parser::ParsedImportSpecifier,
480    target_path: &Path,
481    target_exports: &BTreeMap<String, ExportBinding>,
482) -> Option<ImportBinding> {
483    if specifier.imported == "*" {
484        return Some(ImportBinding {
485            local_name: specifier.local.clone(),
486            imported_name: specifier.imported.clone(),
487            source_module: target_path.to_path_buf(),
488            target: ImportBindingTarget::Namespace {
489                module: target_path.to_path_buf(),
490                exports: target_exports
491                    .iter()
492                    .map(|(name, export)| (name.clone(), export.symbol.clone()))
493                    .collect(),
494            },
495        });
496    }
497
498    target_exports
499        .get(&specifier.imported)
500        .map(|export| ImportBinding {
501            local_name: specifier.local.clone(),
502            imported_name: specifier.imported.clone(),
503            source_module: target_path.to_path_buf(),
504            target: ImportBindingTarget::Symbol(export.symbol.clone()),
505        })
506}
507
508fn module_target(
509    modules: &ModuleGraph,
510    source: &Path,
511    span: presolve_parser::SourceSpan,
512    kind: ModuleEdgeKind,
513) -> ModuleTarget {
514    modules
515        .edges
516        .iter()
517        .find(|edge| edge.kind == kind && edge.source == source && edge.span == span)
518        .map_or(ModuleTarget::Unresolved, |edge| edge.target.clone())
519}
520
521fn insert_export(
522    table: &mut ModuleBindingTable,
523    diagnostics: &mut Vec<BindingDiagnostic>,
524    binding: ExportBinding,
525) {
526    if table.exports.contains_key(&binding.exported_name) {
527        diagnostics.push(BindingDiagnostic {
528            code: "PSBIND1004".to_string(),
529            module: table.path.clone(),
530            name: binding.exported_name.clone(),
531            message: format!("duplicate export binding `{}`", binding.exported_name),
532        });
533        return;
534    }
535
536    table.exports.insert(binding.exported_name.clone(), binding);
537}
538
539fn insert_import(
540    table: &mut ModuleBindingTable,
541    diagnostics: &mut Vec<BindingDiagnostic>,
542    binding: ImportBinding,
543) {
544    if table.imports.contains_key(&binding.local_name) {
545        diagnostics.push(BindingDiagnostic {
546            code: "PSBIND1005".to_string(),
547            module: table.path.clone(),
548            name: binding.local_name.clone(),
549            message: format!("duplicate import binding `{}`", binding.local_name),
550        });
551        return;
552    }
553
554    table.imports.insert(binding.local_name.clone(), binding);
555}
556
557#[cfg(test)]
558mod tests {
559    use super::{build_binding_table, build_binding_table_with_packages, ImportBindingTarget};
560    use crate::semantic_package::{
561        parse_semantic_package_contract, SemanticPackageResolutionTable,
562    };
563    use crate::{build_module_graph, build_symbol_table, CompilationUnit, SemanticId};
564
565    #[test]
566    fn resolves_relative_named_default_and_namespace_imports() {
567        let unit = CompilationUnit::parse_sources([
568            (
569                "src/app/App.tsx",
570                r#"
571import { Widget as CounterWidget } from "../ui/Counter";
572import StatusCard from "../ui/Status";
573import * as counter from "../ui/Counter";
574import { packageValue } from "presolve-runtime";
575"#,
576            ),
577            (
578                "src/ui/Counter.tsx",
579                r#"
580@component("x-counter")
581export class Counter extends Component {
582  render() {
583    return <div>Counter</div>;
584  }
585}
586
587export { Counter as Widget };
588"#,
589            ),
590            (
591                "src/ui/Status.tsx",
592                r#"
593@component("x-status")
594export default class Status extends Component {
595  render() {
596    return <div>Status</div>;
597  }
598}
599"#,
600            ),
601        ]);
602        let symbols = build_symbol_table(&unit);
603        let modules = build_module_graph(&unit);
604        let bindings = build_binding_table(&unit, &symbols, &modules);
605        let app = bindings.module("src/app/App.tsx").expect("app module");
606
607        assert_eq!(
608            bindings
609                .diagnostics
610                .iter()
611                .map(|diagnostic| diagnostic.code.as_str())
612                .collect::<Vec<_>>(),
613            vec!["PSBIND1009"]
614        );
615        assert_eq!(app.imports.len(), 3);
616        assert_eq!(app.exports.len(), 0);
617        assert_eq!(
618            app.imports["CounterWidget"].target,
619            ImportBindingTarget::Symbol(
620                bindings
621                    .module("src/ui/Counter.tsx")
622                    .expect("counter module")
623                    .exports["Widget"]
624                    .symbol
625                    .clone()
626            )
627        );
628        assert_eq!(
629            app.imports["StatusCard"].target,
630            ImportBindingTarget::Symbol(
631                bindings
632                    .module("src/ui/Status.tsx")
633                    .expect("status module")
634                    .exports["default"]
635                    .symbol
636                    .clone()
637            )
638        );
639
640        let ImportBindingTarget::Namespace { module, exports } = &app.imports["counter"].target
641        else {
642            panic!("expected namespace binding");
643        };
644        assert_eq!(module, &std::path::PathBuf::from("src/ui/Counter.tsx"));
645        assert_eq!(
646            exports["Widget"].id,
647            SemanticId::component_in_module("src/ui/Counter.tsx", Some("x-counter"), "Counter")
648        );
649        assert!(bindings
650            .resolve_import("src/app/App.tsx", "packageValue")
651            .is_none());
652    }
653
654    #[test]
655    fn reports_unresolved_relative_and_missing_export_bindings() {
656        let unit = CompilationUnit::parse_sources([
657            (
658                "src/App.tsx",
659                r#"
660import { Missing } from "./Counter";
661import { MissingModule } from "./missing";
662"#,
663            ),
664            (
665                "src/Counter.tsx",
666                r#"
667@component("x-counter")
668export class Counter extends Component {
669  render() {
670    return <div>Counter</div>;
671  }
672}
673"#,
674            ),
675        ]);
676        let symbols = build_symbol_table(&unit);
677        let modules = build_module_graph(&unit);
678        let bindings = build_binding_table(&unit, &symbols, &modules);
679
680        let codes = bindings
681            .diagnostics
682            .iter()
683            .map(|diagnostic| diagnostic.code.as_str())
684            .collect::<Vec<_>>();
685
686        assert_eq!(codes, vec!["PSBIND1003", "PSBIND1002"]);
687    }
688
689    #[test]
690    fn resolves_named_and_export_all_reexport_chains() {
691        let unit = CompilationUnit::parse_sources([
692            (
693                "src/App.tsx",
694                r#"
695import { PrimaryButton } from "./index";
696"#,
697            ),
698            (
699                "src/base.tsx",
700                r#"
701@component("x-button")
702export class Button extends Component {
703  render() {
704    return <button>Button</button>;
705  }
706}
707"#,
708            ),
709            (
710                "src/bridge.ts",
711                r#"
712export { Button as PrimaryButton } from "./base";
713"#,
714            ),
715            (
716                "src/index.ts",
717                r#"
718export * from "./bridge";
719"#,
720            ),
721        ]);
722        let symbols = build_symbol_table(&unit);
723        let modules = build_module_graph(&unit);
724        let bindings = build_binding_table(&unit, &symbols, &modules);
725
726        assert!(bindings.diagnostics.is_empty());
727        assert_eq!(
728            bindings
729                .resolve_import("src/App.tsx", "PrimaryButton")
730                .expect("resolved re-export")
731                .target,
732            ImportBindingTarget::Symbol(
733                bindings
734                    .module("src/index.ts")
735                    .expect("index module")
736                    .exports["PrimaryButton"]
737                    .symbol
738                    .clone()
739            )
740        );
741    }
742
743    #[test]
744    fn module_qualified_component_ids_do_not_collide_across_modules() {
745        let unit = CompilationUnit::parse_sources([
746            (
747                "src/First.tsx",
748                r#"
749@component("x-duplicate")
750class First extends Component {
751  render() {
752    return <div>First</div>;
753  }
754}
755"#,
756            ),
757            (
758                "src/Second.tsx",
759                r#"
760@component("x-duplicate")
761class Second extends Component {
762  render() {
763    return <div>Second</div>;
764  }
765}
766"#,
767            ),
768        ]);
769        let symbols = build_symbol_table(&unit);
770        let modules = build_module_graph(&unit);
771        let bindings = build_binding_table(&unit, &symbols, &modules);
772
773        assert!(bindings.diagnostics.is_empty());
774        assert_ne!(
775            symbols
776                .module("src/First.tsx")
777                .expect("first module")
778                .symbols["First"]
779                .id,
780            symbols
781                .module("src/Second.tsx")
782                .expect("second module")
783                .symbols["Second"]
784                .id
785        );
786    }
787
788    #[test]
789    fn resolves_external_imports_only_through_semantic_package_contracts() {
790        let unit = CompilationUnit::parse_sources([(
791            "src/App.tsx",
792            "import { format } from \"date-kit\"; export class App {}",
793        )]);
794        let symbols = build_symbol_table(&unit);
795        let modules = build_module_graph(&unit);
796        let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#).unwrap();
797        let mut packages = SemanticPackageResolutionTable::default();
798        packages.insert("date-kit".into(), contract).unwrap();
799        let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
800        assert!(bindings.diagnostics.is_empty());
801        let ImportBindingTarget::SemanticPackage {
802            package,
803            version,
804            integrity,
805            export,
806            kind,
807            type_signature,
808            runtime_module,
809            resume_policy,
810            pure_operation: _,
811            resource_endpoint: _,
812            route_loader: _,
813            server_action: _,
814            form_submission: _,
815            opaque_terminal: _,
816        } = &bindings
817            .resolve_import("src/App.tsx", "format")
818            .unwrap()
819            .target
820        else {
821            panic!("expected semantic package binding");
822        };
823        assert_eq!(package, "date-kit");
824        assert_eq!(version, "1.0.0");
825        assert_eq!(
826            integrity,
827            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
828        );
829        assert_eq!(export, "format");
830        assert_eq!(kind, &crate::semantic_package::SemanticPackageKind::Pure);
831        assert_eq!(type_signature, "(Date)->string");
832        assert_eq!(runtime_module, "dist/format.js");
833        assert_eq!(resume_policy, "input_only");
834    }
835
836    #[test]
837    fn accepts_the_public_presolve_authoring_import_without_a_package_capability_contract() {
838        let unit = CompilationUnit::parse_sources([(
839            "src/App.tsx",
840            "import { component, state } from \"presolve\"; export class App {}",
841        )]);
842        let symbols = build_symbol_table(&unit);
843        let modules = build_module_graph(&unit);
844        let bindings = build_binding_table(&unit, &symbols, &modules);
845        assert!(bindings.diagnostics.is_empty());
846        assert!(bindings.module("src/App.tsx").unwrap().imports.is_empty());
847    }
848
849    #[test]
850    fn rejects_external_imports_without_a_matching_contract_or_export() {
851        let unit = CompilationUnit::parse_sources([(
852            "src/App.tsx",
853            "import present from \"date-kit\"; import { absent } from \"date-kit\"; import { other } from \"other-kit\"; export class App {}",
854        )]);
855        let symbols = build_symbol_table(&unit);
856        let modules = build_module_graph(&unit);
857        let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.0.0","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"default":{"kind":"pure","type_signature":"(Date)->string","runtime_module":"dist/default.js","resume_policy":"input_only"}}}"#).unwrap();
858        let mut packages = SemanticPackageResolutionTable::default();
859        packages.insert("date-kit".into(), contract).unwrap();
860
861        let bindings = build_binding_table_with_packages(&unit, &symbols, &modules, &packages);
862        assert_eq!(
863            bindings
864                .diagnostics
865                .iter()
866                .map(|diagnostic| diagnostic.code.as_str())
867                .collect::<Vec<_>>(),
868            vec!["PSBIND1010", "PSBIND1009"]
869        );
870        assert!(bindings.resolve_import("src/App.tsx", "present").is_some());
871        assert!(bindings.resolve_import("src/App.tsx", "absent").is_none());
872        assert!(bindings.resolve_import("src/App.tsx", "other").is_none());
873    }
874}