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