1use std::collections::{BTreeMap, BTreeSet};
48use std::sync::Arc;
49
50use cove_diag::{Diagnostic, Span, Spanned};
51use cove_schema::HostSchemas;
52use cove_syntax::ast::{
53 Block, EnumDecl, Expr, ExprKind, FnDecl, Item, ItemKind, MatchArm, Pattern, PatternKind,
54 Receiver, Stmt, StmtKind, StrPart, StructDecl, TraitDecl, TraitMethod, TypeAlias,
55};
56
57use crate::capability::{Capability, OpenCall};
58use crate::facts::Facts;
59use crate::package::Package;
60
61#[derive(Debug)]
63pub struct FnEntry {
64 pub decl: Arc<FnDecl>,
65 pub exported: bool,
66 pub is_test: bool,
73 pub doc: Option<String>,
74 pub receiver_type: Option<String>,
76 pub from_trait_default: Option<String>,
83 pub direct_capabilities: BTreeSet<Capability>,
85 pub required_capabilities: BTreeSet<Capability>,
110 pub direct_open_calls: BTreeSet<OpenCall>,
114 pub open_calls: BTreeSet<OpenCall>,
123}
124
125impl FnEntry {
126 pub fn is_capability_open(&self) -> bool {
133 !self.open_calls.is_empty()
134 }
135}
136
137#[derive(Debug)]
138pub struct StructEntry {
139 pub decl: Arc<StructDecl>,
140 pub exported: bool,
141 pub opaque: bool,
150 pub doc: Option<String>,
151}
152
153#[derive(Debug)]
154pub struct EnumEntry {
155 pub decl: Arc<EnumDecl>,
156 pub exported: bool,
157 pub doc: Option<String>,
158}
159
160#[derive(Debug)]
162pub struct TraitEntry {
163 pub decl: Arc<TraitDecl>,
164 pub exported: bool,
165 pub doc: Option<String>,
166}
167
168impl TraitEntry {
169 pub fn method(&self, name: &str) -> Option<&TraitMethod> {
171 self.decl.methods.iter().find(|m| m.name.node == name)
172 }
173}
174
175#[derive(Clone, Debug)]
182pub struct Conformance {
183 pub trait_name: String,
184 pub type_name: String,
185 pub trait_module: String,
190 pub type_module: String,
191 pub methods: BTreeSet<String>,
194 pub span: Span,
196}
197
198#[derive(Debug)]
199pub struct AliasEntry {
200 pub decl: Arc<TypeAlias>,
201 pub exported: bool,
202 pub doc: Option<String>,
203}
204
205#[derive(Debug, Default)]
207pub struct ResolvedModule {
208 pub name: String,
209 pub functions: BTreeMap<String, FnEntry>,
211 pub methods: BTreeMap<(String, String), FnEntry>,
213 pub structs: BTreeMap<String, StructEntry>,
214 pub enums: BTreeMap<String, EnumEntry>,
215 pub traits: BTreeMap<String, TraitEntry>,
216 pub conformances: BTreeMap<(String, String), Conformance>,
218 pub aliases: BTreeMap<String, AliasEntry>,
219 pub host_uses: BTreeSet<String>,
221 pub host_items: BTreeMap<String, String>,
223 pub imports: BTreeMap<String, String>,
230 pub module_imports: BTreeMap<String, String>,
234}
235
236impl ResolvedModule {
237 pub fn owner_of<'a>(&'a self, name: &str) -> Option<&'a str> {
244 if self.functions.contains_key(name)
245 || self.structs.contains_key(name)
246 || self.enums.contains_key(name)
247 || self.traits.contains_key(name)
248 || self.aliases.contains_key(name)
249 {
250 return Some(&self.name);
251 }
252 self.imports.get(name).map(String::as_str)
253 }
254
255 pub fn exported(&self, name: &str) -> Option<bool> {
261 if let Some(entry) = self.functions.get(name) {
262 return Some(entry.exported);
263 }
264 if let Some(entry) = self.structs.get(name) {
265 return Some(entry.exported);
266 }
267 if let Some(entry) = self.enums.get(name) {
268 return Some(entry.exported);
269 }
270 if let Some(entry) = self.traits.get(name) {
271 return Some(entry.exported);
272 }
273 self.aliases.get(name).map(|entry| entry.exported)
274 }
275
276 pub fn exports(&self) -> Vec<String> {
278 let functions = self
279 .functions
280 .iter()
281 .filter(|(_, entry)| entry.exported)
282 .map(|(name, _)| name);
283 let structs = self
284 .structs
285 .iter()
286 .filter(|(_, entry)| entry.exported)
287 .map(|(name, _)| name);
288 let enums = self
289 .enums
290 .iter()
291 .filter(|(_, entry)| entry.exported)
292 .map(|(name, _)| name);
293 let traits = self
294 .traits
295 .iter()
296 .filter(|(_, entry)| entry.exported)
297 .map(|(name, _)| name);
298 let aliases = self
299 .aliases
300 .iter()
301 .filter(|(_, entry)| entry.exported)
302 .map(|(name, _)| name);
303 let mut names: Vec<String> = functions
304 .chain(structs)
305 .chain(enums)
306 .chain(traits)
307 .chain(aliases)
308 .cloned()
309 .collect();
310 names.sort();
311 names
312 }
313
314 pub fn dependencies(&self) -> BTreeSet<&str> {
316 self.imports
317 .values()
318 .chain(self.module_imports.values())
319 .map(String::as_str)
320 .collect()
321 }
322}
323
324#[derive(Debug, Default)]
326pub struct Program {
327 pub modules: BTreeMap<String, ResolvedModule>,
328 pub notices: Vec<Diagnostic>,
344 pub call_graph: BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
352 pub facts: Facts,
362}
363
364impl Program {
365 pub fn lookup_fn(&self, module: &str, name: &str) -> Option<&FnEntry> {
367 self.modules.get(module)?.functions.get(name)
368 }
369
370 pub fn tests(&self) -> Vec<DeclaredTest<'_>> {
376 let mut found = Vec::new();
377 for (module, resolved) in &self.modules {
378 for (name, entry) in &resolved.functions {
379 if entry.is_test {
380 found.push(DeclaredTest {
381 module: module.as_str(),
382 name: name.as_str(),
383 entry,
384 });
385 }
386 }
387 }
388 found
389 }
390
391 pub fn conformances_of(&self, type_module: &str, type_name: &str) -> Vec<(&str, &Conformance)> {
401 let mut found: Vec<(&str, &Conformance)> = Vec::new();
402 for (module, resolved) in &self.modules {
403 for conformance in resolved.conformances.values() {
404 if conformance.type_module == type_module && conformance.type_name == type_name {
405 found.push((module.as_str(), conformance));
406 }
407 }
408 }
409 found.sort_by(|(_, a), (_, b)| {
410 (&a.trait_module, &a.trait_name).cmp(&(&b.trait_module, &b.trait_name))
411 });
412 found
413 }
414
415 pub fn methods_of(&self, type_module: &str, type_name: &str) -> Vec<DeclaredMethod<'_>> {
427 let mut found: BTreeMap<&str, DeclaredMethod<'_>> = BTreeMap::new();
428 if let Some(owner) = self.modules.get(type_module) {
429 for ((owner_type, method), entry) in &owner.methods {
430 if owner_type == type_name {
431 found.insert(
432 method.as_str(),
433 DeclaredMethod {
434 module: owner.name.as_str(),
435 name: method.as_str(),
436 entry,
437 },
438 );
439 }
440 }
441 }
442 for (module, conformance) in self.conformances_of(type_module, type_name) {
443 let Some(owner) = self.modules.get(module) else {
444 continue;
445 };
446 for method in &conformance.methods {
447 let key = (type_name.to_string(), method.clone());
448 let Some(entry) = owner.methods.get(&key) else {
449 continue;
450 };
451 found.insert(
452 method.as_str(),
453 DeclaredMethod {
454 module: owner.name.as_str(),
455 name: method.as_str(),
456 entry,
457 },
458 );
459 }
460 }
461 found.into_values().collect()
462 }
463}
464
465#[derive(Clone, Copy, Debug)]
467pub struct DeclaredTest<'a> {
468 pub module: &'a str,
470 pub name: &'a str,
472 pub entry: &'a FnEntry,
474}
475
476impl DeclaredTest<'_> {
477 pub fn qualified_name(&self) -> String {
480 format!("{}.{}", self.module, self.name)
481 }
482}
483
484#[derive(Clone, Copy, Debug)]
486pub struct DeclaredMethod<'a> {
487 pub module: &'a str,
490 pub name: &'a str,
492 pub entry: &'a FnEntry,
494}
495
496pub fn resolve(package: &Package) -> Result<Program, Vec<Diagnostic>> {
517 resolve_with(package, &HostSchemas::new())
518}
519
520pub fn resolve_with(package: &Package, schemas: &HostSchemas) -> Result<Program, Vec<Diagnostic>> {
529 let mut program = Program::default();
530 let mut errors = Vec::new();
531 let mut warnings = Vec::new();
532
533 let surfaces: BTreeMap<&str, Surface> = package
534 .modules
535 .iter()
536 .map(|(name, module)| (name.as_str(), Surface::of(module)))
537 .collect();
538
539 let opaque_fields = OpaqueFields::of(package);
540
541 let mut call_sites: BTreeMap<Node, Vec<CallShape>> = BTreeMap::new();
542 let mut edges: Vec<ImportEdge> = Vec::new();
543 let mut warned_hosts: BTreeSet<String> = BTreeSet::new();
551 for (name, module) in &package.modules {
552 let uses = resolve_uses(
553 name,
554 module,
555 &surfaces,
556 schemas,
557 &mut errors,
558 &mut warnings,
559 &mut warned_hosts,
560 );
561 edges.extend(uses.edges.iter().cloned());
562 let (resolved, calls) = resolve_module(
563 name,
564 module,
565 uses,
566 &surfaces,
567 &opaque_fields,
568 schemas,
569 &mut errors,
570 &mut warnings,
571 );
572 for (key, shapes) in calls {
573 call_sites.insert((name.clone(), key), shapes);
574 }
575 program.modules.insert(name.clone(), resolved);
576 }
577
578 check_import_cycles(&edges, &mut errors);
579 check_method_collisions(&program, &mut errors);
580 let (call_graph, unresolved) = package_call_graph(&program, &call_sites);
581 merge_open_calls(&mut program, &unresolved);
582 propagate_capabilities(&mut program, &call_graph);
583 program.call_graph = call_graph;
584 check_bodies(&program, schemas, &mut errors, &mut warnings);
585
586 if errors.is_empty() {
587 program.notices = warnings;
588 Ok(program)
589 } else {
590 errors.extend(warnings);
591 Err(errors)
592 }
593}
594
595#[allow(clippy::too_many_arguments)]
596fn resolve_module(
597 name: &str,
598 module: &crate::package::Module,
599 uses: ModuleUses,
600 surfaces: &BTreeMap<&str, Surface>,
601 opaque_fields: &OpaqueFields,
602 schemas: &HostSchemas,
603 errors: &mut Vec<Diagnostic>,
604 warnings: &mut Vec<Diagnostic>,
605) -> (ResolvedModule, BTreeMap<FnKey, Vec<CallShape>>) {
606 let mut resolved = ResolvedModule {
607 name: name.to_string(),
608 host_uses: uses.host_uses.clone(),
609 host_items: uses.host_items.clone(),
610 imports: uses.imports.clone(),
611 module_imports: uses.module_imports.clone(),
612 ..ResolvedModule::default()
613 };
614
615 let mut fn_spans: BTreeMap<String, Span> = BTreeMap::new();
617 let mut struct_spans: BTreeMap<String, Span> = BTreeMap::new();
618 let mut enum_spans: BTreeMap<String, Span> = BTreeMap::new();
619 let mut alias_spans: BTreeMap<String, Span> = BTreeMap::new();
620 let mut trait_spans: BTreeMap<String, Span> = BTreeMap::new();
621 let mut pending_impls: Vec<(&cove_syntax::ast::ImplBlock, Span)> = Vec::new();
622 let mut call_sites: BTreeMap<FnKey, Vec<CallShape>> = BTreeMap::new();
625
626 for unit in &module.units {
627 for item in &unit.ast.items {
628 match &item.kind {
629 ItemKind::Fn(decl) => {
630 if let Some(existing) =
631 duplicate(&mut fn_spans, &decl.name.node, decl.name.span)
632 {
633 errors.push(duplicate_declaration(
634 name,
635 &decl.name.node,
636 decl.name.span,
637 existing,
638 ));
639 continue;
640 }
641 missing_doc(warnings, item, &decl.name.node, decl.name.span);
642 let (capabilities, calls, open) = analyze_body(
643 decl,
644 &resolved.host_uses,
645 &resolved.host_items,
646 opaque_fields,
647 schemas,
648 );
649 call_sites.insert(FnKey::Fn(decl.name.node.clone()), calls);
650 resolved.functions.insert(
651 decl.name.node.clone(),
652 FnEntry {
653 decl: Arc::new(decl.clone()),
654 exported: item.exported,
655 is_test: item.is_test,
656 doc: item.doc.clone(),
657 receiver_type: None,
658 from_trait_default: None,
659 direct_capabilities: capabilities,
660 required_capabilities: BTreeSet::new(),
661 direct_open_calls: open,
662 open_calls: BTreeSet::new(),
663 },
664 );
665 }
666 ItemKind::Struct(decl) => {
667 if let Some(existing) =
668 duplicate(&mut struct_spans, &decl.name.node, decl.name.span)
669 {
670 errors.push(duplicate_declaration(
671 name,
672 &decl.name.node,
673 decl.name.span,
674 existing,
675 ));
676 continue;
677 }
678 missing_doc(warnings, item, &decl.name.node, decl.name.span);
679 resolved.structs.insert(
680 decl.name.node.clone(),
681 StructEntry {
682 decl: Arc::new(decl.clone()),
683 exported: item.exported,
684 opaque: item.is_opaque,
685 doc: item.doc.clone(),
686 },
687 );
688 }
689 ItemKind::Enum(decl) => {
690 if let Some(existing) =
691 duplicate(&mut enum_spans, &decl.name.node, decl.name.span)
692 {
693 errors.push(duplicate_declaration(
694 name,
695 &decl.name.node,
696 decl.name.span,
697 existing,
698 ));
699 continue;
700 }
701 missing_doc(warnings, item, &decl.name.node, decl.name.span);
702 resolved.enums.insert(
703 decl.name.node.clone(),
704 EnumEntry {
705 decl: Arc::new(decl.clone()),
706 exported: item.exported,
707 doc: item.doc.clone(),
708 },
709 );
710 }
711 ItemKind::Trait(decl) => {
712 if let Some(existing) =
713 duplicate(&mut trait_spans, &decl.name.node, decl.name.span)
714 {
715 errors.push(duplicate_declaration(
716 name,
717 &decl.name.node,
718 decl.name.span,
719 existing,
720 ));
721 continue;
722 }
723 missing_doc(warnings, item, &decl.name.node, decl.name.span);
724 if item.exported {
727 for method in &decl.methods {
728 if method.doc.is_none() {
729 warnings.push(undocumented(
730 &format!("{}.{}", decl.name.node, method.name.node),
731 method.name.span,
732 ));
733 }
734 }
735 }
736 resolved.traits.insert(
737 decl.name.node.clone(),
738 TraitEntry {
739 decl: Arc::new(decl.clone()),
740 exported: item.exported,
741 doc: item.doc.clone(),
742 },
743 );
744 }
745 ItemKind::TypeAlias(decl) => {
746 if let Some(existing) =
747 duplicate(&mut alias_spans, &decl.name.node, decl.name.span)
748 {
749 errors.push(duplicate_declaration(
750 name,
751 &decl.name.node,
752 decl.name.span,
753 existing,
754 ));
755 continue;
756 }
757 missing_doc(warnings, item, &decl.name.node, decl.name.span);
758 resolved.aliases.insert(
759 decl.name.node.clone(),
760 AliasEntry {
761 decl: Arc::new(decl.clone()),
762 exported: item.exported,
763 doc: item.doc.clone(),
764 },
765 );
766 }
767 ItemKind::Impl(impl_block) => {
768 pending_impls.push((impl_block, item.span));
769 }
770 }
771 }
772 }
773
774 let mut method_spans: BTreeMap<(String, String), Span> = BTreeMap::new();
777 for (impl_block, _impl_span) in pending_impls {
778 let type_name = impl_block.type_name.node.clone();
779 let declares_type =
780 resolved.structs.contains_key(&type_name) || resolved.enums.contains_key(&type_name);
781 let type_module = declaring_module_of(surfaces, name, &uses, &type_name, DeclKind::Type);
785
786 if let Some(trait_ident) = &impl_block.trait_name {
792 let trait_name = trait_ident.node.clone();
793 let declares_trait = resolved.traits.contains_key(&trait_name);
794 let trait_module =
795 declaring_module_of(surfaces, name, &uses, &trait_name, DeclKind::Trait);
796 if !declares_trait && !declares_type {
797 errors.push(orphan_conformance(
798 name,
799 &trait_name,
800 &type_name,
801 trait_ident.span.to(impl_block.type_name.span),
802 ));
803 continue;
804 }
805 if trait_module.is_none() && trait_name != BUILTIN_SNAPSHOT_TRAIT {
809 errors.push(
810 Diagnostic::error(
811 "cove::resolve::unknown_trait",
812 format!("`{trait_name}` names a trait module `{name}` can see"),
813 )
814 .at(trait_ident.span)
815 .rule("A conformance names a trait the module declares or imports.")
816 .help(format!(
817 "Declare `trait {trait_name}` in this module, `use <module>.{trait_name}` to import it, or fix the name."
818 )),
819 );
820 continue;
821 }
822 }
823
824 if type_module.is_none() {
825 errors.push(
826 Diagnostic::error(
827 "cove::resolve::unknown_impl_type",
828 format!("`impl {type_name}` names a type module `{name}` can see"),
829 )
830 .at(impl_block.type_name.span)
831 .rule("An `impl` block extends a struct or enum the module declares, or one it imports as part of a conformance.")
832 .help(format!(
833 "Declare `struct {type_name}` or `enum {type_name}` in this module, `use <module>.{type_name}` to import it, or fix the name."
834 )),
835 );
836 continue;
837 }
838 let type_module = type_module.expect("checked just above").to_string();
839
840 if impl_block.trait_name.is_none() && !declares_type {
845 errors.push(
846 Diagnostic::error(
847 "cove::resolve::foreign_inherent_impl",
848 format!(
849 "`impl {type_name}` adds methods to a type module `{type_module}` declares"
850 ),
851 )
852 .at(impl_block.type_name.span)
853 .rule("An `impl` block with no trait extends a type its own module declares; a method for another module's type belongs to a trait, so that the conformance is a fact both modules can see.")
854 .help(format!(
855 "move this block to module `{type_module}`, or declare a trait here and write `impl <Trait> for {type_name}`"
856 )),
857 );
858 continue;
859 }
860
861 if let Some(trait_ident) = &impl_block.trait_name {
862 let header = trait_ident.span.to(impl_block.type_name.span);
863 let key = (trait_ident.node.clone(), type_name.clone());
864 if let Some(existing) = resolved.conformances.get(&key) {
865 errors.push(
866 Diagnostic::error(
867 "cove::resolve::duplicate_conformance",
868 format!("`{type_name}` already conforms to `{}`", trait_ident.node),
869 )
870 .at(header)
871 .label(existing.span, "the first conformance is declared here")
872 .rule("A type conforms to a trait exactly once; conformance is explicit, so two `impl Trait for Type` blocks would leave no way to choose.")
873 .help("Merge the two blocks into one."),
874 );
875 continue;
876 }
877 let (trait_module, trait_decl) = match declaring_module_of(
878 surfaces,
879 name,
880 &uses,
881 &trait_ident.node,
882 DeclKind::Trait,
883 ) {
884 Some(module) => (
885 module.to_string(),
886 surfaces[module].traits[&trait_ident.node].clone(),
887 ),
888 None => (type_module.clone(), builtin_snapshot_trait(header)),
892 };
893 check_conformance(
894 &mut resolved,
895 name,
896 impl_block,
897 Conformance {
898 trait_name: trait_ident.node.clone(),
899 type_name: type_name.clone(),
900 trait_module,
901 type_module,
902 methods: BTreeSet::new(),
903 span: header,
904 },
905 trait_decl,
906 &mut method_spans,
907 &mut call_sites,
908 opaque_fields,
909 schemas,
910 errors,
911 );
912 continue;
913 }
914
915 for inner in &impl_block.items {
916 match &inner.kind {
917 ItemKind::Fn(decl) => {
918 let key = (type_name.clone(), decl.name.node.clone());
919 if let Some(existing_span) = method_spans.get(&key) {
920 errors.push(
921 Diagnostic::error(
922 "cove::resolve::duplicate_declaration",
923 format!(
924 "`{type_name}.{}` is declared twice in module `{name}`",
925 decl.name.node
926 ),
927 )
928 .at(decl.name.span)
929 .label(
930 *existing_span,
931 format!("`{}` first declared here", decl.name.node),
932 )
933 .rule(
934 "Each method name may be declared once per type across a module's implementation units.",
935 ),
936 );
937 continue;
938 }
939 method_spans.insert(key.clone(), decl.name.span);
940 missing_doc(warnings, inner, &decl.name.node, decl.name.span);
941 let (capabilities, calls, open) = analyze_body(
942 decl,
943 &resolved.host_uses,
944 &resolved.host_items,
945 opaque_fields,
946 schemas,
947 );
948 call_sites.insert(
949 FnKey::Method(type_name.clone(), decl.name.node.clone()),
950 calls,
951 );
952 resolved.methods.insert(
953 key,
954 FnEntry {
955 decl: Arc::new(decl.clone()),
956 exported: inner.exported,
957 is_test: false,
961 doc: inner.doc.clone(),
962 receiver_type: Some(type_name.clone()),
963 from_trait_default: None,
964 direct_capabilities: capabilities,
965 required_capabilities: BTreeSet::new(),
966 direct_open_calls: open,
967 open_calls: BTreeSet::new(),
968 },
969 );
970 }
971 _ => {
972 errors.push(
973 Diagnostic::error(
974 "cove::resolve::invalid_impl_item",
975 "only `fn` declarations are allowed inside an `impl` block",
976 )
977 .at(inner.span)
978 .rule("An `impl` block may only contain method declarations."),
979 );
980 }
981 }
982 }
983 }
984
985 (resolved, call_sites)
989}
990
991pub fn host_modules(schemas: &HostSchemas) -> impl Iterator<Item = &'static str> + '_ {
1009 schemas.names()
1010}
1011
1012#[derive(Debug, Default)]
1023struct Surface {
1024 declarations: BTreeMap<String, Declared>,
1025 traits: BTreeMap<String, Arc<TraitDecl>>,
1028}
1029
1030#[derive(Debug)]
1031struct Declared {
1032 kind: DeclKind,
1033 exported: bool,
1034 span: Span,
1035}
1036
1037#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1039enum DeclKind {
1040 Function,
1041 Type,
1043 Trait,
1044 Alias,
1045}
1046
1047impl Surface {
1048 fn of(module: &crate::package::Module) -> Surface {
1049 let mut declarations: BTreeMap<String, Declared> = BTreeMap::new();
1050 let mut traits: BTreeMap<String, Arc<TraitDecl>> = BTreeMap::new();
1051 for unit in &module.units {
1052 for item in &unit.ast.items {
1053 if let ItemKind::Trait(decl) = &item.kind {
1054 traits
1055 .entry(decl.name.node.clone())
1056 .or_insert_with(|| Arc::new(decl.clone()));
1057 }
1058 let (name, kind) = match &item.kind {
1059 ItemKind::Fn(decl) => (&decl.name, DeclKind::Function),
1060 ItemKind::Struct(decl) => (&decl.name, DeclKind::Type),
1061 ItemKind::Enum(decl) => (&decl.name, DeclKind::Type),
1062 ItemKind::Trait(decl) => (&decl.name, DeclKind::Trait),
1063 ItemKind::TypeAlias(decl) => (&decl.name, DeclKind::Alias),
1064 ItemKind::Impl(_) => continue,
1065 };
1066 declarations.entry(name.node.clone()).or_insert(Declared {
1067 kind,
1068 exported: item.exported,
1069 span: name.span,
1070 });
1071 }
1072 }
1073 Surface {
1074 declarations,
1075 traits,
1076 }
1077 }
1078
1079 fn declares(&self, name: &str, kind: DeclKind) -> bool {
1081 self.declarations
1082 .get(name)
1083 .is_some_and(|declared| declared.kind == kind)
1084 }
1085}
1086
1087fn declaring_module_of<'a>(
1095 surfaces: &'a BTreeMap<&'a str, Surface>,
1096 module: &'a str,
1097 uses: &'a ModuleUses,
1098 name: &str,
1099 kind: DeclKind,
1100) -> Option<&'a str> {
1101 if surfaces
1102 .get(module)
1103 .is_some_and(|surface| surface.declares(name, kind))
1104 {
1105 return Some(module);
1106 }
1107 let owner = uses.imports.get(name)?.as_str();
1108 surfaces
1109 .get(owner)
1110 .is_some_and(|surface| surface.declares(name, kind))
1111 .then_some(owner)
1112}
1113
1114#[derive(Debug, Default)]
1116struct ModuleUses {
1117 imports: BTreeMap<String, String>,
1118 module_imports: BTreeMap<String, String>,
1119 host_uses: BTreeSet<String>,
1120 host_items: BTreeMap<String, String>,
1121 edges: Vec<ImportEdge>,
1124}
1125
1126#[derive(Clone, Debug)]
1129struct ImportEdge {
1130 from: String,
1131 to: String,
1132 span: Span,
1133}
1134
1135#[derive(Clone, Debug)]
1137enum Bound {
1138 HostItem(String),
1140 Item(String),
1142 Module(String),
1144}
1145
1146impl Bound {
1147 fn describe(&self) -> String {
1148 match self {
1149 Bound::HostItem(host) => format!("the host module `{host}`"),
1150 Bound::Item(module) => format!("module `{module}`"),
1151 Bound::Module(module) => format!("the module `{module}`"),
1152 }
1153 }
1154}
1155
1156fn resolve_uses(
1174 name: &str,
1175 module: &crate::package::Module,
1176 surfaces: &BTreeMap<&str, Surface>,
1177 schemas: &HostSchemas,
1178 errors: &mut Vec<Diagnostic>,
1179 warnings: &mut Vec<Diagnostic>,
1180 warned_hosts: &mut BTreeSet<String>,
1181) -> ModuleUses {
1182 let mut uses = ModuleUses::default();
1183 let mut bound: BTreeMap<String, (Bound, Span)> = BTreeMap::new();
1184 let own = surfaces.get(name);
1185
1186 for unit in &module.units {
1187 for use_decl in &unit.ast.uses {
1188 let segments: Vec<&str> = use_decl.path.iter().map(|i| i.node.as_str()).collect();
1189 let path = segments.join(".");
1190 let span = use_decl.span;
1191 let last = segments.last().expect("a `use` path is never empty");
1192
1193 if surfaces.contains_key(path.as_str()) {
1194 if let Some(diagnostic) = shadowed_host(&path, schemas, span) {
1195 errors.push(diagnostic);
1196 continue;
1197 }
1198 if let Some(diagnostic) = ambiguous_module_path(&path, &segments, surfaces, span) {
1199 errors.push(diagnostic);
1200 continue;
1201 }
1202 bind(
1203 &mut bound,
1204 name,
1205 own,
1206 last,
1207 Bound::Module(path.clone()),
1208 span,
1209 errors,
1210 );
1211 uses.module_imports.insert(last.to_string(), path.clone());
1212 uses.edges.push(ImportEdge {
1213 from: name.to_string(),
1214 to: path,
1215 span,
1216 });
1217 continue;
1218 }
1219
1220 if segments.len() >= 2 {
1221 let owner = segments[..segments.len() - 1].join(".");
1222 if let Some(surface) = surfaces.get(owner.as_str()) {
1223 if let Some(diagnostic) = shadowed_host(&owner, schemas, span) {
1224 errors.push(diagnostic);
1225 continue;
1226 }
1227 match surface.declarations.get(*last) {
1228 Some(declared) if declared.exported => {
1229 bind(
1230 &mut bound,
1231 name,
1232 own,
1233 last,
1234 Bound::Item(owner.clone()),
1235 span,
1236 errors,
1237 );
1238 uses.imports.insert(last.to_string(), owner.clone());
1239 uses.edges.push(ImportEdge {
1240 from: name.to_string(),
1241 to: owner,
1242 span,
1243 });
1244 }
1245 Some(declared) => {
1246 errors.push(private_declaration(&owner, last, span, declared.span))
1247 }
1248 None => errors.push(no_such_declaration(&owner, last, surface, span)),
1249 }
1250 continue;
1251 }
1252 }
1253
1254 match segments.len() {
1255 1 => {
1256 warn_unchecked_host_once(&path, schemas, span, warned_hosts, warnings);
1257 uses.host_uses.insert(path);
1258 }
1259 2 => {
1260 let host = segments[0].to_string();
1261 warn_unchecked_host_once(&host, schemas, span, warned_hosts, warnings);
1262 uses.host_uses.insert(host.clone());
1263 bind(
1264 &mut bound,
1265 name,
1266 own,
1267 last,
1268 Bound::HostItem(host.clone()),
1269 span,
1270 errors,
1271 );
1272 uses.host_items.insert(last.to_string(), host);
1273 }
1274 _ => errors.push(unknown_use(&path, &segments, surfaces, span)),
1275 }
1276 }
1277 }
1278
1279 uses
1280}
1281
1282fn bind(
1289 bound: &mut BTreeMap<String, (Bound, Span)>,
1290 module: &str,
1291 own: Option<&Surface>,
1292 name: &str,
1293 what: Bound,
1294 span: Span,
1295 errors: &mut Vec<Diagnostic>,
1296) {
1297 if let Some(declared) = own.and_then(|surface| surface.declarations.get(name)) {
1298 errors.push(
1299 Diagnostic::error(
1300 "cove::resolve::import_conflict",
1301 format!("`{name}` is imported, but module `{module}` also declares it"),
1302 )
1303 .at(span)
1304 .label(declared.span, format!("`{name}` is declared here"))
1305 .rule("An imported name and a declared name cannot both mean `name` in one module.")
1306 .help("rename one of them, or drop the `use` and name the import qualified"),
1307 );
1308 return;
1309 }
1310 match bound.get(name) {
1311 Some((existing, existing_span)) if !same_origin(existing, &what) => {
1312 let code = match (existing, &what) {
1315 (Bound::HostItem(_), Bound::HostItem(_)) => "cove::resolve::ambiguous_use",
1316 _ => "cove::resolve::import_conflict",
1317 };
1318 errors.push(
1319 Diagnostic::error(
1320 code,
1321 format!(
1322 "`{name}` is imported from both {} and {}",
1323 existing.describe(),
1324 what.describe()
1325 ),
1326 )
1327 .at(span)
1328 .label(
1329 *existing_span,
1330 format!("first imported from {} here", existing.describe()),
1331 )
1332 .rule("A `use` name must resolve to exactly one declaration or host module.")
1333 .help(format!(
1334 "drop one of the two `use` declarations, and name `{name}` qualified where the other meaning is wanted"
1335 )),
1336 );
1337 }
1338 Some(_) => {}
1339 None => {
1340 bound.insert(name.to_string(), (what, span));
1341 }
1342 }
1343}
1344
1345fn same_origin(a: &Bound, b: &Bound) -> bool {
1346 match (a, b) {
1347 (Bound::HostItem(a), Bound::HostItem(b))
1348 | (Bound::Item(a), Bound::Item(b))
1349 | (Bound::Module(a), Bound::Module(b)) => a == b,
1350 _ => false,
1351 }
1352}
1353
1354fn unchecked_host_module(module: &str, schemas: &HostSchemas, span: Span) -> Option<Diagnostic> {
1362 if schemas.module(module).is_some() {
1363 return None;
1364 }
1365 Some(
1366 Diagnostic::warning(
1367 "cove::resolve::unchecked_host",
1368 format!("no Host API schema describes the host module `{module}`, so calls into it are unchecked"),
1369 )
1370 .at(span)
1371 .rule(
1372 "A Host API call is checked against its module's schema; the checker reads the shipped schemas and any an embedder supplies.",
1373 )
1374 .help(format!(
1375 "if `{module}` is an embedder's module, hand its `ModuleSchema` to the compiler with `Compiler::new().with_host_schema(...)`; otherwise check the spelling"
1376 )),
1377 )
1378}
1379
1380fn warn_unchecked_host_once(
1390 module: &str,
1391 schemas: &HostSchemas,
1392 span: Span,
1393 warned_hosts: &mut BTreeSet<String>,
1394 warnings: &mut Vec<Diagnostic>,
1395) {
1396 if warned_hosts.contains(module) {
1397 return;
1398 }
1399 if let Some(warning) = unchecked_host_module(module, schemas, span) {
1400 warned_hosts.insert(module.to_string());
1401 warnings.push(warning);
1402 }
1403}
1404
1405fn shadowed_host(module: &str, schemas: &HostSchemas, span: Span) -> Option<Diagnostic> {
1410 if !host_modules(schemas).any(|host| host == module) {
1411 return None;
1412 }
1413 Some(
1414 Diagnostic::error(
1415 "cove::resolve::module_shadows_host",
1416 format!("module `{module}` has the same name as the host module `{module}`"),
1417 )
1418 .at(span)
1419 .rule(
1420 "`use` resolves against the package's modules first, so a module named after a host module hides it.",
1421 )
1422 .help(format!(
1423 "rename the `{module}` module; the host namespace is not this package's to change"
1424 )),
1425 )
1426}
1427
1428fn ambiguous_module_path(
1431 path: &str,
1432 segments: &[&str],
1433 surfaces: &BTreeMap<&str, Surface>,
1434 span: Span,
1435) -> Option<Diagnostic> {
1436 if segments.len() < 2 {
1437 return None;
1438 }
1439 let owner = segments[..segments.len() - 1].join(".");
1440 let last = segments[segments.len() - 1];
1441 let declared = surfaces
1442 .get(owner.as_str())?
1443 .declarations
1444 .get(last)
1445 .filter(|declared| declared.exported)?;
1446 Some(
1447 Diagnostic::error(
1448 "cove::resolve::ambiguous_use",
1449 format!("`use {path}` names both the module `{path}` and `{last}`, exported by module `{owner}`"),
1450 )
1451 .at(span)
1452 .label(declared.span, format!("`{last}` is declared here"))
1453 .rule("A `use` path must have exactly one meaning.")
1454 .help(format!(
1455 "rename the `{path}` module or `{owner}.{last}`, so the path names one of them"
1456 )),
1457 )
1458}
1459
1460fn private_declaration(module: &str, name: &str, span: Span, declared: Span) -> Diagnostic {
1461 Diagnostic::error(
1462 "cove::resolve::private_declaration",
1463 format!("`{name}` is declared by module `{module}`, but is not exported"),
1464 )
1465 .at(span)
1466 .label(
1467 declared,
1468 format!("`{name}` is declared here, without `export`"),
1469 )
1470 .rule("An `export` declaration is public; other declarations are module-private.")
1471 .help(format!(
1472 "write `export` on `{name}` in module `{module}`, or import something else"
1473 ))
1474}
1475
1476fn no_such_declaration(module: &str, name: &str, surface: &Surface, span: Span) -> Diagnostic {
1477 let exported: Vec<String> = surface
1478 .declarations
1479 .iter()
1480 .filter(|(_, declared)| declared.exported)
1481 .map(|(name, _)| name.clone())
1482 .collect();
1483 Diagnostic::error(
1484 "cove::resolve::unknown_use",
1485 format!("module `{module}` declares no `{name}`, and `{module}` is not a host module"),
1486 )
1487 .at(span)
1488 .rule("`use` names a module of this package, one of its exported declarations, a host module, or one host operation.")
1489 .help(if exported.is_empty() {
1490 format!("module `{module}` exports nothing; write `export` on the declaration to import")
1491 } else {
1492 format!("module `{module}` exports {}", list_backticked(&exported))
1493 })
1494}
1495
1496fn unknown_use(
1497 path: &str,
1498 segments: &[&str],
1499 surfaces: &BTreeMap<&str, Surface>,
1500 span: Span,
1501) -> Diagnostic {
1502 let owner = segments[..segments.len() - 1].join(".");
1503 let modules: Vec<String> = surfaces.keys().map(|name| name.to_string()).collect();
1504 Diagnostic::error(
1505 "cove::resolve::unknown_use",
1506 format!("`use {path}` names neither a module of this package nor a host module"),
1507 )
1508 .at(span)
1509 .rule("`use` resolves against the package's modules first and the host registry second.")
1510 .help(format!(
1511 "there is no module `{path}` or `{owner}`; this package declares {}, and a host path names a module (`use console`) or one operation (`use console.println`)",
1512 list_backticked(&modules)
1513 ))
1514}
1515
1516fn check_method_collisions(program: &Program, errors: &mut Vec<Diagnostic>) {
1527 type MethodOf<'a> = (&'a str, &'a str, &'a str);
1530 type Site<'a> = (&'a str, Span);
1532
1533 let mut declared: BTreeMap<MethodOf, Vec<Site>> = BTreeMap::new();
1534 for (module, resolved) in &program.modules {
1535 for ((type_name, method), entry) in &resolved.methods {
1536 let Some(owner) = resolved.owner_of(type_name) else {
1537 continue;
1538 };
1539 declared
1540 .entry((owner, type_name.as_str(), method.as_str()))
1541 .or_default()
1542 .push((module.as_str(), entry.decl.name.span));
1543 }
1544 }
1545
1546 for ((type_module, type_name, method), sites) in declared {
1547 let [(first_module, first), rest @ ..] = sites.as_slice() else {
1548 continue;
1549 };
1550 for (module, span) in rest {
1551 errors.push(
1552 Diagnostic::error(
1553 "cove::resolve::duplicate_declaration",
1554 format!(
1555 "`{type_name}.{method}` is declared in module `{module}` and in module `{first_module}`"
1556 ),
1557 )
1558 .at(*span)
1559 .label(*first, format!("`{method}` first declared here"))
1560 .rule(
1561 "Each method name may be declared once per type, across every module: a conformance declared where its trait is must not collide with a method of the type's own module.",
1562 )
1563 .help(format!(
1564 "rename one of them, or move both into module `{type_module}`, which declares `{type_name}`"
1565 )),
1566 );
1567 }
1568 }
1569}
1570
1571fn check_import_cycles(edges: &[ImportEdge], errors: &mut Vec<Diagnostic>) {
1578 let mut graph: BTreeMap<&str, Vec<&ImportEdge>> = BTreeMap::new();
1579 for edge in edges {
1580 graph.entry(edge.from.as_str()).or_default().push(edge);
1581 }
1582
1583 let mut settled: BTreeSet<&str> = BTreeSet::new();
1584 let mut reported: BTreeSet<Vec<&str>> = BTreeSet::new();
1585 let roots: Vec<&str> = graph.keys().copied().collect();
1586 for root in roots {
1587 let mut stack: Vec<&ImportEdge> = Vec::new();
1588 walk_imports(
1589 root,
1590 &graph,
1591 &mut Vec::new(),
1592 &mut stack,
1593 &mut settled,
1594 &mut reported,
1595 errors,
1596 );
1597 }
1598}
1599
1600fn walk_imports<'a>(
1603 module: &'a str,
1604 graph: &BTreeMap<&'a str, Vec<&'a ImportEdge>>,
1605 path: &mut Vec<&'a str>,
1606 stack: &mut Vec<&'a ImportEdge>,
1607 settled: &mut BTreeSet<&'a str>,
1608 reported: &mut BTreeSet<Vec<&'a str>>,
1609 errors: &mut Vec<Diagnostic>,
1610) {
1611 if settled.contains(module) {
1612 return;
1613 }
1614 if let Some(start) = path.iter().position(|name| *name == module) {
1615 let mut cycle: Vec<&str> = path[start..].to_vec();
1616 cycle.push(module);
1617 let closing = *stack.last().expect("a cycle is closed by an edge");
1618 let mut members: Vec<&str> = cycle.clone();
1621 members.sort();
1622 members.dedup();
1623 if reported.insert(members) {
1624 errors.push(
1625 Diagnostic::error(
1626 "cove::resolve::import_cycle",
1627 format!(
1628 "module `{module}` imports itself through {}",
1629 cycle.join(" -> ")
1630 ),
1631 )
1632 .at(closing.span)
1633 .rule(
1634 "A module may not import, directly or transitively, a module that imports it.",
1635 )
1636 .help("move what both modules need into a third module they can each import"),
1637 );
1638 }
1639 return;
1640 }
1641
1642 path.push(module);
1643 for edge in graph.get(module).into_iter().flatten() {
1644 stack.push(edge);
1645 walk_imports(&edge.to, graph, path, stack, settled, reported, errors);
1646 stack.pop();
1647 }
1648 path.pop();
1649 settled.insert(module);
1650}
1651
1652const BUILTIN_SNAPSHOT_TRAIT: &str = "Snapshot";
1664
1665fn builtin_snapshot_trait(span: Span) -> Arc<TraitDecl> {
1674 Arc::new(TraitDecl {
1675 name: Spanned::new(BUILTIN_SNAPSHOT_TRAIT.to_string(), span),
1676 methods: vec![TraitMethod {
1677 doc: Some(
1678 "Returns an independent, mutable copy of this value's own graph, preserving \
1679 cycles and internal sharing where it has any."
1680 .to_string(),
1681 ),
1682 name: Spanned::new("snapshot".to_string(), span),
1683 is_async: false,
1684 receiver: Some(Receiver {
1685 is_var: false,
1686 span,
1687 }),
1688 params: Vec::new(),
1689 return_type: None,
1690 default: None,
1691 span,
1692 }],
1693 span,
1694 })
1695}
1696
1697#[allow(clippy::too_many_arguments)]
1706fn check_conformance(
1707 resolved: &mut ResolvedModule,
1708 module: &str,
1709 impl_block: &cove_syntax::ast::ImplBlock,
1710 conformance: Conformance,
1711 trait_decl: Arc<TraitDecl>,
1712 method_spans: &mut BTreeMap<(String, String), Span>,
1713 call_sites: &mut BTreeMap<FnKey, Vec<CallShape>>,
1714 opaque_fields: &OpaqueFields,
1715 schemas: &HostSchemas,
1716 errors: &mut Vec<Diagnostic>,
1717) {
1718 let Conformance {
1719 trait_name,
1720 type_name,
1721 span: header,
1722 ..
1723 } = conformance.clone();
1724 let trait_exported = resolved
1728 .traits
1729 .get(&trait_name)
1730 .map(|entry| entry.exported)
1731 .unwrap_or(true);
1732 let mut supplied: BTreeSet<String> = BTreeSet::new();
1733
1734 for inner in &impl_block.items {
1735 let ItemKind::Fn(decl) = &inner.kind else {
1736 errors.push(
1737 Diagnostic::error(
1738 "cove::resolve::invalid_impl_item",
1739 "only `fn` declarations are allowed inside an `impl` block",
1740 )
1741 .at(inner.span)
1742 .rule("An `impl` block may only contain method declarations."),
1743 );
1744 continue;
1745 };
1746 let method_name = decl.name.node.clone();
1747 let Some(declared) = trait_decl
1748 .methods
1749 .iter()
1750 .find(|m| m.name.node == method_name)
1751 else {
1752 errors.push(
1753 Diagnostic::error(
1754 "cove::resolve::unknown_trait_method",
1755 format!("`{trait_name}` declares no method `{method_name}`"),
1756 )
1757 .at(decl.name.span)
1758 .label(trait_decl.name.span, format!("`{trait_name}` is declared here"))
1759 .rule("An `impl Trait for Type` block supplies exactly the methods the trait declares; anything else belongs in the type's own `impl` block.")
1760 .help(format!(
1761 "declare `{method_name}` in `trait {trait_name}`, or move it to `impl {type_name}`"
1762 )),
1763 );
1764 continue;
1765 };
1766 supplied.insert(method_name);
1767 record_method(
1768 resolved,
1769 module,
1770 &type_name,
1771 Arc::new(decl.clone()),
1772 trait_exported,
1773 declared.doc.clone().or_else(|| inner.doc.clone()),
1774 None,
1775 method_spans,
1776 call_sites,
1777 opaque_fields,
1778 schemas,
1779 errors,
1780 );
1781 }
1782
1783 let missing: Vec<String> = trait_decl
1784 .methods
1785 .iter()
1786 .filter(|m| m.default.is_none() && !supplied.contains(&m.name.node))
1787 .map(|m| m.name.node.clone())
1788 .collect();
1789 if !missing.is_empty() {
1790 errors.push(
1791 Diagnostic::error(
1792 "cove::resolve::missing_trait_method",
1793 format!(
1794 "`{type_name}` does not conform to `{trait_name}`: missing {}",
1795 list_backticked(&missing)
1796 ),
1797 )
1798 .at(header)
1799 .label(
1800 trait_decl.name.span,
1801 format!("`{trait_name}` declares {}", list_backticked(&missing)),
1802 )
1803 .rule("A conformance supplies every method its trait declares without a default body.")
1804 .help(format!(
1805 "add {} to this block",
1806 missing
1807 .iter()
1808 .map(|m| format!("`fn {m}(...)`"))
1809 .collect::<Vec<_>>()
1810 .join(", ")
1811 )),
1812 );
1813 }
1814
1815 let mut methods = supplied.clone();
1818 for method in &trait_decl.methods {
1819 if supplied.contains(&method.name.node) {
1820 continue;
1821 }
1822 let Some(body) = &method.default else {
1823 continue;
1824 };
1825 methods.insert(method.name.node.clone());
1826 let decl = Arc::new(FnDecl {
1827 name: method.name.clone(),
1828 is_async: method.is_async,
1829 generics: Vec::new(),
1830 receiver: method.receiver,
1831 params: method.params.clone(),
1832 return_type: method.return_type.clone(),
1833 body: body.clone(),
1834 span: method.span,
1835 });
1836 record_method(
1837 resolved,
1838 module,
1839 &type_name,
1840 decl,
1841 trait_exported,
1842 method.doc.clone(),
1843 Some(trait_name.clone()),
1844 method_spans,
1845 call_sites,
1846 opaque_fields,
1847 schemas,
1848 errors,
1849 );
1850 }
1851
1852 resolved.conformances.insert(
1853 (trait_name, type_name),
1854 Conformance {
1855 methods,
1856 ..conformance
1857 },
1858 );
1859}
1860
1861#[allow(clippy::too_many_arguments)]
1866fn record_method(
1867 resolved: &mut ResolvedModule,
1868 module: &str,
1869 type_name: &str,
1870 decl: Arc<FnDecl>,
1871 exported: bool,
1872 doc: Option<String>,
1873 from_trait_default: Option<String>,
1874 method_spans: &mut BTreeMap<(String, String), Span>,
1875 call_sites: &mut BTreeMap<FnKey, Vec<CallShape>>,
1876 opaque_fields: &OpaqueFields,
1877 schemas: &HostSchemas,
1878 errors: &mut Vec<Diagnostic>,
1879) {
1880 let key = (type_name.to_string(), decl.name.node.clone());
1881 if let Some(existing_span) = method_spans.get(&key) {
1882 errors.push(
1883 Diagnostic::error(
1884 "cove::resolve::duplicate_declaration",
1885 format!(
1886 "`{type_name}.{}` is declared twice in module `{module}`",
1887 decl.name.node
1888 ),
1889 )
1890 .at(decl.name.span)
1891 .label(
1892 *existing_span,
1893 format!("`{}` first declared here", decl.name.node),
1894 )
1895 .rule(
1896 "Each method name may be declared once per type across a module's implementation units.",
1897 ),
1898 );
1899 return;
1900 }
1901 method_spans.insert(key.clone(), decl.name.span);
1902 let (capabilities, calls, open) = analyze_body(
1903 &decl,
1904 &resolved.host_uses,
1905 &resolved.host_items,
1906 opaque_fields,
1907 schemas,
1908 );
1909 call_sites.insert(
1910 FnKey::Method(type_name.to_string(), decl.name.node.clone()),
1911 calls,
1912 );
1913 resolved.methods.insert(
1914 key,
1915 FnEntry {
1916 decl,
1917 exported,
1918 is_test: false,
1920 doc,
1921 receiver_type: Some(type_name.to_string()),
1922 from_trait_default,
1923 direct_capabilities: capabilities,
1924 required_capabilities: BTreeSet::new(),
1925 direct_open_calls: open,
1926 open_calls: BTreeSet::new(),
1927 },
1928 );
1929}
1930
1931fn orphan_conformance(module: &str, trait_name: &str, type_name: &str, span: Span) -> Diagnostic {
1933 Diagnostic::error(
1934 "cove::resolve::orphan_conformance",
1935 format!(
1936 "module `{module}` declares neither `{trait_name}` nor `{type_name}`, so it cannot make one conform to the other"
1937 ),
1938 )
1939 .at(span)
1940 .rule("An `impl Trait for Type` is allowed only in the module that declares the trait or the module that declares the type, so that a conformance cannot appear from a module neither party knows about.")
1941 .help(format!(
1942 "move this block to the module that declares `{trait_name}` or the one that declares `{type_name}`"
1943 ))
1944}
1945
1946fn duplicate(spans: &mut BTreeMap<String, Span>, name: &str, span: Span) -> Option<Span> {
1949 if let Some(existing) = spans.get(name) {
1950 return Some(*existing);
1951 }
1952 spans.insert(name.to_string(), span);
1953 None
1954}
1955
1956fn duplicate_declaration(module: &str, name: &str, span: Span, first: Span) -> Diagnostic {
1957 Diagnostic::error(
1958 "cove::resolve::duplicate_declaration",
1959 format!("`{name}` is declared twice in module `{module}`"),
1960 )
1961 .at(span)
1962 .label(first, format!("`{name}` first declared here"))
1963 .rule("Each name may be declared once per module across its implementation units.")
1964}
1965
1966fn missing_doc(warnings: &mut Vec<Diagnostic>, item: &Item, name: &str, span: Span) {
1968 if item.exported && item.doc.is_none() {
1969 warnings.push(undocumented(name, span));
1970 }
1971}
1972
1973fn undocumented(name: &str, span: Span) -> Diagnostic {
1976 Diagnostic::warning(
1977 "cove::resolve::missing_doc",
1978 format!("exported `{name}` has no doc comment"),
1979 )
1980 .at(span)
1981 .rule("Public declarations without doc comments warn by default.")
1982 .help(format!("Add a `///` doc comment above `{name}`."))
1983}
1984
1985#[derive(Debug, Default)]
2004struct OpaqueFields {
2005 direct: BTreeSet<String>,
2008 containers: BTreeSet<String>,
2012}
2013
2014impl OpaqueFields {
2015 fn of(package: &Package) -> Self {
2021 let mut fields = OpaqueFields::default();
2022 for module in package.modules.values() {
2023 for unit in &module.units {
2024 for item in &unit.ast.items {
2025 let ItemKind::Struct(decl) = &item.kind else {
2026 continue;
2027 };
2028 let generics: BTreeSet<String> = decl
2029 .generics
2030 .iter()
2031 .map(|param| param.name.node.clone())
2032 .collect();
2033 for field in &decl.fields {
2034 match type_opacity(&field.ty, &generics) {
2035 Opacity::Direct => {
2036 fields.direct.insert(field.name.node.clone());
2037 }
2038 Opacity::Container => {
2039 fields.containers.insert(field.name.node.clone());
2040 }
2041 Opacity::None => {}
2042 }
2043 }
2044 }
2045 }
2046 }
2047 fields
2048 }
2049}
2050
2051#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2058enum Opacity {
2059 None,
2062 Container,
2065 Direct,
2070}
2071
2072fn analyze_body(
2090 decl: &FnDecl,
2091 host_uses: &BTreeSet<String>,
2092 host_items: &BTreeMap<String, String>,
2093 opaque_fields: &OpaqueFields,
2094 schemas: &HostSchemas,
2095) -> (BTreeSet<Capability>, Vec<CallShape>, BTreeSet<OpenCall>) {
2096 let generics: BTreeSet<String> = decl
2097 .generics
2098 .iter()
2099 .map(|param| param.name.node.clone())
2100 .collect();
2101 let mut walk = BodyWalk {
2102 host_uses,
2103 host_items,
2104 opaque_fields,
2105 schemas,
2106 enums: None,
2107 capabilities: BTreeSet::new(),
2108 calls: Vec::new(),
2109 errors: Vec::new(),
2110 warnings: Vec::new(),
2111 loop_depth: 0,
2112 generics,
2113 scopes: vec![Scope::default()],
2114 opaque: BTreeSet::new(),
2115 containers: BTreeSet::new(),
2116 open: BTreeSet::new(),
2117 };
2118 walk.bind_value("self");
2122 walk.bind_params(&decl.params);
2123 walk_block(&decl.body, &mut walk);
2124 (walk.capabilities, walk.calls, walk.open)
2125}
2126
2127fn type_opacity(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> Opacity {
2130 if is_opaque_type(ty, generics) {
2131 Opacity::Direct
2132 } else if mentions_opaque_type(ty, generics) {
2133 Opacity::Container
2134 } else {
2135 Opacity::None
2136 }
2137}
2138
2139fn is_opaque_type(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> bool {
2148 use cove_syntax::ast::TypeKind;
2149 match &ty.kind {
2150 TypeKind::Dyn(_) => true,
2151 TypeKind::Named { path, .. } => path.len() == 1 && generics.contains(path[0].node.as_str()),
2152 TypeKind::Unit | TypeKind::Fn { .. } => false,
2153 }
2154}
2155
2156fn mentions_opaque_type(ty: &cove_syntax::ast::Type, generics: &BTreeSet<String>) -> bool {
2163 use cove_syntax::ast::TypeKind;
2164 match &ty.kind {
2165 TypeKind::Dyn(_) => true,
2166 TypeKind::Unit => false,
2167 TypeKind::Named { path, args } => {
2168 (path.len() == 1 && generics.contains(path[0].node.as_str()))
2169 || args.iter().any(|arg| mentions_opaque_type(arg, generics))
2170 }
2171 TypeKind::Fn {
2172 params,
2173 return_type,
2174 ..
2175 } => {
2176 params.iter().any(|param| {
2177 param
2178 .ty
2179 .as_ref()
2180 .is_some_and(|ty| mentions_opaque_type(ty, generics))
2181 }) || return_type
2182 .as_ref()
2183 .is_some_and(|ty| mentions_opaque_type(ty, generics))
2184 }
2185 }
2186}
2187
2188fn value_is_opaque(expr: &Expr, walk: &BodyWalk) -> bool {
2202 match &expr.kind {
2203 ExprKind::Ident(name) => walk.is_opaque(name),
2204 ExprKind::Field { base, name } => {
2205 walk.opaque_fields.direct.contains(name.node.as_str()) || value_is_opaque(base, walk)
2206 }
2207 _ => mentions_opaque(expr, walk),
2208 }
2209}
2210
2211fn holds_opaque_container(expr: &Expr, walk: &BodyWalk) -> bool {
2214 match &expr.kind {
2215 ExprKind::Ident(name) => walk.is_container(name),
2216 ExprKind::Field { name, .. } => walk.opaque_fields.containers.contains(name.node.as_str()),
2217 _ => false,
2218 }
2219}
2220
2221fn binding_opacity(ty: Option<&cove_syntax::ast::Type>, value: &Expr, walk: &BodyWalk) -> Opacity {
2235 let written = ty.map_or(Opacity::None, |ty| type_opacity(ty, &walk.generics));
2236 let read = if value_is_opaque(value, walk) {
2237 Opacity::Direct
2238 } else if holds_opaque_container(value, walk) {
2239 Opacity::Container
2240 } else if mentions_opaque(value, walk) {
2241 Opacity::Direct
2242 } else {
2243 Opacity::None
2244 };
2245 written.max(read)
2246}
2247
2248fn mentions_opaque(expr: &Expr, walk: &BodyWalk) -> bool {
2255 let any = |exprs: &[Expr]| exprs.iter().any(|e| mentions_opaque(e, walk));
2256 match &expr.kind {
2257 ExprKind::Ident(name) => walk.is_opaque(name) || walk.is_container(name),
2258 ExprKind::Field { base, name } => {
2259 walk.opaque_fields.direct.contains(name.node.as_str())
2260 || walk.opaque_fields.containers.contains(name.node.as_str())
2261 || mentions_opaque(base, walk)
2262 }
2263 ExprKind::Call {
2264 callee,
2265 args,
2266 trailing,
2267 ..
2268 } => {
2269 mentions_opaque(callee, walk)
2270 || args.iter().any(|arg| mentions_opaque(&arg.value, walk))
2271 || trailing
2272 .as_ref()
2273 .is_some_and(|tail| mentions_opaque(tail, walk))
2274 }
2275 ExprKind::Try(inner) | ExprKind::Await(inner) | ExprKind::Unary { operand: inner, .. } => {
2276 mentions_opaque(inner, walk)
2277 }
2278 ExprKind::Binary { lhs, rhs, .. } => {
2279 mentions_opaque(lhs, walk) || mentions_opaque(rhs, walk)
2280 }
2281 ExprKind::ArrayLit(items) => any(items),
2282 ExprKind::Str(parts) => parts.iter().any(|part| match part {
2283 StrPart::Interpolation(inner) => mentions_opaque(inner, walk),
2284 StrPart::Text(_) => false,
2285 }),
2286 ExprKind::Block(block) | ExprKind::Scope { body: block, .. } => {
2287 block_mentions_opaque(block, walk)
2288 }
2289 ExprKind::If {
2290 then_branch,
2291 else_branch,
2292 ..
2293 } => {
2294 block_mentions_opaque(then_branch, walk)
2295 || else_branch
2296 .as_ref()
2297 .is_some_and(|branch| mentions_opaque(branch, walk))
2298 }
2299 ExprKind::Match { arms, .. } => arms.iter().any(|arm| mentions_opaque(&arm.body, walk)),
2300 _ => false,
2301 }
2302}
2303
2304fn block_mentions_opaque(block: &Block, walk: &BodyWalk) -> bool {
2313 block
2314 .tail
2315 .as_ref()
2316 .is_some_and(|tail| mentions_opaque(tail, walk))
2317}
2318
2319type EnumsInScope<'a> = BTreeMap<&'a str, &'a EnumEntry>;
2322
2323fn check_bodies(
2335 program: &Program,
2336 schemas: &HostSchemas,
2337 errors: &mut Vec<Diagnostic>,
2338 warnings: &mut Vec<Diagnostic>,
2339) {
2340 for resolved in program.modules.values() {
2341 let enums = enums_in_scope(program, resolved);
2342 for entry in resolved.functions.values() {
2343 check_body(
2344 &entry.decl.body,
2345 resolved,
2346 &enums,
2347 schemas,
2348 errors,
2349 warnings,
2350 );
2351 }
2352 for entry in resolved.methods.values() {
2353 if entry.from_trait_default.is_none() {
2358 check_body(
2359 &entry.decl.body,
2360 resolved,
2361 &enums,
2362 schemas,
2363 errors,
2364 warnings,
2365 );
2366 }
2367 }
2368 for entry in resolved.traits.values() {
2369 for method in &entry.decl.methods {
2370 if let Some(body) = &method.default {
2371 check_body(body, resolved, &enums, schemas, errors, warnings);
2372 }
2373 }
2374 }
2375 }
2376}
2377
2378fn enums_in_scope<'a>(program: &'a Program, resolved: &'a ResolvedModule) -> EnumsInScope<'a> {
2383 let mut enums: EnumsInScope<'a> = resolved
2384 .enums
2385 .iter()
2386 .map(|(name, entry)| (name.as_str(), entry))
2387 .collect();
2388 for (name, owner) in &resolved.imports {
2389 let Some(entry) = program
2390 .modules
2391 .get(owner)
2392 .and_then(|owner| owner.enums.get(name))
2393 else {
2394 continue;
2395 };
2396 enums.insert(name.as_str(), entry);
2397 }
2398 enums
2399}
2400
2401fn check_body(
2402 body: &Block,
2403 resolved: &ResolvedModule,
2404 enums: &EnumsInScope,
2405 schemas: &HostSchemas,
2406 errors: &mut Vec<Diagnostic>,
2407 warnings: &mut Vec<Diagnostic>,
2408) {
2409 let no_opaque_fields = OpaqueFields::default();
2410 let mut walk = BodyWalk {
2411 host_uses: &resolved.host_uses,
2412 host_items: &resolved.host_items,
2413 schemas,
2414 enums: Some(enums),
2415 capabilities: BTreeSet::new(),
2416 calls: Vec::new(),
2417 errors: Vec::new(),
2418 warnings: Vec::new(),
2419 loop_depth: 0,
2420 generics: BTreeSet::new(),
2424 opaque_fields: &no_opaque_fields,
2425 scopes: Vec::new(),
2426 opaque: BTreeSet::new(),
2427 containers: BTreeSet::new(),
2428 open: BTreeSet::new(),
2429 };
2430 walk_block(body, &mut walk);
2431 errors.extend(walk.errors);
2432 warnings.extend(walk.warnings);
2433}
2434
2435struct BodyWalk<'a> {
2445 host_uses: &'a BTreeSet<String>,
2446 host_items: &'a BTreeMap<String, String>,
2447 schemas: &'a HostSchemas,
2450 enums: Option<&'a EnumsInScope<'a>>,
2451 capabilities: BTreeSet<Capability>,
2452 calls: Vec<CallShape>,
2453 errors: Vec<Diagnostic>,
2454 warnings: Vec<Diagnostic>,
2455 loop_depth: u32,
2461 generics: BTreeSet<String>,
2465 opaque_fields: &'a OpaqueFields,
2468 scopes: Vec<Scope>,
2475 opaque: BTreeSet<String>,
2486 containers: BTreeSet<String>,
2490 open: BTreeSet<OpenCall>,
2492}
2493
2494#[derive(Debug, Default)]
2496struct Scope {
2497 values: BTreeSet<String>,
2501 functions: BTreeSet<String>,
2505}
2506
2507impl BodyWalk<'_> {
2508 fn push_scope(&mut self) {
2509 self.scopes.push(Scope::default());
2510 }
2511
2512 fn pop_scope(&mut self) {
2513 self.scopes.pop();
2514 }
2515
2516 fn bind_value(&mut self, name: &str) {
2518 if let Some(scope) = self.scopes.last_mut() {
2519 scope.values.insert(name.to_string());
2520 }
2521 }
2522
2523 fn bind_local_fn(&mut self, name: &str) {
2525 if let Some(scope) = self.scopes.last_mut() {
2526 scope.functions.insert(name.to_string());
2527 }
2528 }
2529
2530 fn bind_params(&mut self, params: &[cove_syntax::ast::Param]) {
2536 for param in params {
2537 let mut opacity = param
2538 .ty
2539 .as_ref()
2540 .map_or(Opacity::None, |ty| type_opacity(ty, &self.generics));
2541 if param.variadic {
2542 opacity = opacity.min(Opacity::Container);
2543 }
2544 self.bind_value(¶m.name.node);
2545 self.mark(¶m.name.node, opacity);
2546 }
2547 }
2548
2549 fn mark(&mut self, name: &str, opacity: Opacity) {
2551 match opacity {
2552 Opacity::None => {}
2553 Opacity::Container => {
2554 self.containers.insert(name.to_string());
2555 }
2556 Opacity::Direct => {
2557 self.opaque.insert(name.to_string());
2558 }
2559 }
2560 }
2561
2562 fn binds_value(&self, name: &str) -> bool {
2563 self.scopes.iter().any(|scope| scope.values.contains(name))
2564 }
2565
2566 fn binds_local_fn(&self, name: &str) -> bool {
2567 self.scopes
2568 .iter()
2569 .any(|scope| scope.functions.contains(name))
2570 }
2571
2572 fn is_opaque(&self, name: &str) -> bool {
2573 self.opaque.contains(name)
2574 }
2575
2576 fn is_container(&self, name: &str) -> bool {
2577 self.containers.contains(name)
2578 }
2579}
2580
2581#[derive(Clone, Debug)]
2585enum CallShape {
2586 Ident(String),
2588 Field {
2592 receiver_ident: Option<String>,
2593 method: String,
2594 },
2595 Reference(String),
2603}
2604
2605#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
2607pub enum FnKey {
2608 Fn(String),
2610 Method(String, String),
2612}
2613
2614fn walk_block(block: &Block, walk: &mut BodyWalk) {
2615 walk.push_scope();
2616 for stmt in &block.statements {
2617 walk_stmt(stmt, walk);
2618 }
2619 if let Some(tail) = &block.tail {
2620 walk_expr(tail, walk);
2621 }
2622 walk.pop_scope();
2623}
2624
2625fn walk_stmt(stmt: &Stmt, walk: &mut BodyWalk) {
2626 match &stmt.kind {
2627 StmtKind::Let {
2628 name, ty, value, ..
2629 } => {
2630 walk_expr(value, walk);
2633 let opacity = binding_opacity(ty.as_ref(), value, walk);
2634 walk.bind_value(&name.node);
2635 walk.mark(&name.node, opacity);
2636 }
2637 StmtKind::Expr(expr) => walk_expr(expr, walk),
2638 StmtKind::Item(item) => {
2645 if let ItemKind::Fn(decl) = &item.kind {
2646 walk.bind_local_fn(&decl.name.node);
2647 walk.push_scope();
2648 walk.bind_params(&decl.params);
2649 let outer_depth = std::mem::replace(&mut walk.loop_depth, 0);
2653 walk_block(&decl.body, walk);
2654 walk.loop_depth = outer_depth;
2655 walk.pop_scope();
2656 }
2657 }
2658 }
2659}
2660
2661fn walk_expr(expr: &Expr, walk: &mut BodyWalk) {
2662 match &expr.kind {
2663 ExprKind::Int(_)
2664 | ExprKind::Float(_)
2665 | ExprKind::Bool(_)
2666 | ExprKind::Duration(_)
2667 | ExprKind::Unit => {}
2668 ExprKind::Ident(name) => {
2676 if !walk.binds_value(name) && !walk.binds_local_fn(name) {
2677 walk.calls.push(CallShape::Reference(name.clone()));
2678 }
2679 }
2680 ExprKind::Str(parts) => {
2681 for part in parts {
2682 if let StrPart::Interpolation(inner) = part {
2683 walk_expr(inner, walk);
2684 }
2685 }
2686 }
2687 ExprKind::ArrayLit(items) => {
2688 for item in items {
2689 walk_expr(item, walk);
2690 }
2691 }
2692 ExprKind::Field { base, .. } => walk_expr(base, walk),
2693 ExprKind::Call {
2694 callee,
2695 args,
2696 trailing,
2697 ..
2698 } => {
2699 if let Some(capability) =
2700 call_capability(callee, walk.host_uses, walk.host_items, walk.schemas)
2701 {
2702 walk.capabilities.insert(capability);
2703 }
2704 match call_shape(callee) {
2705 Some(CallShape::Ident(name)) if walk.binds_local_fn(&name) => {}
2709 Some(CallShape::Ident(name)) if walk.binds_value(&name) => {
2712 walk.open.insert(OpenCall::FunctionValue);
2713 }
2714 Some(shape) => {
2715 if let (CallShape::Field { .. }, ExprKind::Field { base: receiver, .. }) =
2720 (&shape, &callee.kind)
2721 {
2722 if value_is_opaque(receiver, walk) {
2723 walk.open.insert(OpenCall::DynamicDispatch);
2724 }
2725 }
2726 walk.calls.push(shape);
2727 }
2728 None => {
2731 walk.open.insert(OpenCall::FunctionValue);
2732 }
2733 }
2734 walk_expr(callee, walk);
2735 for arg in args {
2736 walk_expr(&arg.value, walk);
2737 }
2738 if let Some(trailing) = trailing {
2739 walk_expr(trailing, walk);
2740 }
2741 }
2742 ExprKind::Unary { operand, .. } => walk_expr(operand, walk),
2743 ExprKind::Binary { lhs, rhs, .. } => {
2744 walk_expr(lhs, walk);
2745 walk_expr(rhs, walk);
2746 }
2747 ExprKind::Assign { target, value, .. } => {
2748 walk_expr(target, walk);
2749 walk_expr(value, walk);
2750 }
2751 ExprKind::Try(inner) | ExprKind::Await(inner) => walk_expr(inner, walk),
2752 ExprKind::Block(block) => walk_block(block, walk),
2753 ExprKind::If {
2754 condition,
2755 then_branch,
2756 else_branch,
2757 } => {
2758 walk_expr(condition, walk);
2759 walk_block(then_branch, walk);
2760 if let Some(else_branch) = else_branch {
2761 walk_expr(else_branch, walk);
2762 }
2763 }
2764 ExprKind::Match { scrutinee, arms } => {
2765 walk_expr(scrutinee, walk);
2766 check_match_arms(expr, arms, walk);
2767 for MatchArm { body, .. } in arms {
2768 walk_expr(body, walk);
2769 }
2770 }
2771 ExprKind::For {
2772 binding,
2773 iterable,
2774 body,
2775 } => {
2776 walk_expr(iterable, walk);
2777 let opaque = mentions_opaque(iterable, walk);
2781 walk.push_scope();
2782 walk.bind_value(&binding.node);
2783 if opaque {
2784 walk.mark(&binding.node, Opacity::Direct);
2785 }
2786 walk.loop_depth += 1;
2787 walk_block(body, walk);
2788 walk.loop_depth -= 1;
2789 walk.pop_scope();
2790 }
2791 ExprKind::While { condition, body } => {
2792 walk_expr(condition, walk);
2793 walk.loop_depth += 1;
2794 walk_block(body, walk);
2795 walk.loop_depth -= 1;
2796 }
2797 ExprKind::Return(inner) => {
2798 if let Some(inner) = inner {
2799 walk_expr(inner, walk);
2800 }
2801 }
2802 ExprKind::Break(inner) => {
2803 if let Some(inner) = inner {
2804 walk_expr(inner, walk);
2805 }
2806 check_in_loop(expr, "break", walk);
2807 }
2808 ExprKind::Continue => check_in_loop(expr, "continue", walk),
2809 ExprKind::Lambda { params, body, .. } => {
2813 let outer_depth = std::mem::replace(&mut walk.loop_depth, 0);
2814 walk.push_scope();
2815 walk.bind_params(params);
2816 walk_block(body, walk);
2817 walk.pop_scope();
2818 walk.loop_depth = outer_depth;
2819 }
2820 ExprKind::Scope { body, .. } => walk_block(body, walk),
2821 ExprKind::Range { start, end, .. } => {
2822 walk_expr(start, walk);
2823 walk_expr(end, walk);
2824 }
2825 }
2826}
2827
2828fn check_in_loop(expr: &Expr, keyword: &str, walk: &mut BodyWalk) {
2832 if walk.loop_depth == 0 {
2833 walk.errors.push(
2834 Diagnostic::error(
2835 format!("cove::resolve::{keyword}_outside_loop"),
2836 format!("`{keyword}` outside a loop"),
2837 )
2838 .at(expr.span)
2839 .rule(format!(
2840 "`{keyword}` only makes sense inside a `for` or `while` loop, and cannot reach one outside a closure."
2841 ))
2842 .help(format!("move this `{keyword}` inside an enclosing loop, or remove it")),
2843 );
2844 }
2845}
2846
2847fn check_match_arms(match_expr: &Expr, arms: &[MatchArm], walk: &mut BodyWalk) {
2859 let Some(enums) = walk.enums else {
2860 return;
2861 };
2862
2863 let catch_all_index = arms.iter().position(|arm| is_catch_all(&arm.pattern));
2864 if let Some(catch_all_index) = catch_all_index {
2865 let catch_all_span = arms[catch_all_index].span;
2866 for arm in &arms[catch_all_index + 1..] {
2867 walk.warnings.push(
2868 Diagnostic::warning(
2869 "cove::resolve::unreachable_match_arm",
2870 "this `match` arm can never run",
2871 )
2872 .at(arm.span)
2873 .label(
2874 catch_all_span,
2875 "unreachable because this earlier arm matches everything",
2876 )
2877 .rule("An arm after a `_` or binding arm can never run."),
2878 );
2879 }
2880 }
2881 let has_catch_all = catch_all_index.is_some();
2882
2883 if let Some(target) = resolve_target_enum(arms, enums) {
2884 let valid_cases = target.case_names();
2885 let mut seen: BTreeMap<&str, Span> = BTreeMap::new();
2889 let mut reachable: Vec<&Pattern> = Vec::new();
2892 for arm in arms {
2893 let PatternKind::Variant { path, .. } = &arm.pattern.kind else {
2894 continue;
2895 };
2896 let case_name = path.last().expect("a variant path is never empty");
2897 if !valid_cases.iter().any(|case| case == &case_name.node) {
2898 walk.errors.push(
2899 Diagnostic::error(
2900 "cove::resolve::unknown_enum_case",
2901 format!(
2902 "`{}` is not a case of `{}`",
2903 case_name.node,
2904 target.display_name()
2905 ),
2906 )
2907 .at(arm.pattern.span)
2908 .rule("Every `match` arm must name a case its enum declares.")
2909 .help(format!(
2910 "`{}` declares {}",
2911 target.display_name(),
2912 list_backticked(&valid_cases)
2913 )),
2914 );
2915 continue;
2916 }
2917 let covering = reachable
2918 .iter()
2919 .find(|earlier| pattern_covers(earlier, &arm.pattern));
2920 if let Some(covering) = covering {
2921 walk.errors.push(
2922 Diagnostic::error(
2923 "cove::resolve::duplicate_match_arm",
2924 format!(
2925 "this `{}` arm is already covered by an earlier arm",
2926 case_name.node
2927 ),
2928 )
2929 .at(arm.pattern.span)
2930 .label(
2931 covering.span,
2932 "this earlier arm matches every value it would",
2933 )
2934 .rule("A `match` arm must match some value no earlier arm matches."),
2935 );
2936 } else {
2937 reachable.push(&arm.pattern);
2938 }
2939 seen.entry(case_name.node.as_str())
2940 .or_insert(arm.pattern.span);
2941 }
2942
2943 if !has_catch_all {
2944 let missing: Vec<String> = valid_cases
2945 .iter()
2946 .filter(|case| !seen.contains_key(case.as_str()))
2947 .map(|case| target.qualified(case))
2948 .collect();
2949 if !missing.is_empty() {
2950 walk.errors.push(non_exhaustive_enum_match(
2951 match_expr.span,
2952 &target,
2953 &missing,
2954 ));
2955 }
2956 }
2957 return;
2958 }
2959
2960 check_literal_arms(match_expr, arms, catch_all_index, has_catch_all, walk);
2961}
2962
2963fn check_literal_arms(
2977 match_expr: &Expr,
2978 arms: &[MatchArm],
2979 catch_all_index: Option<usize>,
2980 has_catch_all: bool,
2981 walk: &mut BodyWalk,
2982) {
2983 let literal_indices: Vec<usize> = arms
2984 .iter()
2985 .enumerate()
2986 .filter(|(_, arm)| is_literal(&arm.pattern))
2987 .map(|(index, _)| index)
2988 .collect();
2989 if literal_indices.is_empty() {
2990 return;
2991 }
2992
2993 let all_bool = literal_indices
2994 .iter()
2995 .all(|&index| literal_bool_value(&arms[index].pattern).is_some());
2996
2997 if !all_bool {
2998 if !has_catch_all {
2999 walk.errors.push(
3000 Diagnostic::error(
3001 "cove::resolve::non_exhaustive_match",
3002 "`match` over literal patterns needs a `_` or binding arm",
3003 )
3004 .at(match_expr.span)
3005 .rule("`match` must cover every enum case.")
3006 .help(
3007 "add a `_` arm, or a binding arm, to cover every value the literal arms do not",
3008 ),
3009 );
3010 }
3011 return;
3012 }
3013
3014 let mut seen: BTreeMap<bool, Span> = BTreeMap::new();
3015 let mut covered_at: Option<usize> = None;
3016 for &index in &literal_indices {
3017 let value = literal_bool_value(&arms[index].pattern).expect("checked all_bool above");
3018 if let Some(first_span) = seen.get(&value) {
3019 walk.errors.push(
3020 Diagnostic::error(
3021 "cove::resolve::duplicate_match_arm",
3022 format!("`{value}` is already covered by an earlier arm"),
3023 )
3024 .at(arms[index].pattern.span)
3025 .label(*first_span, format!("`{value}` first matched here"))
3026 .rule("Each value of `Bool` may be matched by at most one arm."),
3027 );
3028 continue;
3029 }
3030 seen.insert(value, arms[index].pattern.span);
3031 if seen.len() == 2 && covered_at.is_none() {
3032 covered_at = Some(index);
3033 }
3034 }
3035
3036 if let Some(catch_all_index) = catch_all_index {
3037 if let Some(covered_at) = covered_at {
3038 if covered_at < catch_all_index {
3039 walk.warnings.push(
3040 Diagnostic::warning(
3041 "cove::resolve::unreachable_match_arm",
3042 "this `match` arm can never run",
3043 )
3044 .at(arms[catch_all_index].span)
3045 .label(
3046 arms[covered_at].span,
3047 "unreachable because `true` and `false` are already covered here",
3048 )
3049 .rule("A `match` over `Bool` covering both `true` and `false` leaves no value for a later `_` or binding arm."),
3050 );
3051 }
3052 }
3053 return;
3054 }
3055
3056 if seen.len() < 2 {
3057 let missing = if seen.contains_key(&true) {
3058 "false"
3059 } else {
3060 "true"
3061 };
3062 walk.errors.push(
3063 Diagnostic::error(
3064 "cove::resolve::non_exhaustive_match",
3065 format!("this `match` does not cover `{missing}`"),
3066 )
3067 .at(match_expr.span)
3068 .rule("A `match` over `Bool` must cover both `true` and `false`.")
3069 .help(format!("add a `{missing} => ...` arm, or add a `_` arm")),
3070 );
3071 }
3072}
3073
3074fn literal_bool_value(pattern: &Pattern) -> Option<bool> {
3077 let PatternKind::Literal(expr) = &pattern.kind else {
3078 return None;
3079 };
3080 match expr.kind {
3081 ExprKind::Bool(value) => Some(value),
3082 _ => None,
3083 }
3084}
3085
3086fn pattern_covers(earlier: &Pattern, later: &Pattern) -> bool {
3124 match (&earlier.kind, &later.kind) {
3125 (PatternKind::Wildcard | PatternKind::Binding(_), _) => true,
3126 (_, PatternKind::Wildcard | PatternKind::Binding(_)) => false,
3127 (PatternKind::Literal(earlier), PatternKind::Literal(later)) => {
3128 same_literal(earlier, later)
3129 }
3130 (
3131 PatternKind::Variant {
3132 path: earlier_path,
3133 payload: earlier_payload,
3134 },
3135 PatternKind::Variant {
3136 path: later_path,
3137 payload: later_payload,
3138 },
3139 ) => {
3140 let earlier_case = earlier_path.last().expect("a variant path is never empty");
3141 let later_case = later_path.last().expect("a variant path is never empty");
3142 earlier_case.node == later_case.node
3143 && (0..earlier_payload.len().max(later_payload.len()))
3144 .all(|slot| covers_slot(earlier_payload.get(slot), later_payload.get(slot)))
3145 }
3146 _ => false,
3147 }
3148}
3149
3150fn covers_slot(earlier: Option<&Pattern>, later: Option<&Pattern>) -> bool {
3154 match (earlier, later) {
3155 (None, _) => true,
3156 (Some(earlier), None) => is_catch_all(earlier),
3157 (Some(earlier), Some(later)) => pattern_covers(earlier, later),
3158 }
3159}
3160
3161fn same_literal(earlier: &Expr, later: &Expr) -> bool {
3167 match (&earlier.kind, &later.kind) {
3168 (ExprKind::Int(earlier), ExprKind::Int(later)) => earlier == later,
3169 (ExprKind::Bool(earlier), ExprKind::Bool(later)) => earlier == later,
3170 (ExprKind::Duration(earlier), ExprKind::Duration(later)) => earlier == later,
3171 (ExprKind::Str(earlier), ExprKind::Str(later)) => {
3172 match (plain_text(earlier), plain_text(later)) {
3173 (Some(earlier), Some(later)) => earlier == later,
3174 _ => false,
3175 }
3176 }
3177 _ => false,
3178 }
3179}
3180
3181fn plain_text(parts: &[StrPart]) -> Option<String> {
3184 let mut text = String::new();
3185 for part in parts {
3186 match part {
3187 StrPart::Text(chunk) => text.push_str(chunk),
3188 StrPart::Interpolation(_) => return None,
3189 }
3190 }
3191 Some(text)
3192}
3193
3194fn is_catch_all(pattern: &Pattern) -> bool {
3195 matches!(
3196 pattern.kind,
3197 PatternKind::Wildcard | PatternKind::Binding(_)
3198 )
3199}
3200
3201fn is_literal(pattern: &Pattern) -> bool {
3202 matches!(pattern.kind, PatternKind::Literal(_))
3203}
3204
3205enum TargetEnum<'a> {
3209 Declared(&'a EnumEntry),
3210 Builtin(&'static cove_schema::builtins::BuiltinSchema),
3216}
3217
3218impl TargetEnum<'_> {
3219 fn display_name(&self) -> &str {
3220 match self {
3221 TargetEnum::Declared(entry) => &entry.decl.name.node,
3222 TargetEnum::Builtin(schema) => schema.name,
3223 }
3224 }
3225
3226 fn case_names(&self) -> Vec<String> {
3228 match self {
3229 TargetEnum::Declared(entry) => entry
3230 .decl
3231 .cases
3232 .iter()
3233 .map(|case| case.name.node.clone())
3234 .collect(),
3235 TargetEnum::Builtin(schema) => schema
3236 .cases
3237 .iter()
3238 .map(|case| case.name.to_string())
3239 .collect(),
3240 }
3241 }
3242
3243 fn qualified(&self, case: &str) -> String {
3247 match self {
3248 TargetEnum::Declared(entry) => format!("{}.{case}", entry.decl.name.node),
3249 TargetEnum::Builtin(_) => case.to_string(),
3250 }
3251 }
3252}
3253
3254fn resolve_target_enum<'a>(arms: &[MatchArm], enums: &EnumsInScope<'a>) -> Option<TargetEnum<'a>> {
3264 let mut candidate: Option<String> = None;
3265 for arm in arms {
3266 let PatternKind::Variant { path, .. } = &arm.pattern.kind else {
3267 continue;
3268 };
3269 let this_enum = match path.as_slice() {
3270 [case] => bare_case_enum(&case.node, enums)?,
3271 [enum_name, _case] => enum_name.node.clone(),
3272 _ => return None,
3273 };
3274 match &candidate {
3275 None => candidate = Some(this_enum),
3276 Some(existing) if *existing != this_enum => return None,
3277 _ => {}
3278 }
3279 }
3280
3281 let candidate = candidate?;
3282 match cove_schema::builtin(&candidate) {
3283 Some(schema) if schema.is_enum() => Some(TargetEnum::Builtin(schema)),
3284 _ => enums
3285 .get(candidate.as_str())
3286 .copied()
3287 .map(TargetEnum::Declared),
3288 }
3289}
3290
3291fn bare_case_enum(case_name: &str, enums: &EnumsInScope) -> Option<String> {
3295 if let Some(schema) = cove_schema::builtins::enum_declaring(case_name) {
3296 return Some(schema.name.to_string());
3297 }
3298 let mut matches = enums
3299 .iter()
3300 .filter(|(_, entry)| {
3301 entry
3302 .decl
3303 .cases
3304 .iter()
3305 .any(|case| case.name.node == case_name)
3306 })
3307 .map(|(name, _)| (*name).to_string());
3308 let first = matches.next()?;
3309 if matches.next().is_some() {
3310 return None;
3311 }
3312 Some(first)
3313}
3314
3315fn list_backticked(items: &[String]) -> String {
3316 items
3317 .iter()
3318 .map(|item| format!("`{item}`"))
3319 .collect::<Vec<_>>()
3320 .join(", ")
3321}
3322
3323fn non_exhaustive_enum_match(span: Span, target: &TargetEnum, missing: &[String]) -> Diagnostic {
3326 let list = list_backticked(missing);
3327 let help = if missing.len() == 1 {
3328 format!("add an arm for {list}, or add a `_` arm")
3329 } else {
3330 format!("add arms for {list}, or add a `_` arm")
3331 };
3332 Diagnostic::error(
3333 "cove::resolve::non_exhaustive_match",
3334 format!(
3335 "`match` does not cover every case of `{}`: missing {list}",
3336 target.display_name()
3337 ),
3338 )
3339 .at(span)
3340 .rule("`match` must cover every enum case.")
3341 .help(help)
3342}
3343
3344fn call_shape(callee: &Expr) -> Option<CallShape> {
3358 match &callee.kind {
3359 ExprKind::Ident(name) => Some(CallShape::Ident(name.clone())),
3360 ExprKind::Field { base, name } => {
3361 let receiver_ident = match &base.kind {
3362 ExprKind::Ident(base_name) => Some(base_name.clone()),
3363 _ => None,
3364 };
3365 Some(CallShape::Field {
3366 receiver_ident,
3367 method: name.node.clone(),
3368 })
3369 }
3370 _ => None,
3371 }
3372}
3373
3374pub type Node = (String, FnKey);
3377
3378#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
3384pub enum CallPrecision {
3385 Exact,
3389 Approximate,
3393}
3394
3395#[allow(clippy::type_complexity)]
3404fn package_call_graph(
3405 program: &Program,
3406 call_sites: &BTreeMap<Node, Vec<CallShape>>,
3407) -> (
3408 BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
3409 BTreeMap<Node, BTreeSet<OpenCall>>,
3410) {
3411 let reachable: BTreeMap<&str, BTreeSet<&str>> = program
3412 .modules
3413 .keys()
3414 .map(|name| (name.as_str(), reachable_modules(program, name)))
3415 .collect();
3416 let mut graph = BTreeMap::new();
3417 let mut open = BTreeMap::new();
3418 for ((module, key), calls) in call_sites {
3419 let (targets, unresolved) =
3420 resolve_calls(program, module, calls, &reachable[module.as_str()]);
3421 let node = (module.clone(), key.clone());
3422 if !unresolved.is_empty() {
3423 open.insert(node.clone(), unresolved);
3424 }
3425 graph.insert(node, targets);
3426 }
3427 (graph, open)
3428}
3429
3430fn merge_open_calls(program: &mut Program, unresolved: &BTreeMap<Node, BTreeSet<OpenCall>>) {
3433 for (module, resolved) in program.modules.iter_mut() {
3434 for (name, entry) in resolved.functions.iter_mut() {
3435 if let Some(open) = unresolved.get(&(module.clone(), FnKey::Fn(name.clone()))) {
3436 entry.direct_open_calls.extend(open.iter().copied());
3437 }
3438 }
3439 for ((type_name, method_name), entry) in resolved.methods.iter_mut() {
3440 let key = FnKey::Method(type_name.clone(), method_name.clone());
3441 if let Some(open) = unresolved.get(&(module.clone(), key)) {
3442 entry.direct_open_calls.extend(open.iter().copied());
3443 }
3444 }
3445 }
3446}
3447
3448fn reachable_modules<'a>(program: &'a Program, module: &'a str) -> BTreeSet<&'a str> {
3456 let mut reached: BTreeSet<&str> = BTreeSet::new();
3457 let mut pending: Vec<&str> = vec![module];
3458 while let Some(name) = pending.pop() {
3459 if !reached.insert(name) {
3460 continue;
3461 }
3462 if let Some(resolved) = program.modules.get(name) {
3463 pending.extend(resolved.dependencies());
3464 }
3465 }
3466 reached
3467}
3468
3469fn resolve_calls(
3497 program: &Program,
3498 module: &str,
3499 calls: &[CallShape],
3500 reachable: &BTreeSet<&str>,
3501) -> (BTreeMap<Node, CallPrecision>, BTreeSet<OpenCall>) {
3502 let Some(resolved) = program.modules.get(module) else {
3503 return (BTreeMap::new(), BTreeSet::new());
3504 };
3505 let mut targets: BTreeMap<Node, CallPrecision> = BTreeMap::new();
3506 let mut open: BTreeSet<OpenCall> = BTreeSet::new();
3507 for call in calls {
3508 match call {
3509 CallShape::Reference(name) => {
3510 if resolved.functions.contains_key(name) {
3511 exact(&mut targets, (module.to_string(), FnKey::Fn(name.clone())));
3512 } else if let Some(owner) = declaring_module(program, resolved, name, |owner| {
3513 owner.functions.contains_key(name)
3514 }) {
3515 exact(&mut targets, (owner, FnKey::Fn(name.clone())));
3516 }
3517 }
3518 CallShape::Ident(name) => {
3519 if resolved.functions.contains_key(name) {
3520 exact(&mut targets, (module.to_string(), FnKey::Fn(name.clone())));
3521 } else if let Some(owner) = declaring_module(program, resolved, name, |owner| {
3522 owner.functions.contains_key(name)
3523 }) {
3524 exact(&mut targets, (owner, FnKey::Fn(name.clone())));
3525 } else if calls_a_value(program, resolved, name) {
3526 open.insert(OpenCall::FunctionValue);
3527 }
3528 }
3529 CallShape::Field {
3530 receiver_ident,
3531 method,
3532 } => {
3533 let owner = receiver_ident.as_ref().and_then(|head| {
3534 declaring_module(program, resolved, head, |owner| {
3535 owner.structs.contains_key(head) || owner.enums.contains_key(head)
3536 })
3537 .map(|owner| (owner, head.clone()))
3538 });
3539 if let Some((owner, type_name)) = owner {
3540 for node in type_methods(program, &owner, &type_name, method) {
3541 exact(&mut targets, node);
3542 }
3543 continue;
3544 }
3545 if let Some(target) = receiver_ident
3546 .as_ref()
3547 .and_then(|head| resolved.module_imports.get(head))
3548 {
3549 if let Some(owner) = program.modules.get(target) {
3550 if owner
3551 .functions
3552 .get(method)
3553 .is_some_and(|entry| entry.exported)
3554 {
3555 exact(&mut targets, (target.clone(), FnKey::Fn(method.clone())));
3556 continue;
3557 }
3558 }
3559 }
3560 for name in reachable {
3561 let Some(candidate) = program.modules.get(*name) else {
3562 continue;
3563 };
3564 for (type_name, method_name) in candidate.methods.keys() {
3565 if method_name == method {
3566 targets
3567 .entry((
3568 (*name).to_string(),
3569 FnKey::Method(type_name.clone(), method_name.clone()),
3570 ))
3571 .or_insert(CallPrecision::Approximate);
3572 }
3573 }
3574 }
3575 }
3576 }
3577 }
3578 (targets, open)
3579}
3580
3581fn calls_a_value(program: &Program, resolved: &ResolvedModule, name: &str) -> bool {
3593 let names_a_type = |owner: &ResolvedModule| {
3594 owner.structs.contains_key(name)
3595 || owner.enums.contains_key(name)
3596 || owner.aliases.contains_key(name)
3597 };
3598 !(declaring_module(program, resolved, name, names_a_type).is_some()
3599 || resolved.host_items.contains_key(name)
3600 || cove_schema::builtins::builtin(name).is_some()
3601 || cove_schema::builtins::free_builtin(name).is_some()
3602 || name == cove_schema::builtins::NONE_CASE.name)
3603}
3604
3605fn exact(targets: &mut BTreeMap<Node, CallPrecision>, node: Node) {
3609 targets.insert(node, CallPrecision::Exact);
3610}
3611
3612fn type_methods(
3618 program: &Program,
3619 type_module: &str,
3620 type_name: &str,
3621 method: &str,
3622) -> BTreeSet<Node> {
3623 program
3624 .methods_of(type_module, type_name)
3625 .into_iter()
3626 .filter(|declared| declared.name == method)
3627 .map(|declared| {
3628 (
3629 declared.module.to_string(),
3630 FnKey::Method(type_name.to_string(), method.to_string()),
3631 )
3632 })
3633 .collect()
3634}
3635
3636fn declaring_module(
3639 program: &Program,
3640 resolved: &ResolvedModule,
3641 name: &str,
3642 is_kind: impl Fn(&ResolvedModule) -> bool,
3643) -> Option<String> {
3644 if is_kind(resolved) {
3645 return Some(resolved.name.clone());
3646 }
3647 let owner_name = resolved.imports.get(name)?;
3648 let owner = program.modules.get(owner_name)?;
3649 is_kind(owner).then(|| owner_name.clone())
3650}
3651
3652fn propagate_capabilities(
3672 program: &mut Program,
3673 call_graph: &BTreeMap<Node, BTreeMap<Node, CallPrecision>>,
3674) {
3675 let mut required: BTreeMap<Node, BTreeSet<Capability>> = BTreeMap::new();
3676 let mut open: BTreeMap<Node, BTreeSet<OpenCall>> = BTreeMap::new();
3677 for (module, resolved) in &program.modules {
3678 for (name, entry) in &resolved.functions {
3679 let node = (module.clone(), FnKey::Fn(name.clone()));
3680 required.insert(node.clone(), entry.direct_capabilities.clone());
3681 open.insert(node, entry.direct_open_calls.clone());
3682 }
3683 for ((type_name, method_name), entry) in &resolved.methods {
3684 let node = (
3685 module.clone(),
3686 FnKey::Method(type_name.clone(), method_name.clone()),
3687 );
3688 required.insert(node.clone(), entry.direct_capabilities.clone());
3689 open.insert(node, entry.direct_open_calls.clone());
3690 }
3691 }
3692
3693 let keys: Vec<Node> = required.keys().cloned().collect();
3694 loop {
3695 let mut changed = false;
3696 for key in &keys {
3697 let Some(callees) = call_graph.get(key) else {
3698 continue;
3699 };
3700 let mut additions = BTreeSet::new();
3701 let mut reached_open = false;
3705 for callee in callees.keys() {
3706 if let Some(callee_open) = open.get(callee) {
3707 reached_open |= !callee_open.is_empty();
3708 }
3709 let Some(callee_caps) = required.get(callee) else {
3710 continue;
3711 };
3712 for cap in callee_caps {
3713 if !required[key].contains(cap) {
3714 additions.insert(cap.clone());
3715 }
3716 }
3717 }
3718 if !additions.is_empty() {
3719 required.get_mut(key).unwrap().extend(additions);
3720 changed = true;
3721 }
3722 if reached_open && open.get_mut(key).unwrap().insert(OpenCall::ReachedOpenCall) {
3723 changed = true;
3724 }
3725 }
3726 if !changed {
3727 break;
3728 }
3729 }
3730
3731 for (module, resolved) in program.modules.iter_mut() {
3732 for (name, entry) in resolved.functions.iter_mut() {
3733 let node = (module.clone(), FnKey::Fn(name.clone()));
3734 entry.required_capabilities = required.remove(&node).unwrap_or_default();
3735 entry.open_calls = open.remove(&node).unwrap_or_default();
3736 }
3737 for ((type_name, method_name), entry) in resolved.methods.iter_mut() {
3738 let node = (
3739 module.clone(),
3740 FnKey::Method(type_name.clone(), method_name.clone()),
3741 );
3742 entry.required_capabilities = required.remove(&node).unwrap_or_default();
3743 entry.open_calls = open.remove(&node).unwrap_or_default();
3744 }
3745 }
3746}
3747
3748fn call_capability(
3764 callee: &Expr,
3765 host_uses: &BTreeSet<String>,
3766 host_items: &BTreeMap<String, String>,
3767 schemas: &HostSchemas,
3768) -> Option<Capability> {
3769 let (module, operation) = match &callee.kind {
3770 ExprKind::Field { base, name } => match &base.kind {
3771 ExprKind::Ident(module_name) if host_uses.contains(module_name.as_str()) => {
3772 (module_name.clone(), name.node.clone())
3773 }
3774 _ => return None,
3775 },
3776 ExprKind::Ident(name) => (host_items.get(name)?.clone(), name.clone()),
3777 _ => return None,
3778 };
3779 Some(operation_capability(&module, &operation, schemas))
3780}
3781
3782fn operation_capability(module: &str, operation: &str, schemas: &HostSchemas) -> Capability {
3787 match schemas.module(module) {
3788 Some(schema) => match schema.operation(operation) {
3789 Some(op) => Capability::new(op.capability),
3790 None => Capability::new(schema.capability),
3791 },
3792 None => Capability::new(module),
3793 }
3794}
3795
3796#[cfg(test)]
3797mod tests {
3798 use super::*;
3799 use crate::config::Config;
3800 use crate::package::{Module, Unit};
3801 use cove_diag::SourceMap;
3802 use cove_schema::{Effect, HostType, ModuleSchema, OperationSchema};
3803 use std::path::PathBuf;
3804
3805 fn module_from_sources(name: &str, sources_text: &[&str]) -> Module {
3808 let mut sources = SourceMap::new();
3809 let mut units = Vec::new();
3810 for (i, text) in sources_text.iter().enumerate() {
3811 let path = PathBuf::from(format!("{name}{i}.cove"));
3812 let file = sources.add(path.clone(), *text);
3813 let ast = cove_syntax::parse_file(&sources, file).expect("test source parses");
3814 units.push(Unit { file, path, ast });
3815 }
3816 Module {
3817 name: name.to_string(),
3818 dir: PathBuf::from(name),
3819 units,
3820 }
3821 }
3822
3823 fn package_of(module: Module) -> Package {
3824 package_of_modules(vec![module])
3825 }
3826
3827 fn package_of_modules(modules: Vec<Module>) -> Package {
3830 let mut map = BTreeMap::new();
3831 for module in modules {
3832 map.insert(module.name.clone(), module);
3833 }
3834 Package {
3835 root: PathBuf::new(),
3836 config: Config::default(),
3837 modules: map,
3838 }
3839 }
3840
3841 fn resolve_modules(modules: &[(&str, &str)]) -> Result<Program, Vec<Diagnostic>> {
3843 let package = package_of_modules(
3844 modules
3845 .iter()
3846 .map(|(name, source)| module_from_sources(name, &[source]))
3847 .collect(),
3848 );
3849 resolve(&package)
3850 }
3851
3852 #[track_caller]
3853 fn resolve_ok(modules: &[(&str, &str)]) -> Program {
3854 match resolve_modules(modules) {
3855 Ok(program) => program,
3856 Err(errors) => panic!(
3857 "expected the package to resolve, found: {}",
3858 errors
3859 .iter()
3860 .map(|d| format!("{}: {}", d.code, d.message))
3861 .collect::<Vec<_>>()
3862 .join("; ")
3863 ),
3864 }
3865 }
3866
3867 #[track_caller]
3870 fn resolve_err(modules: &[(&str, &str)], code: &str) -> Diagnostic {
3871 let errors = resolve_modules(modules).expect_err("expected the package to be rejected");
3872 errors
3873 .into_iter()
3874 .find(|d| d.code == code)
3875 .unwrap_or_else(|| panic!("expected a `{code}` diagnostic"))
3876 }
3877
3878 fn resolve_modules_with(
3881 modules: &[(&str, &str)],
3882 schemas: &HostSchemas,
3883 ) -> Result<Program, Vec<Diagnostic>> {
3884 let package = package_of_modules(
3885 modules
3886 .iter()
3887 .map(|(name, source)| module_from_sources(name, &[source]))
3888 .collect(),
3889 );
3890 resolve_with(&package, schemas)
3891 }
3892
3893 #[track_caller]
3894 fn resolve_ok_with(modules: &[(&str, &str)], schemas: &HostSchemas) -> Program {
3895 match resolve_modules_with(modules, schemas) {
3896 Ok(program) => program,
3897 Err(errors) => panic!(
3898 "expected the package to resolve, found: {}",
3899 errors
3900 .iter()
3901 .map(|d| format!("{}: {}", d.code, d.message))
3902 .collect::<Vec<_>>()
3903 .join("; ")
3904 ),
3905 }
3906 }
3907
3908 #[test]
3909 fn records_a_test_and_leaves_it_module_private() {
3910 let program = resolve_ok(&[(
3911 "text",
3912 "fn wordCount(text: String) -> Int {\n text.words().length()\n}\n\n test fn countsWords() -> Result<Unit, Error> {\n Ok(())\n}\n",
3913 )]);
3914 let entry = &program.modules["text"].functions["countsWords"];
3915 assert!(entry.is_test);
3916 assert!(!entry.exported);
3917 assert!(!program.modules["text"]
3918 .exports()
3919 .contains(&"countsWords".to_string()));
3920
3921 let tests = program.tests();
3922 assert_eq!(tests.len(), 1);
3923 assert_eq!(tests[0].qualified_name(), "text.countsWords");
3924 }
3925
3926 #[test]
3927 fn lists_every_test_of_the_package_in_module_then_name_order() {
3928 let program = resolve_ok(&[
3929 (
3930 "second",
3931 "test fn b() -> Result<Unit, Error> {\n Ok(())\n}\n\n test fn a() -> Result<Unit, Error> {\n Ok(())\n}\n",
3932 ),
3933 (
3934 "first",
3935 "test fn c() -> Result<Unit, Error> {\n Ok(())\n}\n",
3936 ),
3937 ]);
3938 let names: Vec<String> = program
3939 .tests()
3940 .iter()
3941 .map(DeclaredTest::qualified_name)
3942 .collect();
3943 assert_eq!(names, ["first.c", "second.a", "second.b"]);
3944 }
3945
3946 #[test]
3947 fn a_test_requires_the_capabilities_its_call_graph_reaches() {
3948 let program = resolve_ok(&[(
3949 "text",
3950 "use console.println\n\n fn report(text: String) -> Result<Unit, Error> {\n println(text)\n}\n\n test fn reports() -> Result<Unit, Error> {\n report(\"a\")?\n Ok(())\n}\n\n test fn countsNothing() -> Result<Unit, Error> {\n Ok(())\n}\n",
3951 )]);
3952 let required = |name: &str| -> Vec<String> {
3953 program.modules["text"].functions[name]
3954 .required_capabilities
3955 .iter()
3956 .map(Capability::to_string)
3957 .collect()
3958 };
3959 assert_eq!(required("reports"), ["console".to_string()]);
3962 assert!(required("countsNothing").is_empty());
3963 }
3964
3965 #[test]
3966 fn a_test_may_call_its_modules_private_declarations() {
3967 let program = resolve_ok(&[(
3968 "text",
3969 "fn secret() -> Int {\n 7\n}\n\n test fn seesSecret() -> Result<Unit, Error> {\n secret()\n Ok(())\n}\n",
3970 )]);
3971 let edges = &program.call_graph[&("text".to_string(), FnKey::Fn("seesSecret".to_string()))];
3972 assert!(edges.contains_key(&("text".to_string(), FnKey::Fn("secret".to_string()))));
3973 }
3974
3975 #[test]
3976 fn merges_two_units_of_the_same_module() {
3977 let module = module_from_sources(
3978 "greet",
3979 &[
3980 "/// Greets by name.\nexport fn greet(name: String) -> String {\n name\n}\n",
3981 "/// Says goodbye.\nexport fn farewell(name: String) -> String {\n name\n}\n",
3982 ],
3983 );
3984 let package = package_of(module);
3985 let program = resolve(&package).expect("resolves");
3986 let resolved = &program.modules["greet"];
3987 assert!(resolved.functions.contains_key("greet"));
3988 assert!(resolved.functions.contains_key("farewell"));
3989 }
3990
3991 #[test]
3992 fn reports_duplicate_declaration_across_units() {
3993 let module = module_from_sources(
3994 "dup",
3995 &[
3996 "/// First.\nexport fn greet(name: String) -> String {\n name\n}\n",
3997 "/// Second.\nexport fn greet(name: String) -> String {\n name\n}\n",
3998 ],
3999 );
4000 let package = package_of(module);
4001 let errs = resolve(&package).unwrap_err();
4002 assert!(errs
4003 .iter()
4004 .any(|d| d.code == "cove::resolve::duplicate_declaration"));
4005 }
4006
4007 #[test]
4008 fn resolves_impl_methods() {
4009 let module = module_from_sources(
4010 "booking",
4011 &[
4012 "/// A booking.\nexport struct Booking {\n id: String\n}\n\nimpl Booking {\n /// Returns the id.\n fn id(self) -> String {\n self.id\n }\n}\n",
4013 ],
4014 );
4015 let package = package_of(module);
4016 let program = resolve(&package).expect("resolves");
4017 let resolved = &program.modules["booking"];
4018 let method = resolved
4019 .methods
4020 .get(&("Booking".to_string(), "id".to_string()))
4021 .expect("method resolved");
4022 assert_eq!(method.receiver_type.as_deref(), Some("Booking"));
4023 }
4024
4025 const TRAIT_SOURCE: &str = "\
4027/// Renders itself.
4028export trait Display {
4029 /// The full form.
4030 fn describe(self) -> String
4031
4032 /// A short form, defaulting to the full one.
4033 fn label(self) -> String { self.describe() }
4034}
4035
4036/// A booking.
4037export struct Booking(id: Int)
4038
4039/// A receipt.
4040export struct Receipt(total: Int)
4041";
4042
4043 fn resolved_of(name: &str, sources: &[&str]) -> ResolvedModule {
4044 let package = package_of(module_from_sources(name, sources));
4045 let mut program = resolve(&package).expect("resolves");
4046 program.modules.remove(name).expect("the module resolves")
4047 }
4048
4049 fn resolve_errors(name: &str, sources: &[&str]) -> Vec<Diagnostic> {
4050 let package = package_of(module_from_sources(name, sources));
4051 resolve(&package).expect_err("expected resolution to fail")
4052 }
4053
4054 fn has_code(diagnostics: &[Diagnostic], code: &str) -> bool {
4055 diagnostics.iter().any(|d| d.code == code)
4056 }
4057
4058 #[test]
4059 fn records_a_conformance_and_the_methods_it_supplies() {
4060 let source = format!(
4061 "{TRAIT_SOURCE}\nimpl Display for Booking {{\n fn describe(self) -> String {{ \"b\" }}\n fn label(self) -> String {{ \"#\" }}\n}}\n"
4062 );
4063 let resolved = resolved_of("render", &[&source]);
4064 let conformance = resolved
4065 .conformances
4066 .get(&("Display".to_string(), "Booking".to_string()))
4067 .expect("the conformance is recorded");
4068 assert_eq!(
4069 conformance.methods.iter().cloned().collect::<Vec<_>>(),
4070 ["describe", "label"]
4071 );
4072 assert!(resolved
4075 .methods
4076 .contains_key(&("Booking".to_string(), "describe".to_string())));
4077 }
4078
4079 #[test]
4080 fn a_defaulted_method_becomes_the_type_s_own_method() {
4081 let source = format!(
4082 "{TRAIT_SOURCE}\nimpl Display for Receipt {{\n fn describe(self) -> String {{ \"r\" }}\n}}\n"
4083 );
4084 let resolved = resolved_of("render", &[&source]);
4085 let label = resolved
4086 .methods
4087 .get(&("Receipt".to_string(), "label".to_string()))
4088 .expect("the default body is recorded as a method");
4089 assert_eq!(label.receiver_type.as_deref(), Some("Receipt"));
4090 assert_eq!(
4091 label.doc.as_deref(),
4092 Some("A short form, defaulting to the full one.")
4093 );
4094 }
4095
4096 #[test]
4097 fn rejects_a_conformance_missing_a_required_method() {
4098 let source = format!("{TRAIT_SOURCE}\nimpl Display for Booking {{\n}}\n");
4099 let errors = resolve_errors("render", &[&source]);
4100 assert!(has_code(&errors, "cove::resolve::missing_trait_method"));
4101 assert!(errors[0].message.contains("`describe`"));
4102 assert!(!errors[0].message.contains("`label`"));
4104 }
4105
4106 #[test]
4107 fn rejects_a_method_the_trait_does_not_declare() {
4108 let source = format!(
4109 "{TRAIT_SOURCE}\nimpl Display for Booking {{\n fn describe(self) -> String {{ \"b\" }}\n fn extra(self) -> Int {{ 1 }}\n}}\n"
4110 );
4111 let errors = resolve_errors("render", &[&source]);
4112 assert!(has_code(&errors, "cove::resolve::unknown_trait_method"));
4113 }
4114
4115 #[test]
4116 fn rejects_the_same_conformance_twice() {
4117 let source = format!(
4118 "{TRAIT_SOURCE}\nimpl Display for Booking {{\n fn describe(self) -> String {{ \"b\" }}\n}}\n\nimpl Display for Booking {{\n fn describe(self) -> String {{ \"c\" }}\n}}\n"
4119 );
4120 let errors = resolve_errors("render", &[&source]);
4121 assert!(has_code(&errors, "cove::resolve::duplicate_conformance"));
4122 }
4123
4124 #[test]
4125 fn rejects_a_trait_method_that_collides_with_an_inherent_method() {
4126 let source = format!(
4127 "{TRAIT_SOURCE}\nimpl Display for Booking {{\n fn describe(self) -> String {{ \"b\" }}\n}}\n\nimpl Booking {{\n /// Also describes.\n fn describe(self) -> String {{ \"c\" }}\n}}\n"
4128 );
4129 let errors = resolve_errors("render", &[&source]);
4130 assert!(has_code(&errors, "cove::resolve::duplicate_declaration"));
4131 }
4132
4133 #[test]
4134 fn the_orphan_rule_allows_a_local_trait_or_a_local_type() {
4135 let source = format!("{TRAIT_SOURCE}\nimpl Display for Int {{\n fn describe(self) -> String {{ \"i\" }}\n}}\n");
4139 let errors = resolve_errors("render", &[&source]);
4140 assert!(has_code(&errors, "cove::resolve::unknown_impl_type"));
4141 assert!(!has_code(&errors, "cove::resolve::orphan_conformance"));
4142 }
4143
4144 #[test]
4145 fn rejects_a_conformance_between_two_types_the_module_does_not_declare() {
4146 let errors = resolve_errors(
4147 "elsewhere",
4148 &["impl Display for Int {\n fn describe(self) -> String { \"i\" }\n}\n"],
4149 );
4150 assert!(has_code(&errors, "cove::resolve::orphan_conformance"));
4151 }
4152
4153 #[test]
4156 fn impl_snapshot_records_a_conformance_with_no_trait_declaration_in_source() {
4157 let source = "\
4158/// A booking.
4159export struct Booking(id: Int)
4160
4161impl Snapshot for Booking {
4162 /// Returns a copy of this booking.
4163 fn snapshot(self) -> Booking { self }
4164}
4165";
4166 let resolved = resolved_of("booking", &[source]);
4167 let conformance = resolved
4168 .conformances
4169 .get(&("Snapshot".to_string(), "Booking".to_string()))
4170 .expect("the conformance is recorded even though no `trait Snapshot` was written");
4171 assert_eq!(
4172 conformance.methods.iter().cloned().collect::<Vec<_>>(),
4173 ["snapshot"]
4174 );
4175 assert!(resolved
4176 .methods
4177 .contains_key(&("Booking".to_string(), "snapshot".to_string())));
4178 assert!(!resolved.traits.contains_key("Snapshot"));
4181 }
4182
4183 #[test]
4184 fn impl_snapshot_still_requires_the_snapshot_method() {
4185 let source = "\
4186/// A booking.
4187export struct Booking(id: Int)
4188
4189impl Snapshot for Booking {
4190}
4191";
4192 let errors = resolve_errors("booking", &[source]);
4193 assert!(has_code(&errors, "cove::resolve::missing_trait_method"));
4194 assert!(errors[0].message.contains("`snapshot`"));
4195 }
4196
4197 #[test]
4198 fn a_third_module_may_not_conform_an_imported_type_to_snapshot() {
4199 let error = resolve_err(
4203 &[
4204 (
4205 "booking",
4206 "/// A booking.\nexport struct Booking(id: Int)\n",
4207 ),
4208 (
4209 "other",
4210 "use booking.Booking\n\nimpl Snapshot for Booking {\n fn snapshot(self) -> Booking { self }\n}\n",
4211 ),
4212 ],
4213 "cove::resolve::orphan_conformance",
4214 );
4215 assert!(error.message.contains("Snapshot"));
4216 assert!(error.message.contains("Booking"));
4217 }
4218
4219 #[test]
4220 fn warns_on_an_exported_trait_and_its_methods_without_docs() {
4221 let package = package_of(module_from_sources(
4222 "render",
4223 &["export trait Display {\n fn describe(self) -> String\n}\n"],
4224 ));
4225 let program = resolve(&package).expect("resolves");
4226 let names: Vec<&str> = program
4227 .notices
4228 .iter()
4229 .filter(|d| d.code == "cove::resolve::missing_doc")
4230 .map(|d| d.message.as_str())
4231 .collect();
4232 assert_eq!(
4233 names,
4234 [
4235 "exported `Display` has no doc comment",
4236 "exported `Display.describe` has no doc comment"
4237 ]
4238 );
4239 }
4240
4241 #[test]
4242 fn a_defaulted_method_is_marked_as_coming_from_its_trait() {
4243 let source = format!(
4244 "{TRAIT_SOURCE}\nimpl Display for Receipt {{\n fn describe(self) -> String {{ \"r\" }}\n}}\n"
4245 );
4246 let resolved = resolved_of("render", &[&source]);
4247 let methods = &resolved.methods;
4248 assert_eq!(
4249 methods[&("Receipt".to_string(), "label".to_string())]
4250 .from_trait_default
4251 .as_deref(),
4252 Some("Display")
4253 );
4254 assert!(methods[&("Receipt".to_string(), "describe".to_string())]
4255 .from_trait_default
4256 .is_none());
4257 }
4258
4259 #[test]
4260 fn a_default_body_s_match_is_checked_once_however_many_types_conform() {
4261 let source = "\
4262/// A signal.
4263enum Signal {
4264 Red
4265 Green
4266}
4267
4268/// Shows itself.
4269trait Show {
4270 /// The signal.
4271 fn signal(self) -> Signal
4272
4273 /// A name, from a `match` that misses a case.
4274 fn name(self) -> String {
4275 match self.signal() {
4276 Signal.Red => \"red\"
4277 }
4278 }
4279}
4280
4281/// One.
4282struct A(x: Int)
4283
4284/// Two.
4285struct B(x: Int)
4286
4287impl Show for A {
4288 fn signal(self) -> Signal { Signal.Red }
4289}
4290
4291impl Show for B {
4292 fn signal(self) -> Signal { Signal.Green }
4293}
4294";
4295 let errors = resolve_errors("show", &[source]);
4296 assert_eq!(
4297 errors
4298 .iter()
4299 .filter(|d| d.code == "cove::resolve::non_exhaustive_match")
4300 .count(),
4301 1
4302 );
4303 }
4304
4305 #[test]
4306 fn a_conformance_method_propagates_its_capabilities() {
4307 let source = format!(
4308 "use console.println\n\n{TRAIT_SOURCE}\nimpl Display for Booking {{\n fn describe(self) -> String {{\n console.println(\"b\")\n \"b\"\n }}\n}}\n"
4309 );
4310 let resolved = resolved_of("render", &[&source]);
4311 let describe = &resolved.methods[&("Booking".to_string(), "describe".to_string())];
4312 assert!(describe
4313 .required_capabilities
4314 .iter()
4315 .any(|c| c.to_string() == "console"));
4316 }
4317
4318 #[test]
4319 fn rejects_impl_for_unknown_type() {
4320 let module = module_from_sources("orphan", &["impl Nothing {\n fn go(self) {\n }\n}\n"]);
4321 let package = package_of(module);
4322 let errs = resolve(&package).unwrap_err();
4323 assert!(errs
4324 .iter()
4325 .any(|d| d.code == "cove::resolve::unknown_impl_type"));
4326 }
4327
4328 #[test]
4329 fn rejects_non_fn_impl_items() {
4330 let module = module_from_sources(
4331 "badimpl",
4332 &[
4333 "export struct Thing {\n x: Int\n}\n\nimpl Thing {\n struct Nested {\n y: Int\n }\n}\n",
4334 ],
4335 );
4336 let package = package_of(module);
4337 let errs = resolve(&package).unwrap_err();
4338 assert!(errs
4339 .iter()
4340 .any(|d| d.code == "cove::resolve::invalid_impl_item"));
4341 }
4342
4343 #[test]
4344 fn one_segment_use_records_a_host_use() {
4345 let module = module_from_sources("hostuse", &["use http\n\nexport fn main() {\n}\n"]);
4346 let package = package_of(module);
4347 let program = resolve(&package).expect("resolves");
4348 assert!(program.modules["hostuse"].host_uses.contains("http"));
4349 }
4350
4351 #[test]
4352 fn two_segment_use_records_use_and_item() {
4353 let module = module_from_sources(
4354 "hostitem",
4355 &["use console.println\n\nexport fn main() {\n}\n"],
4356 );
4357 let package = package_of(module);
4358 let program = resolve(&package).expect("resolves");
4359 let resolved = &program.modules["hostitem"];
4360 assert!(resolved.host_uses.contains("console"));
4361 assert_eq!(
4362 resolved.host_items.get("println").map(String::as_str),
4363 Some("console")
4364 );
4365 }
4366
4367 #[test]
4368 fn a_use_matching_no_module_and_no_host_path_is_rejected() {
4369 let diagnostic = resolve_err(&[("toolong", "use a.b.c\n")], "cove::resolve::unknown_use");
4370 assert!(diagnostic.message.contains("a.b.c"));
4371 assert!(diagnostic.message.contains("module"));
4373 assert!(diagnostic.message.contains("host module"));
4374 }
4375
4376 #[test]
4379 fn a_use_imports_an_exported_declaration() {
4380 let program = resolve_ok(&[
4381 (
4382 "greet",
4383 "/// Greets by name.\nexport fn greeting(name: String) -> String {\n name\n}\n",
4384 ),
4385 (
4386 "hello",
4387 "use greet.greeting\n\n/// Entry point.\nexport fn main() -> String {\n greeting(\"world\")\n}\n",
4388 ),
4389 ]);
4390 assert_eq!(
4391 program.modules["hello"].imports.get("greeting"),
4392 Some(&"greet".to_string())
4393 );
4394 assert!(program.modules["hello"].module_imports.is_empty());
4395 assert!(program.modules["hello"].host_uses.is_empty());
4398 }
4399
4400 #[test]
4401 fn a_use_of_a_module_alone_imports_the_module() {
4402 let program = resolve_ok(&[
4403 (
4404 "greet",
4405 "/// Greets by name.\nexport fn greeting(name: String) -> String {\n name\n}\n",
4406 ),
4407 (
4408 "hello",
4409 "use greet\n\n/// Entry point.\nexport fn main() -> String {\n greet.greeting(\"world\")\n}\n",
4410 ),
4411 ]);
4412 assert_eq!(
4413 program.modules["hello"].module_imports.get("greet"),
4414 Some(&"greet".to_string())
4415 );
4416 assert!(program.modules["hello"].imports.is_empty());
4417 assert!(program.modules["hello"].host_uses.is_empty());
4418 }
4419
4420 #[test]
4421 fn a_nested_module_is_imported_by_its_full_path() {
4422 let program = resolve_ok(&[
4423 (
4424 "src.booking",
4425 "/// Creates a booking.\nexport fn createBooking() -> String {\n \"b\"\n}\n",
4426 ),
4427 (
4428 "app",
4429 "use src.booking.createBooking\n\n/// Entry point.\nexport fn main() -> String {\n createBooking()\n}\n",
4430 ),
4431 ]);
4432 assert_eq!(
4433 program.modules["app"].imports.get("createBooking"),
4434 Some(&"src.booking".to_string())
4435 );
4436 }
4437
4438 #[test]
4439 fn a_use_of_a_private_declaration_is_rejected() {
4440 let diagnostic = resolve_err(
4441 &[
4442 (
4443 "greet",
4444 "fn greeting(name: String) -> String {\n name\n}\n",
4445 ),
4446 ("hello", "use greet.greeting\n"),
4447 ],
4448 "cove::resolve::private_declaration",
4449 );
4450 assert!(diagnostic.message.contains("not exported"));
4451 assert_eq!(diagnostic.labels.len(), 1);
4453 assert!(diagnostic.help.as_deref().unwrap().contains("export"));
4454 }
4455
4456 #[test]
4457 fn a_use_naming_a_module_that_declares_no_such_name_is_rejected() {
4458 let diagnostic = resolve_err(
4459 &[
4460 (
4461 "greet",
4462 "/// Greets.\nexport fn greeting() -> String {\n \"hi\"\n}\n",
4463 ),
4464 ("hello", "use greet.farewell\n"),
4465 ],
4466 "cove::resolve::unknown_use",
4467 );
4468 assert!(diagnostic.message.contains("declares no `farewell`"));
4469 assert!(diagnostic.message.contains("not a host module"));
4470 assert!(diagnostic.help.as_deref().unwrap().contains("greeting"));
4471 }
4472
4473 #[test]
4474 fn a_module_named_after_a_host_module_is_rejected_rather_than_preferred() {
4475 let diagnostic = resolve_err(
4476 &[
4477 (
4478 "console",
4479 "/// Prints.\nexport fn println(line: String) {\n}\n",
4480 ),
4481 ("app", "use console.println\n"),
4482 ],
4483 "cove::resolve::module_shadows_host",
4484 );
4485 assert!(diagnostic.help.as_deref().unwrap().contains("rename"));
4486 }
4487
4488 #[test]
4497 fn every_shipped_host_module_is_refused_as_a_package_module() {
4498 for host in host_modules(&HostSchemas::new()) {
4499 let diagnostic = resolve_err(
4500 &[
4501 (host, "/// Does something.\nexport fn thing() {\n}\n"),
4502 ("app", &format!("use {host}.thing\n")),
4503 ],
4504 "cove::resolve::module_shadows_host",
4505 );
4506 assert!(
4507 diagnostic.message.contains(host),
4508 "`{host}` should be refused as a package module"
4509 );
4510 }
4511 }
4512
4513 #[test]
4514 fn a_use_naming_both_a_module_and_a_declaration_is_rejected() {
4515 let diagnostic = resolve_err(
4516 &[
4517 (
4518 "booking",
4519 "/// Creates a booking.\nexport fn create() -> String {\n \"b\"\n}\n",
4520 ),
4521 (
4522 "booking.create",
4523 "/// Validates a booking.\nexport fn validate() -> Bool {\n true\n}\n",
4524 ),
4525 ("app", "use booking.create\n"),
4526 ],
4527 "cove::resolve::ambiguous_use",
4528 );
4529 assert!(diagnostic.message.contains("both"));
4530 }
4531
4532 #[test]
4533 fn an_import_colliding_with_a_declaration_is_rejected() {
4534 let diagnostic = resolve_err(
4535 &[
4536 (
4537 "greet",
4538 "/// Greets.\nexport fn greeting() -> String {\n \"hi\"\n}\n",
4539 ),
4540 (
4541 "hello",
4542 "use greet.greeting\n\nfn greeting() -> String {\n \"other\"\n}\n",
4543 ),
4544 ],
4545 "cove::resolve::import_conflict",
4546 );
4547 assert!(diagnostic.message.contains("also declares it"));
4548 }
4549
4550 #[test]
4551 fn two_imports_of_one_name_from_different_modules_are_rejected() {
4552 let diagnostic = resolve_err(
4553 &[
4554 (
4555 "left",
4556 "/// Greets.\nexport fn greeting() -> String {\n \"l\"\n}\n",
4557 ),
4558 (
4559 "right",
4560 "/// Greets.\nexport fn greeting() -> String {\n \"r\"\n}\n",
4561 ),
4562 ("hello", "use left.greeting\nuse right.greeting\n"),
4563 ],
4564 "cove::resolve::import_conflict",
4565 );
4566 assert!(diagnostic.message.contains("both"));
4567 }
4568
4569 #[test]
4570 fn importing_the_same_declaration_twice_is_not_a_conflict() {
4571 let package = package_of_modules(vec![
4572 module_from_sources(
4573 "greet",
4574 &["/// Greets.\nexport fn greeting() -> String {\n \"hi\"\n}\n"],
4575 ),
4576 module_from_sources("hello", &["use greet.greeting\n", "use greet.greeting\n"]),
4577 ]);
4578 resolve(&package).expect("resolves");
4579 }
4580
4581 #[test]
4582 fn an_unknown_two_segment_use_is_still_a_host_path() {
4583 let program = resolve_ok(&[("app", "use other.println\n")]);
4584 assert!(program.modules["app"].host_uses.contains("other"));
4585 assert_eq!(
4586 program.modules["app"].host_items.get("println"),
4587 Some(&"other".to_string())
4588 );
4589 }
4590
4591 #[test]
4603 fn unchecked_host_warns_once_per_module_not_once_per_use() {
4604 let module_a = module_from_sources(
4605 "a",
4606 &[
4607 "use company\n\n/// Calls into `company`.\nexport fn f() {\n company.employee()\n}\n",
4608 "use company.employee\n\n/// Calls the unqualified import.\nexport fn g() {\n employee()\n}\n",
4609 ],
4610 );
4611 let first_use_file = module_a.units[0].file;
4612 let module_b = module_from_sources(
4613 "b",
4614 &["use company\n\n/// Also calls into `company`.\nexport fn h() {\n company.employee()\n}\n"],
4615 );
4616 let package = package_of_modules(vec![module_a, module_b]);
4617 let program = resolve(&package).expect("resolves despite the unchecked host warning");
4618 let warnings: Vec<_> = program
4619 .notices
4620 .iter()
4621 .filter(|d| d.code == "cove::resolve::unchecked_host")
4622 .collect();
4623 assert_eq!(
4624 warnings.len(),
4625 1,
4626 "expected exactly one unchecked_host warning, found {warnings:?}"
4627 );
4628 assert_eq!(
4629 warnings[0].primary.expect("warning has a span").file,
4630 first_use_file,
4631 "the warning should point at module `a`'s first `use company`"
4632 );
4633 }
4634
4635 #[test]
4636 fn unchecked_host_warns_once_per_distinct_module() {
4637 let program = resolve_ok(&[
4638 (
4639 "a",
4640 "use company\n\n/// Calls into `company`.\nexport fn f() {\n company.employee()\n}\n",
4641 ),
4642 (
4643 "b",
4644 "use vendor\n\n/// Calls into `vendor`.\nexport fn g() {\n vendor.order()\n}\n",
4645 ),
4646 ]);
4647 let modules_warned: BTreeSet<&str> = program
4648 .notices
4649 .iter()
4650 .filter(|d| d.code == "cove::resolve::unchecked_host")
4651 .map(|d| {
4652 if d.message.contains("`company`") {
4653 "company"
4654 } else if d.message.contains("`vendor`") {
4655 "vendor"
4656 } else {
4657 panic!("unexpected unchecked_host warning: {}", d.message)
4658 }
4659 })
4660 .collect();
4661 assert_eq!(
4662 modules_warned,
4663 BTreeSet::from(["company", "vendor"]),
4664 "two distinct undescribed host modules should each warn once"
4665 );
4666 }
4667
4668 #[test]
4671 fn a_direct_import_cycle_is_rejected() {
4672 let diagnostic = resolve_err(
4673 &[
4674 (
4675 "a",
4676 "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n 1\n}\n",
4677 ),
4678 (
4679 "b",
4680 "use a.fromA\n\n/// Exported.\nexport fn fromB() -> Int {\n 2\n}\n",
4681 ),
4682 ],
4683 "cove::resolve::import_cycle",
4684 );
4685 assert!(
4686 diagnostic.message.contains("a -> b -> a")
4687 || diagnostic.message.contains("b -> a -> b")
4688 );
4689 }
4690
4691 #[test]
4692 fn a_transitive_import_cycle_is_rejected() {
4693 let diagnostic = resolve_err(
4694 &[
4695 (
4696 "a",
4697 "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n 1\n}\n",
4698 ),
4699 (
4700 "b",
4701 "use c.fromC\n\n/// Exported.\nexport fn fromB() -> Int {\n 2\n}\n",
4702 ),
4703 (
4704 "c",
4705 "use a.fromA\n\n/// Exported.\nexport fn fromC() -> Int {\n 3\n}\n",
4706 ),
4707 ],
4708 "cove::resolve::import_cycle",
4709 );
4710 assert!(diagnostic.message.contains(" -> "));
4711 assert_eq!(
4712 resolve_modules(&[
4713 (
4714 "a",
4715 "use b.fromB\n\n/// Exported.\nexport fn fromA() -> Int {\n 1\n}\n",
4716 ),
4717 (
4718 "b",
4719 "use c.fromC\n\n/// Exported.\nexport fn fromB() -> Int {\n 2\n}\n",
4720 ),
4721 (
4722 "c",
4723 "use a.fromA\n\n/// Exported.\nexport fn fromC() -> Int {\n 3\n}\n",
4724 ),
4725 ])
4726 .unwrap_err()
4727 .iter()
4728 .filter(|d| d.code == "cove::resolve::import_cycle")
4729 .count(),
4730 1,
4731 "one cycle is reported once, however many modules it runs through"
4732 );
4733 }
4734
4735 #[test]
4736 fn a_module_importing_itself_is_a_cycle() {
4737 resolve_err(
4738 &[(
4739 "a",
4740 "use a.fromA\n\n/// Exported.\nexport fn fromA() -> Int {\n 1\n}\n",
4741 )],
4742 "cove::resolve::import_cycle",
4743 );
4744 }
4745
4746 #[test]
4749 fn a_diamond_import_is_accepted() {
4750 let program = resolve_ok(&[
4751 (
4752 "base",
4753 "/// The shared helper.\nexport fn base() -> Int {\n 1\n}\n",
4754 ),
4755 (
4756 "left",
4757 "use base.base\n\n/// Exported.\nexport fn left() -> Int {\n base()\n}\n",
4758 ),
4759 (
4760 "right",
4761 "use base.base\n\n/// Exported.\nexport fn right() -> Int {\n base()\n}\n",
4762 ),
4763 (
4764 "top",
4765 "use left.left\nuse right.right\n\n/// Exported.\nexport fn top() -> Int {\n left() + right()\n}\n",
4766 ),
4767 ]);
4768 assert_eq!(program.modules.len(), 4);
4769 }
4770
4771 #[test]
4774 fn required_capabilities_cross_a_module_boundary() {
4775 let program = resolve_ok(&[
4776 (
4777 "log",
4778 "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n console.println(msg)\n}\n",
4779 ),
4780 (
4781 "app",
4782 "use log.log\n\n/// Entry point; never names a host module.\nexport fn main() {\n log(\"hi\")\n}\n",
4783 ),
4784 ]);
4785 let main = &program.modules["app"].functions["main"];
4786 assert!(main.direct_capabilities.is_empty());
4787 assert!(main
4788 .required_capabilities
4789 .contains(&Capability::new("console")));
4790 }
4791
4792 #[test]
4793 fn required_capabilities_cross_a_qualified_module_call() {
4794 let program = resolve_ok(&[
4795 (
4796 "log",
4797 "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n console.println(msg)\n}\n",
4798 ),
4799 (
4800 "app",
4801 "use log\n\n/// Entry point.\nexport fn main() {\n log.log(\"hi\")\n}\n",
4802 ),
4803 ]);
4804 assert!(program.modules["app"].functions["main"]
4805 .required_capabilities
4806 .contains(&Capability::new("console")));
4807 }
4808
4809 #[test]
4810 fn required_capabilities_cross_two_module_boundaries() {
4811 let program = resolve_ok(&[
4812 (
4813 "bottom",
4814 "use console.println\n\n/// Logs.\nexport fn log(msg: String) {\n console.println(msg)\n}\n",
4815 ),
4816 (
4817 "middle",
4818 "use bottom.log\n\n/// Logs twice.\nexport fn twice(msg: String) {\n log(msg)\n log(msg)\n}\n",
4819 ),
4820 (
4821 "top",
4822 "use middle.twice\n\n/// Entry point.\nexport fn main() {\n twice(\"hi\")\n}\n",
4823 ),
4824 ]);
4825 assert!(program.modules["top"].functions["main"]
4826 .required_capabilities
4827 .contains(&Capability::new("console")));
4828 }
4829
4830 #[test]
4831 fn required_capabilities_cross_an_imported_type_s_method() {
4832 let program = resolve_ok(&[
4833 (
4834 "thing",
4835 "use console.println\n\n/// A thing.\nexport struct Thing {\n id: String\n}\n\n\
4836 impl Thing {\n /// Prints the id.\n fn touch(self) {\n console.println(self.id)\n }\n}\n",
4837 ),
4838 (
4839 "app",
4840 "use thing.Thing\n\n/// Entry point.\nexport fn main() {\n Thing.touch()\n}\n",
4841 ),
4842 ]);
4843 assert!(program.modules["app"].functions["main"]
4844 .required_capabilities
4845 .contains(&Capability::new("console")));
4846 }
4847
4848 #[test]
4852 fn an_unknown_receiver_reaches_an_imported_type_s_method() {
4853 let program = resolve_ok(&[
4854 (
4855 "thing",
4856 "use console.println\n\n/// A thing.\nexport struct Thing {\n id: String\n}\n\n\
4857 impl Thing {\n /// Prints the id.\n fn touch(self) {\n console.println(self.id)\n }\n}\n",
4858 ),
4859 (
4860 "app",
4861 "use thing.Thing\n\n/// Entry point.\nexport fn main(value: Thing) {\n value.touch()\n}\n",
4862 ),
4863 ]);
4864 assert!(program.modules["app"].functions["main"]
4865 .required_capabilities
4866 .contains(&Capability::new("console")));
4867 }
4868
4869 #[test]
4870 fn a_module_that_imports_nothing_requires_nothing_from_its_neighbours() {
4871 let program = resolve_ok(&[
4872 (
4873 "log",
4874 "use console.println\n\n/// Logs a message.\nexport fn log(msg: String) {\n console.println(msg)\n}\n",
4875 ),
4876 (
4877 "pure",
4878 "/// Adds.\nexport fn add(a: Int, b: Int) -> Int {\n a + b\n}\n",
4879 ),
4880 ]);
4881 assert!(program.modules["pure"].functions["add"]
4882 .required_capabilities
4883 .is_empty());
4884 }
4885
4886 #[test]
4889 fn match_over_an_imported_enum_is_checked_for_exhaustiveness() {
4890 let diagnostic = resolve_err(
4891 &[
4892 (
4893 "levels",
4894 "/// Levels.\nexport enum LogLevel {\n Debug\n Info\n Warn\n}\n",
4895 ),
4896 (
4897 "app",
4898 "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n \
4899 match level {\n LogLevel.Debug => \"debug\"\n LogLevel.Info => \"info\"\n }\n}\n",
4900 ),
4901 ],
4902 "cove::resolve::non_exhaustive_match",
4903 );
4904 assert!(diagnostic.message.contains("LogLevel.Warn"));
4905 }
4906
4907 #[test]
4908 fn match_covering_every_case_of_an_imported_enum_passes() {
4909 let program = resolve_ok(&[
4910 (
4911 "levels",
4912 "/// Levels.\nexport enum LogLevel {\n Debug\n Info\n}\n",
4913 ),
4914 (
4915 "app",
4916 "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n \
4917 match level {\n LogLevel.Debug => \"debug\"\n LogLevel.Info => \"info\"\n }\n}\n",
4918 ),
4919 ]);
4920 assert!(program.notices.is_empty());
4921 }
4922
4923 #[test]
4924 fn an_unknown_case_of_an_imported_enum_is_reported() {
4925 let diagnostic = resolve_err(
4926 &[
4927 (
4928 "levels",
4929 "/// Levels.\nexport enum LogLevel {\n Debug\n Info\n}\n",
4930 ),
4931 (
4932 "app",
4933 "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n \
4934 match level {\n LogLevel.Debug => \"debug\"\n LogLevel.Bogus => \"bogus\"\n LogLevel.Info => \"info\"\n }\n}\n",
4935 ),
4936 ],
4937 "cove::resolve::unknown_enum_case",
4938 );
4939 assert!(diagnostic.message.contains("Bogus"));
4940 }
4941
4942 const DISPLAY: &str = "\
4948/// Renders itself.
4949export trait Display {
4950 /// The full form.
4951 fn describe(self) -> String
4952
4953 /// A short form, defaulting to the full one.
4954 fn label(self) -> String { self.describe() }
4955}
4956";
4957
4958 const BOOKING: &str = "\
4959/// A booking.
4960export struct Booking {
4961 id: Int
4962}
4963";
4964
4965 #[test]
4968 fn a_module_may_conform_its_own_type_to_an_imported_trait() {
4969 let program = resolve_ok(&[
4970 ("display", DISPLAY),
4971 (
4972 "booking",
4973 &format!(
4974 "use display.Display\n\n{BOOKING}\nimpl Display for Booking {{\n \
4975 /// The full form.\n fn describe(self) -> String {{\n \"b\"\n }}\n}}\n"
4976 ),
4977 ),
4978 ]);
4979 let conformance = program.modules["booking"]
4980 .conformances
4981 .get(&("Display".to_string(), "Booking".to_string()))
4982 .expect("the conformance is recorded where the type is declared");
4983 assert_eq!(conformance.trait_module, "display");
4984 assert_eq!(conformance.type_module, "booking");
4985 assert_eq!(
4987 conformance.methods.iter().cloned().collect::<Vec<_>>(),
4988 ["describe", "label"]
4989 );
4990 }
4991
4992 #[test]
4995 fn a_module_may_conform_an_imported_type_to_its_own_trait() {
4996 let program = resolve_ok(&[
4997 ("booking", BOOKING),
4998 (
4999 "display",
5000 &format!(
5001 "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n \
5002 /// The full form.\n fn describe(self) -> String {{\n \"b\"\n }}\n}}\n"
5003 ),
5004 ),
5005 ]);
5006 let conformance = program.modules["display"]
5007 .conformances
5008 .get(&("Display".to_string(), "Booking".to_string()))
5009 .expect("the conformance is recorded where the trait is declared");
5010 assert_eq!(conformance.trait_module, "display");
5011 assert_eq!(conformance.type_module, "booking");
5012 assert!(program.modules["display"]
5014 .methods
5015 .contains_key(&("Booking".to_string(), "describe".to_string())));
5016 assert!(program.modules["booking"].methods.is_empty());
5017 }
5018
5019 #[test]
5022 fn a_third_module_may_not_conform_an_imported_type_to_an_imported_trait() {
5023 let diagnostic = resolve_err(
5024 &[
5025 ("display", DISPLAY),
5026 ("booking", BOOKING),
5027 (
5028 "app",
5029 "use display.Display\nuse booking.Booking\n\n\
5030 impl Display for Booking {\n /// The full form.\n fn describe(self) -> String {\n \"b\"\n }\n}\n",
5031 ),
5032 ],
5033 "cove::resolve::orphan_conformance",
5034 );
5035 assert!(diagnostic.message.contains("declares neither"));
5036 }
5037
5038 #[test]
5042 fn an_inherent_impl_may_not_extend_an_imported_type() {
5043 let diagnostic = resolve_err(
5044 &[
5045 ("booking", BOOKING),
5046 (
5047 "app",
5048 "use booking.Booking\n\nimpl Booking {\n /// The id.\n fn id(self) -> Int {\n self.id\n }\n}\n",
5049 ),
5050 ],
5051 "cove::resolve::foreign_inherent_impl",
5052 );
5053 assert!(diagnostic.help.as_deref().unwrap().contains("booking"));
5054 }
5055
5056 #[test]
5060 fn a_conformance_may_not_collide_with_the_type_s_own_method() {
5061 let diagnostic = resolve_err(
5062 &[
5063 (
5064 "booking",
5065 &format!(
5066 "{BOOKING}\nimpl Booking {{\n /// Describes.\n export fn describe(self) -> String {{\n \"inherent\"\n }}\n}}\n"
5067 ),
5068 ),
5069 (
5070 "display",
5071 &format!(
5072 "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n \
5073 /// The full form.\n fn describe(self) -> String {{\n \"conformance\"\n }}\n}}\n"
5074 ),
5075 ),
5076 ],
5077 "cove::resolve::duplicate_declaration",
5078 );
5079 assert!(diagnostic.message.contains("Booking.describe"));
5080 assert!(diagnostic.message.contains("display"));
5081 assert!(diagnostic.message.contains("booking"));
5082 }
5083
5084 #[test]
5087 fn two_modules_may_not_give_one_type_the_same_method_name() {
5088 let diagnostic = resolve_err(
5089 &[
5090 ("booking", BOOKING),
5091 (
5092 "display",
5093 &format!(
5094 "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n \
5095 /// The full form.\n fn describe(self) -> String {{\n \"d\"\n }}\n}}\n"
5096 ),
5097 ),
5098 (
5099 "audit",
5100 "use booking.Booking\n\n\
5101 /// Audits itself.\nexport trait Audit {\n /// The full form.\n fn describe(self) -> String\n}\n\n\
5102 impl Audit for Booking {\n /// The full form.\n fn describe(self) -> String {\n \"a\"\n }\n}\n",
5103 ),
5104 ],
5105 "cove::resolve::duplicate_declaration",
5106 );
5107 assert!(diagnostic.message.contains("Booking.describe"));
5108 }
5109
5110 #[test]
5115 fn one_conformance_cannot_be_declared_in_both_parties_modules() {
5116 let diagnostic = resolve_err(
5117 &[
5118 (
5119 "display",
5120 &format!(
5121 "use booking.Booking\n\n{DISPLAY}\nimpl Display for Booking {{\n \
5122 /// The full form.\n fn describe(self) -> String {{\n \"d\"\n }}\n}}\n"
5123 ),
5124 ),
5125 (
5126 "booking",
5127 &format!(
5128 "use display.Display\n\n{BOOKING}\nimpl Display for Booking {{\n \
5129 /// The full form.\n fn describe(self) -> String {{\n \"b\"\n }}\n}}\n"
5130 ),
5131 ),
5132 ],
5133 "cove::resolve::import_cycle",
5134 );
5135 assert!(diagnostic.message.contains(" -> "));
5136 }
5137
5138 #[test]
5139 fn an_import_colliding_with_a_declared_trait_is_rejected() {
5140 let diagnostic = resolve_err(
5141 &[
5142 ("display", DISPLAY),
5143 (
5144 "app",
5145 "use display.Display\n\ntrait Display {\n /// The full form.\n fn describe(self) -> String\n}\n",
5146 ),
5147 ],
5148 "cove::resolve::import_conflict",
5149 );
5150 assert!(diagnostic.message.contains("also declares it"));
5151 }
5152
5153 #[test]
5154 fn a_use_of_a_private_trait_is_rejected() {
5155 resolve_err(
5156 &[
5157 (
5158 "display",
5159 "trait Display {\n /// The full form.\n fn describe(self) -> String\n}\n",
5160 ),
5161 ("app", "use display.Display\n"),
5162 ],
5163 "cove::resolve::private_declaration",
5164 );
5165 }
5166
5167 #[test]
5168 fn a_conformance_naming_a_trait_no_module_declares_is_rejected() {
5169 let diagnostic = resolve_err(
5170 &[(
5171 "booking",
5172 &format!("{BOOKING}\nimpl Display for Booking {{\n fn describe(self) -> String {{\n \"b\"\n }}\n}}\n"),
5173 )],
5174 "cove::resolve::unknown_trait",
5175 );
5176 assert!(diagnostic.help.as_deref().unwrap().contains("use"));
5177 }
5178
5179 #[test]
5182 fn required_capabilities_cross_a_conformance_in_another_module() {
5183 let program = resolve_ok(&[
5184 ("booking", BOOKING),
5185 (
5186 "display",
5187 &format!(
5188 "use console.println\nuse booking.Booking\n\n{DISPLAY}\n\
5189 impl Display for Booking {{\n /// The full form.\n fn describe(self) -> String {{\n \
5190 console.println(\"tracing\")\n \"b\"\n }}\n}}\n"
5191 ),
5192 ),
5193 (
5194 "app",
5195 "use booking.Booking\nuse display.Display\n\n\
5196 /// Entry point.\nexport fn main(value: Booking) -> String {\n value.describe()\n}\n",
5197 ),
5198 ]);
5199 assert!(program.modules["app"].functions["main"]
5200 .required_capabilities
5201 .contains(&Capability::new("console")));
5202 }
5203
5204 #[test]
5207 fn a_bare_case_of_an_imported_enum_resolves() {
5208 let diagnostic = resolve_err(
5209 &[
5210 (
5211 "levels",
5212 "/// Levels.\nexport enum LogLevel {\n Debug\n Info\n}\n",
5213 ),
5214 (
5215 "app",
5216 "use levels.LogLevel\n\n/// Describes a level.\nexport fn describe(level: LogLevel) -> String {\n \
5217 match level {\n Debug => \"debug\"\n }\n}\n",
5218 ),
5219 ],
5220 "cove::resolve::non_exhaustive_match",
5221 );
5222 assert!(diagnostic.message.contains("LogLevel.Info"));
5223 }
5224
5225 #[test]
5226 fn ambiguous_unqualified_use_is_rejected() {
5227 let module =
5228 module_from_sources("ambiguous", &["use console.println\nuse other.println\n"]);
5229 let package = package_of(module);
5230 let errs = resolve(&package).unwrap_err();
5231 assert!(errs
5232 .iter()
5233 .any(|d| d.code == "cove::resolve::ambiguous_use"));
5234 }
5235
5236 #[test]
5237 fn derives_capability_from_qualified_call() {
5238 let module = module_from_sources(
5239 "cap",
5240 &[
5241 "use console.println\n\n/// Prints.\nexport fn main() {\n console.println(\"hi\")\n}\n",
5242 ],
5243 );
5244 let package = package_of(module);
5245 let program = resolve(&package).expect("resolves");
5246 let entry = &program.modules["cap"].functions["main"];
5247 assert!(entry
5248 .direct_capabilities
5249 .contains(&Capability::new("console")));
5250 }
5251
5252 #[test]
5253 fn derives_capability_from_unqualified_call() {
5254 let module = module_from_sources(
5255 "cap2",
5256 &["use console.println\n\n/// Prints.\nexport fn main() {\n println(\"hi\")\n}\n"],
5257 );
5258 let package = package_of(module);
5259 let program = resolve(&package).expect("resolves");
5260 let entry = &program.modules["cap2"].functions["main"];
5261 assert!(entry
5262 .direct_capabilities
5263 .contains(&Capability::new("console")));
5264 }
5265
5266 #[test]
5267 fn finds_a_host_call_inside_a_closure() {
5268 let module = module_from_sources(
5269 "cap3",
5270 &[
5271 "use console.println\n\n/// Builds a callback.\nexport fn build() {\n let cb = fn() {\n console.println(\"hi\")\n }\n}\n",
5272 ],
5273 );
5274 let package = package_of(module);
5275 let program = resolve(&package).expect("resolves");
5276 let entry = &program.modules["cap3"].functions["build"];
5277 assert!(entry
5278 .direct_capabilities
5279 .contains(&Capability::new("console")));
5280 }
5281
5282 #[test]
5283 fn warns_on_missing_doc_for_exported_declaration() {
5284 let module = module_from_sources("nodoc", &["export fn main() {\n}\n"]);
5285 let package = package_of(module);
5286 let program = resolve(&package).expect("resolves even with a warning");
5287 assert!(program
5288 .notices
5289 .iter()
5290 .any(|d| d.code == "cove::resolve::missing_doc"));
5291 }
5292
5293 #[test]
5294 fn private_declaration_without_doc_does_not_warn() {
5295 let module = module_from_sources("private", &["fn helper() {\n}\n"]);
5296 let package = package_of(module);
5297 let program = resolve(&package).expect("resolves");
5298 assert!(program.notices.is_empty());
5299 }
5300
5301 #[test]
5302 fn loads_and_resolves_the_real_examples_package() {
5303 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
5304 let mut sources = SourceMap::new();
5305 let package = crate::package::load(&root, &mut sources).expect("examples package loads");
5306 let program = resolve(&package);
5307 assert!(program.is_ok(), "examples package should resolve cleanly");
5308 }
5309
5310 #[test]
5311 fn required_capabilities_reach_through_a_helper_chain() {
5312 let module = module_from_sources(
5313 "chain",
5314 &["use console.println\n\n\
5315 /// Logs a message.\n\
5316 fn log(msg: String) {\n console.println(msg)\n}\n\n\
5317 /// Entry point; never calls a Host API directly.\n\
5318 export fn main() {\n log(\"hi\")\n}\n"],
5319 );
5320 let package = package_of(module);
5321 let program = resolve(&package).expect("resolves");
5322 let main = &program.modules["chain"].functions["main"];
5323 assert!(main.direct_capabilities.is_empty());
5324 assert!(main
5325 .required_capabilities
5326 .contains(&Capability::new("console")));
5327 }
5328
5329 #[test]
5330 fn required_capabilities_reach_through_a_method_call() {
5331 let module = module_from_sources(
5332 "methodprop",
5333 &["use console.println\n\n\
5334 /// A thing with an id.\n\
5335 export struct Thing {\n id: String\n}\n\n\
5336 impl Thing {\n \
5337 /// Prints the id.\n \
5338 fn touch(self) {\n console.println(self.id)\n }\n}\n\n\
5339 /// Entry point that reaches the Host API only through `Thing.touch`.\n\
5340 export fn main() {\n Thing.touch()\n}\n"],
5341 );
5342 let package = package_of(module);
5343 let program = resolve(&package).expect("resolves");
5344 let touch =
5345 &program.modules["methodprop"].methods[&("Thing".to_string(), "touch".to_string())];
5346 assert!(touch
5347 .direct_capabilities
5348 .contains(&Capability::new("console")));
5349 let main = &program.modules["methodprop"].functions["main"];
5350 assert!(main.direct_capabilities.is_empty());
5351 assert!(main
5352 .required_capabilities
5353 .contains(&Capability::new("console")));
5354 }
5355
5356 #[test]
5357 fn required_capabilities_propagate_through_mutual_recursion() {
5358 let module = module_from_sources(
5359 "mutual",
5360 &["use console.println\n\n\
5361 /// True when `n` is even; recurses through `isOdd`.\n\
5362 fn isEven(n: Int) -> Bool {\n \
5363 if n == 0 {\n true\n } else {\n isOdd(n - 1)\n }\n}\n\n\
5364 /// True when `n` is odd; logs, then recurses through `isEven`.\n\
5365 fn isOdd(n: Int) -> Bool {\n \
5366 console.println(\"checking\")\n \
5367 if n == 0 {\n false\n } else {\n isEven(n - 1)\n }\n}\n\n\
5368 /// Entry point.\n\
5369 export fn main() -> Bool {\n isEven(4)\n}\n"],
5370 );
5371 let package = package_of(module);
5372 let program = resolve(&package).expect("resolves");
5373 let resolved = &program.modules["mutual"];
5374
5375 assert!(resolved.functions["isOdd"]
5376 .direct_capabilities
5377 .contains(&Capability::new("console")));
5378 assert!(resolved.functions["isEven"].direct_capabilities.is_empty());
5379
5380 assert!(resolved.functions["isEven"]
5384 .required_capabilities
5385 .contains(&Capability::new("console")));
5386 assert!(resolved.functions["main"]
5387 .required_capabilities
5388 .contains(&Capability::new("console")));
5389 }
5390
5391 const COMPANY: ModuleSchema = ModuleSchema {
5398 name: "company",
5399 capability: "directory",
5400 operations: &[
5401 OperationSchema {
5402 name: "employee",
5403 params: &[],
5404 variadic: false,
5405 result: HostType::Unit,
5406 capability: "directory",
5407 effect: Effect::Read,
5408 cancellable: false,
5409 recordable: true,
5410 result_is_task_safe: true,
5411 },
5412 OperationSchema {
5413 name: "payroll",
5414 params: &[],
5415 variadic: false,
5416 result: HostType::Unit,
5417 capability: "payroll",
5418 effect: Effect::Read,
5419 cancellable: false,
5420 recordable: true,
5421 result_is_task_safe: true,
5422 },
5423 ],
5424 types: &[],
5425 resources: &[],
5426 };
5427
5428 #[test]
5429 fn required_capabilities_use_the_operation_s_capability_not_the_module_s() {
5430 let schemas = HostSchemas::new().with(COMPANY);
5431 let program = resolve_ok_with(
5432 &[(
5433 "app",
5434 "use company\n\n/// Entry point.\nexport fn main() {\n company.payroll()\n}\n",
5435 )],
5436 &schemas,
5437 );
5438 let main = &program.modules["app"].functions["main"];
5439 assert!(main
5440 .required_capabilities
5441 .contains(&Capability::new("payroll")));
5442 assert!(!main
5443 .required_capabilities
5444 .contains(&Capability::new("directory")));
5445 }
5446
5447 #[test]
5448 fn required_capabilities_fall_back_to_the_module_s_capability_for_an_undeclared_operation() {
5449 let schemas = HostSchemas::new().with(COMPANY);
5450 let program = resolve_ok_with(
5451 &[(
5452 "app",
5453 "use company\n\n/// Entry point; calls an operation the schema does not declare.\nexport fn main() {\n company.other()\n}\n",
5454 )],
5455 &schemas,
5456 );
5457 let main = &program.modules["app"].functions["main"];
5458 assert!(main
5459 .required_capabilities
5460 .contains(&Capability::new("directory")));
5461 assert!(!main
5462 .required_capabilities
5463 .contains(&Capability::new("payroll")));
5464 }
5465
5466 #[test]
5467 fn a_function_requiring_nothing_stays_empty() {
5468 let module = module_from_sources(
5469 "pure",
5470 &[
5471 "/// Adds two numbers.\nfn add(a: Int, b: Int) -> Int {\n a + b\n}\n\n\
5472 /// Entry point; calls only a pure helper.\n\
5473 export fn main() -> Int {\n add(1, 2)\n}\n",
5474 ],
5475 );
5476 let package = package_of(module);
5477 let program = resolve(&package).expect("resolves");
5478 let resolved = &program.modules["pure"];
5479 assert!(resolved.functions["add"].required_capabilities.is_empty());
5480 assert!(resolved.functions["main"].required_capabilities.is_empty());
5481 }
5482
5483 #[test]
5484 fn derives_required_capabilities_for_the_real_examples_package() {
5485 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
5486 let mut sources = SourceMap::new();
5487 let package = crate::package::load(&root, &mut sources).expect("examples package loads");
5488 let program = resolve(&package).expect("examples package resolves");
5489
5490 let hello_main = &program.modules["hello"].functions["main"];
5491 assert!(hello_main
5492 .required_capabilities
5493 .contains(&Capability::new("console")));
5494
5495 let hello_greeting = &program.modules["hello"].functions["greeting"];
5496 assert!(hello_greeting.required_capabilities.is_empty());
5497
5498 let restricted_main = &program.modules["restricted"].functions["main"];
5499 assert!(restricted_main
5500 .required_capabilities
5501 .contains(&Capability::new("documents")));
5502 assert!(restricted_main
5503 .required_capabilities
5504 .contains(&Capability::new("console")));
5505
5506 let config_load_config = &program.modules["config"].functions["loadConfig"];
5507 assert!(config_load_config
5508 .required_capabilities
5509 .contains(&Capability::new("env")));
5510 }
5511
5512 #[track_caller]
5516 fn function<'a>(program: &'a Program, module: &str, name: &str) -> &'a FnEntry {
5517 program
5518 .lookup_fn(module, name)
5519 .unwrap_or_else(|| panic!("`{module}.{name}` is declared"))
5520 }
5521
5522 #[test]
5523 fn calling_a_function_typed_parameter_is_capability_open() {
5524 let program = resolve_ok(&[(
5525 "higher",
5526 "/// Runs whatever it was handed.\n\
5527 export fn run(work: fn() -> Unit) {\n work()\n}\n",
5528 )]);
5529 let run = function(&program, "higher", "run");
5530 assert!(run.required_capabilities.is_empty());
5531 assert_eq!(
5532 run.open_calls,
5533 BTreeSet::from([OpenCall::FunctionValue]),
5534 "a call to a value the call graph cannot name is the higher-order case"
5535 );
5536 }
5537
5538 #[test]
5543 fn a_closure_that_calls_a_host_charges_the_function_that_wrote_it() {
5544 let program = resolve_ok(&[(
5545 "callback",
5546 "use console.println\n\n\
5547 /// Runs whatever it was handed.\n\
5548 fn run(work: fn() -> Unit) {\n work()\n}\n\n\
5549 /// Hands `run` a closure that prints.\n\
5550 export fn main() {\n run(fn() {\n console.println(\"hi\")\n })\n}\n",
5551 )]);
5552
5553 let main = function(&program, "callback", "main");
5554 assert!(
5555 main.direct_capabilities
5556 .contains(&Capability::new("console")),
5557 "the closure's body is part of the body that wrote it"
5558 );
5559
5560 let run = function(&program, "callback", "run");
5561 assert!(run.required_capabilities.is_empty());
5562 assert!(run.is_capability_open());
5563 assert_eq!(
5564 main.open_calls,
5565 BTreeSet::from([OpenCall::ReachedOpenCall]),
5566 "calling a capability-open declaration makes its caller one too"
5567 );
5568 }
5569
5570 #[test]
5571 fn calling_a_method_on_a_dyn_parameter_is_capability_open() {
5572 let program = resolve_ok(&[(
5573 "dynamic",
5574 "/// Something that describes itself.\n\
5575 export trait Summary {\n \
5576 /// One line about this value.\n \
5577 fn summarize(self) -> String\n}\n\n\
5578 /// Renders entries whose types may differ.\n\
5579 export fn report(entries: Array<dyn Summary>) -> String {\n \
5580 var text = \"\"\n \
5581 for entry in entries {\n text = entry.summarize()\n }\n text\n}\n",
5582 )]);
5583 assert_eq!(
5584 function(&program, "dynamic", "report").open_calls,
5585 BTreeSet::from([OpenCall::DynamicDispatch]),
5586 "a `dyn` value taken out of a container still dispatches by its own type"
5587 );
5588 }
5589
5590 #[test]
5591 fn calling_a_method_on_a_bounded_generic_is_capability_open() {
5592 let program = resolve_ok(&[(
5593 "generic",
5594 "/// Something that describes itself.\n\
5595 export trait Summary {\n \
5596 /// One line about this value.\n \
5597 fn summarize(self) -> String\n}\n\n\
5598 /// Headlines one entry.\n\
5599 export fn headline<T: Summary>(entry: T) -> String {\n entry.summarize()\n}\n",
5600 )]);
5601 assert_eq!(
5602 function(&program, "generic", "headline").open_calls,
5603 BTreeSet::from([OpenCall::DynamicDispatch]),
5604 "the caller instantiates `T`, so it also picks the conformance that runs"
5605 );
5606 }
5607
5608 #[test]
5609 fn calling_a_callback_stored_in_data_is_capability_open() {
5610 let program = resolve_ok(&[(
5611 "stored",
5612 "/// Runs every handler in turn.\n\
5613 export fn dispatch(handlers: Array<fn() -> Unit>) {\n \
5614 for handler in handlers {\n handler()\n }\n}\n",
5615 )]);
5616 assert_eq!(
5617 function(&program, "stored", "dispatch").open_calls,
5618 BTreeSet::from([OpenCall::FunctionValue])
5619 );
5620 }
5621
5622 #[test]
5627 fn ordinary_calls_leave_a_function_capability_closed() {
5628 let program = resolve_ok(&[(
5629 "closed",
5630 "use console.println\n\n\
5631 /// A thing with an id.\n\
5632 export struct Thing {\n id: String\n}\n\n\
5633 /// Makes one.\n\
5634 fn make() -> Thing {\n Thing(id: \"a\")\n}\n\n\
5635 /// Entry point.\n\
5636 export fn main() -> Result<Unit, Error> {\n \
5637 let thing = make()\n \
5638 let items = Vector.of(thing.id)\n \
5639 println(\"{items.length()}\")?\n \
5640 assert(true)?\n \
5641 Ok(())\n}\n",
5642 )]);
5643 let main = function(&program, "closed", "main");
5644 assert!(!main.is_capability_open(), "found {:?}", main.open_calls);
5645 assert!(main
5646 .required_capabilities
5647 .contains(&Capability::new("console")));
5648 }
5649
5650 #[test]
5654 fn naming_a_function_as_a_value_reaches_what_it_requires() {
5655 let program = resolve_ok(&[(
5656 "reentry",
5657 "use http\n\
5658 use console.println\n\n\
5659 /// Answers one request, and says so on the console.\n\
5660 fn health(request: http.Request) -> http.Response {\n \
5661 console.println(\"served\")\n \
5662 http.json(200, \"ok\")\n}\n\n\
5663 /// Registers the handler the host will call back.\n\
5664 export fn routes() -> Array<http.Route> {\n \
5665 [http.Route(method: http.Method.Get, path: \"/health\", handler: health)]\n}\n",
5666 )]);
5667 let routes = function(&program, "reentry", "routes");
5668 assert!(
5669 routes
5670 .required_capabilities
5671 .contains(&Capability::new("console")),
5672 "a callback the host will invoke is reached through the name that stored it"
5673 );
5674 assert!(
5675 !routes.is_capability_open(),
5676 "nothing here is a call the graph could not follow"
5677 );
5678 }
5679
5680 #[test]
5688 fn dispatching_through_a_dyn_struct_field_is_capability_open() {
5689 let program = resolve_ok(&[
5690 (
5691 "lib",
5692 "/// Something that describes itself.\n\
5693 export trait Summary {\n \
5694 /// One line about this value.\n \
5695 fn summarize(self) -> String\n}\n\n\
5696 /// Holds one of them.\n\
5697 export struct Box {\n item: dyn Summary\n}\n\n\
5698 impl Box {\n \
5699 /// Shows what it holds.\n \
5700 export fn show(self) -> String {\n self.item.summarize()\n }\n}\n",
5701 ),
5702 (
5703 "plugin",
5704 "use console.println\n\
5705 use lib.Summary\n\n\
5706 /// Says so out loud.\n\
5707 export struct Noisy {\n n: Int\n}\n\n\
5708 impl Summary for Noisy {\n \
5709 /// One line about this value.\n \
5710 fn summarize(self) -> String {\n \
5711 let ignored = println(\"side effect\")\n \"noisy\"\n }\n}\n",
5712 ),
5713 (
5714 "app",
5715 "use lib\nuse plugin\n\n\
5716 /// Entry point.\n\
5717 export fn main() -> Result<Unit, Error> {\n \
5718 let held = lib.Box(item: plugin.Noisy(n: 1))\n \
5719 let text = held.show()\n \
5720 Ok(())\n}\n",
5721 ),
5722 ]);
5723 let show = &program.modules["lib"].methods[&("Box".to_string(), "show".to_string())];
5724 assert_eq!(
5725 show.open_calls,
5726 BTreeSet::from([OpenCall::DynamicDispatch]),
5727 "a `dyn` field is a value whose implementation its producer chose"
5728 );
5729 assert!(
5730 function(&program, "app", "main").is_capability_open(),
5731 "openness has to reach the entry, or its empty set reads as complete"
5732 );
5733 }
5734
5735 #[test]
5739 fn a_method_on_a_container_of_generics_is_not_dynamic_dispatch() {
5740 let program = resolve_ok(&[(
5741 "counting",
5742 "/// How many entries there are.\n\
5743 export fn count<T>(items: Array<T>) -> Int {\n items.length()\n}\n",
5744 )]);
5745 let count = function(&program, "counting", "count");
5746 assert!(!count.is_capability_open(), "found {:?}", count.open_calls);
5747 }
5748
5749 #[test]
5751 fn a_method_on_an_element_of_a_dyn_container_is_dynamic_dispatch() {
5752 let program = resolve_ok(&[(
5753 "element",
5754 "/// Something that describes itself.\n\
5755 export trait Summary {\n \
5756 /// One line about this value.\n \
5757 fn summarize(self) -> String\n}\n\n\
5758 /// The first entry's line, or nothing.\n\
5759 export fn first(entries: Array<dyn Summary>) -> String {\n \
5760 entries.get(0).map(fn(entry) {\n entry.summarize()\n }).unwrapOr(\"\")\n}\n",
5761 )]);
5762 assert_eq!(
5763 function(&program, "element", "first").open_calls,
5764 BTreeSet::from([OpenCall::DynamicDispatch]),
5765 "an element taken out of a `dyn` container dispatches by its own type"
5766 );
5767 }
5768
5769 #[test]
5773 fn a_parameter_shadowing_a_function_records_no_edge() {
5774 let program = resolve_ok(&[(
5775 "shadow",
5776 "use console.println\n\n\
5777 /// Prints one line.\n\
5778 fn report(text: String) -> Result<Unit, Error> {\n println(text)\n}\n\n\
5779 /// Returns what it was given.\n\
5780 export fn label(report: String) -> String {\n report\n}\n",
5781 )]);
5782 let label = function(&program, "shadow", "label");
5783 assert!(
5784 label.required_capabilities.is_empty(),
5785 "found {:?}",
5786 label.required_capabilities
5787 );
5788 assert!(!label.is_capability_open(), "found {:?}", label.open_calls);
5789 }
5790
5791 #[test]
5795 fn a_local_fn_is_charged_to_the_body_that_wrote_it() {
5796 let program = resolve_ok(&[(
5797 "local",
5798 "use console.println\n\n\
5799 /// Entry point.\n\
5800 export fn main() -> Result<Unit, Error> {\n \
5801 /// Prints once.\n \
5802 fn helper() -> Result<Unit, Error> {\n println(\"hi\")\n }\n \
5803 helper()?\n Ok(())\n}\n",
5804 )]);
5805 let main = function(&program, "local", "main");
5806 assert!(main
5807 .required_capabilities
5808 .contains(&Capability::new("console")));
5809 assert!(!main.is_capability_open(), "found {:?}", main.open_calls);
5810 }
5811
5812 #[test]
5813 fn openness_crosses_a_module_boundary() {
5814 let program = resolve_ok(&[
5815 (
5816 "runner",
5817 "/// Runs whatever it was handed.\n\
5818 export fn run(work: fn() -> Unit) {\n work()\n}\n",
5819 ),
5820 (
5821 "app",
5822 "use runner.run\n\n\
5823 /// Entry point.\n\
5824 export fn main() {\n run(fn() {\n })\n}\n",
5825 ),
5826 ]);
5827 assert!(function(&program, "app", "main").is_capability_open());
5828 }
5829
5830 #[test]
5831 fn match_covering_every_enum_case_passes() {
5832 let module = module_from_sources(
5833 "exhaustive",
5834 &["enum LogLevel {\n Debug\n Info\n}\n\n\
5835 fn describe(level: LogLevel) -> String {\n \
5836 match level {\n \
5837 LogLevel.Debug => \"debug\"\n \
5838 LogLevel.Info => \"info\"\n \
5839 }\n}\n"],
5840 );
5841 let package = package_of(module);
5842 let program = resolve(&package).expect("resolves");
5843 assert!(program.notices.is_empty());
5844 }
5845
5846 #[test]
5847 fn missing_case_is_reported_by_name() {
5848 let module = module_from_sources(
5849 "missing",
5850 &["enum LogLevel {\n Debug\n Info\n Warn\n Error\n}\n\n\
5851 fn describe(level: LogLevel) -> String {\n \
5852 match level {\n \
5853 LogLevel.Debug => \"debug\"\n \
5854 LogLevel.Info => \"info\"\n \
5855 }\n}\n"],
5856 );
5857 let package = package_of(module);
5858 let errs = resolve(&package).unwrap_err();
5859 let diag = errs
5860 .iter()
5861 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5862 .expect("reports non_exhaustive_match");
5863 assert!(diag.message.contains("LogLevel.Warn"));
5864 assert!(diag.message.contains("LogLevel.Error"));
5865 assert!(diag.help.as_deref().unwrap().contains("LogLevel.Warn"));
5866 }
5867
5868 #[test]
5869 fn a_wildcard_arm_makes_a_partial_match_exhaustive() {
5870 let module = module_from_sources(
5871 "wildcard_ok",
5872 &["enum LogLevel {\n Debug\n Info\n Warn\n Error\n}\n\n\
5873 fn describe(level: LogLevel) -> String {\n \
5874 match level {\n \
5875 LogLevel.Debug => \"debug\"\n \
5876 _ => \"other\"\n \
5877 }\n}\n"],
5878 );
5879 let package = package_of(module);
5880 let program = resolve(&package).expect("resolves");
5881 assert!(!program
5882 .notices
5883 .iter()
5884 .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
5885 }
5886
5887 #[test]
5888 fn option_match_covering_both_cases_passes() {
5889 let module = module_from_sources(
5890 "option_ok",
5891 &["fn describe(value: Option<Int>) -> Int {\n \
5892 match value {\n \
5893 Some(x) => x\n \
5894 None => 0\n \
5895 }\n}\n"],
5896 );
5897 let package = package_of(module);
5898 resolve(&package).expect("resolves");
5899 }
5900
5901 #[test]
5902 fn option_match_missing_none_is_reported() {
5903 let module = module_from_sources(
5904 "option_missing",
5905 &["fn describe(value: Option<Int>) -> Int {\n \
5906 match value {\n \
5907 Some(x) => x\n \
5908 }\n}\n"],
5909 );
5910 let package = package_of(module);
5911 let errs = resolve(&package).unwrap_err();
5912 let diag = errs
5913 .iter()
5914 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5915 .expect("reports non_exhaustive_match");
5916 assert!(diag.message.contains("None"));
5917 }
5918
5919 #[test]
5920 fn result_match_covering_both_cases_passes() {
5921 let module = module_from_sources(
5922 "result_ok",
5923 &["fn describe(value: Result<Int, Error>) -> Int {\n \
5924 match value {\n \
5925 Ok(x) => x\n \
5926 Err(e) => 0\n \
5927 }\n}\n"],
5928 );
5929 let package = package_of(module);
5930 resolve(&package).expect("resolves");
5931 }
5932
5933 #[test]
5934 fn result_match_missing_err_is_reported() {
5935 let module = module_from_sources(
5936 "result_missing",
5937 &["fn describe(value: Result<Int, Error>) -> Int {\n \
5938 match value {\n \
5939 Ok(x) => x\n \
5940 }\n}\n"],
5941 );
5942 let package = package_of(module);
5943 let errs = resolve(&package).unwrap_err();
5944 let diag = errs
5945 .iter()
5946 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
5947 .expect("reports non_exhaustive_match");
5948 assert!(diag.message.contains("Err"));
5949 }
5950
5951 #[test]
5952 fn unknown_enum_case_is_reported() {
5953 let module = module_from_sources(
5954 "unknown_case",
5955 &["enum LogLevel {\n Debug\n Info\n}\n\n\
5956 fn describe(level: LogLevel) -> String {\n \
5957 match level {\n \
5958 LogLevel.Debug => \"debug\"\n \
5959 LogLevel.Bogus => \"bogus\"\n \
5960 }\n}\n"],
5961 );
5962 let package = package_of(module);
5963 let errs = resolve(&package).unwrap_err();
5964 assert!(errs
5965 .iter()
5966 .any(|d| d.code == "cove::resolve::unknown_enum_case"));
5967 }
5968
5969 #[test]
5970 fn duplicate_match_arm_is_reported() {
5971 let module = module_from_sources(
5972 "dup_arm",
5973 &["enum LogLevel {\n Debug\n Info\n}\n\n\
5974 fn describe(level: LogLevel) -> String {\n \
5975 match level {\n \
5976 LogLevel.Debug => \"first\"\n \
5977 LogLevel.Debug => \"second\"\n \
5978 LogLevel.Info => \"info\"\n \
5979 }\n}\n"],
5980 );
5981 let package = package_of(module);
5982 let errs = resolve(&package).unwrap_err();
5983 assert!(errs
5984 .iter()
5985 .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
5986 }
5987
5988 #[test]
5994 fn a_narrower_sub_pattern_does_not_cover_its_whole_case() {
5995 let module = module_from_sources(
5996 "nested_sub_pattern",
5997 &["enum Json {\n Text(String)\n Number(Int)\n}\n\n\
5998 fn describe(entry: Option<Json>) -> String {\n \
5999 match entry {\n \
6000 None => \"none\"\n \
6001 Some(Json.Text(value)) => value\n \
6002 Some(other) => \"other\"\n \
6003 }\n}\n"],
6004 );
6005 let package = package_of(module);
6006 let program = resolve(&package).expect("resolves: the arms do not overlap");
6007 assert!(!program
6008 .notices
6009 .iter()
6010 .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6011 }
6012
6013 #[test]
6016 fn a_binding_sub_pattern_covers_its_whole_case() {
6017 let module = module_from_sources(
6018 "binding_sub_pattern",
6019 &["fn describe(entry: Option<Int>) -> String {\n \
6020 match entry {\n \
6021 None => \"none\"\n \
6022 Some(first) => \"first\"\n \
6023 Some(second) => \"second\"\n \
6024 }\n}\n"],
6025 );
6026 let package = package_of(module);
6027 let errs = resolve(&package).unwrap_err();
6028 assert!(errs
6029 .iter()
6030 .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6031 }
6032
6033 #[test]
6037 fn identical_sub_patterns_are_still_a_duplicate() {
6038 let module = module_from_sources(
6039 "identical_sub_pattern",
6040 &["enum Json {\n Text(String)\n Number(Int)\n}\n\n\
6041 fn describe(entry: Option<Json>) -> String {\n \
6042 match entry {\n \
6043 None => \"none\"\n \
6044 Some(Json.Text(first)) => first\n \
6045 Some(Json.Text(second)) => second\n \
6046 }\n}\n"],
6047 );
6048 let package = package_of(module);
6049 let errs = resolve(&package).unwrap_err();
6050 assert!(errs
6051 .iter()
6052 .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6053 }
6054
6055 #[test]
6056 fn arm_after_a_wildcard_is_an_unreachable_warning() {
6057 let module = module_from_sources(
6058 "unreachable_arm",
6059 &["fn tag(n: Int) -> String {\n \
6060 match n {\n \
6061 _ => \"any\"\n \
6062 1 => \"one\"\n \
6063 }\n}\n"],
6064 );
6065 let package = package_of(module);
6066 let program = resolve(&package).expect("resolves; only a warning");
6067 assert!(program
6068 .notices
6069 .iter()
6070 .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6071 }
6072
6073 #[test]
6074 fn literal_match_over_non_bool_without_a_catch_all_arm_is_reported() {
6075 let module = module_from_sources(
6076 "literal_missing",
6077 &["fn tag(n: Int) -> String {\n \
6078 match n {\n \
6079 1 => \"one\"\n \
6080 2 => \"two\"\n \
6081 }\n}\n"],
6082 );
6083 let package = package_of(module);
6084 let errs = resolve(&package).unwrap_err();
6085 let diag = errs
6086 .iter()
6087 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6088 .expect("reports non_exhaustive_match");
6089 assert!(diag.message.contains("literal"));
6090 }
6091
6092 #[test]
6095 fn bool_match_covering_both_values_passes_without_a_catch_all() {
6096 let module = module_from_sources(
6097 "bool_ok",
6098 &["fn flag(on: Bool) -> String {\n \
6099 match on {\n \
6100 true => \"yes\"\n \
6101 false => \"no\"\n \
6102 }\n}\n"],
6103 );
6104 let package = package_of(module);
6105 let program = resolve(&package).expect("resolves");
6106 assert!(!program
6107 .notices
6108 .iter()
6109 .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
6110 }
6111
6112 #[test]
6113 fn bool_match_missing_false_is_reported_by_name() {
6114 let module = module_from_sources(
6115 "bool_missing_false",
6116 &["fn flag(on: Bool) -> String {\n \
6117 match on {\n \
6118 true => \"yes\"\n \
6119 }\n}\n"],
6120 );
6121 let package = package_of(module);
6122 let errs = resolve(&package).unwrap_err();
6123 let diag = errs
6124 .iter()
6125 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6126 .expect("reports non_exhaustive_match");
6127 assert!(diag.message.contains("`false`"));
6128 assert!(diag.help.as_deref().unwrap().contains("false"));
6129 }
6130
6131 #[test]
6132 fn bool_match_missing_true_is_reported_by_name() {
6133 let module = module_from_sources(
6134 "bool_missing_true",
6135 &["fn flag(on: Bool) -> String {\n \
6136 match on {\n \
6137 false => \"no\"\n \
6138 }\n}\n"],
6139 );
6140 let package = package_of(module);
6141 let errs = resolve(&package).unwrap_err();
6142 let diag = errs
6143 .iter()
6144 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6145 .expect("reports non_exhaustive_match");
6146 assert!(diag.message.contains("`true`"));
6147 assert!(diag.help.as_deref().unwrap().contains("true"));
6148 }
6149
6150 #[test]
6151 fn bool_match_with_both_values_and_a_wildcard_warns_the_wildcard_is_unreachable() {
6152 let module = module_from_sources(
6153 "bool_wildcard_unreachable",
6154 &["fn flag(on: Bool) -> String {\n \
6155 match on {\n \
6156 true => \"yes\"\n \
6157 false => \"no\"\n \
6158 _ => \"other\"\n \
6159 }\n}\n"],
6160 );
6161 let package = package_of(module);
6162 let program = resolve(&package).expect("resolves; only a warning");
6163 assert!(program
6164 .notices
6165 .iter()
6166 .any(|d| d.code == "cove::resolve::unreachable_match_arm"));
6167 }
6168
6169 #[test]
6170 fn duplicate_bool_match_arm_is_reported() {
6171 let module = module_from_sources(
6172 "bool_dup_arm",
6173 &["fn flag(on: Bool) -> String {\n \
6174 match on {\n \
6175 true => \"yes\"\n \
6176 true => \"also yes\"\n \
6177 false => \"no\"\n \
6178 }\n}\n"],
6179 );
6180 let package = package_of(module);
6181 let errs = resolve(&package).unwrap_err();
6182 assert!(errs
6183 .iter()
6184 .any(|d| d.code == "cove::resolve::duplicate_match_arm"));
6185 }
6186
6187 #[test]
6191 fn mixed_bool_and_int_literal_match_still_needs_a_catch_all() {
6192 let module = module_from_sources(
6193 "mixed_literal",
6194 &["fn describe(n: Int) -> String {\n \
6195 match n {\n \
6196 true => \"true?\"\n \
6197 1 => \"one\"\n \
6198 }\n}\n"],
6199 );
6200 let package = package_of(module);
6201 let errs = resolve(&package).unwrap_err();
6202 let diag = errs
6203 .iter()
6204 .find(|d| d.code == "cove::resolve::non_exhaustive_match")
6205 .expect("reports non_exhaustive_match");
6206 assert!(diag.message.contains("literal"));
6207 }
6208
6209 #[test]
6212 fn literal_match_with_a_binding_arm_passes() {
6213 let module = module_from_sources(
6214 "literal_ok",
6215 &["fn parseLevel(raw: String) -> String {\n \
6216 match raw {\n \
6217 \"debug\" => \"Debug\"\n \
6218 \"info\" => \"Info\"\n \
6219 other => other\n \
6220 }\n}\n"],
6221 );
6222 let package = package_of(module);
6223 let program = resolve(&package).expect("resolves");
6224 assert!(!program
6225 .notices
6226 .iter()
6227 .any(|d| d.code == "cove::resolve::non_exhaustive_match"));
6228 }
6229
6230 #[test]
6231 fn a_match_whose_enum_is_ambiguous_stays_silent() {
6232 let module = module_from_sources(
6233 "ambiguous_enum",
6234 &["enum Left {\n A\n B\n}\n\n\
6235 enum Right {\n A\n C\n}\n\n\
6236 fn pick(x: Int) -> Int {\n \
6237 match x {\n \
6238 A => 1\n \
6239 B => 2\n \
6240 }\n}\n"],
6241 );
6242 let package = package_of(module);
6243 let program = resolve(&package).expect("resolves; the enum cannot be determined");
6244 assert!(!program
6245 .notices
6246 .iter()
6247 .any(|d| d.code.starts_with("cove::resolve::") && d.code.contains("match")));
6248 }
6249
6250 #[test]
6251 fn break_and_continue_inside_a_loop_resolve_cleanly() {
6252 let module = module_from_sources(
6253 "loop_ok",
6254 &["fn firstEven(items: Int...) -> Int {\n \
6255 for item in items {\n \
6256 if item % 2 != 0 {\n continue\n }\n \
6257 break item\n \
6258 }\n}\n"],
6259 );
6260 let package = package_of(module);
6261 resolve(&package).expect("resolves");
6262 }
6263
6264 #[test]
6265 fn break_outside_a_loop_is_rejected() {
6266 let module = module_from_sources("break_bare", &["fn go() {\n break\n}\n"]);
6267 let package = package_of(module);
6268 let errs = resolve(&package).unwrap_err();
6269 assert!(errs
6270 .iter()
6271 .any(|d| d.code == "cove::resolve::break_outside_loop"));
6272 }
6273
6274 #[test]
6275 fn continue_outside_a_loop_is_rejected() {
6276 let module = module_from_sources("continue_bare", &["fn go() {\n continue\n}\n"]);
6277 let package = package_of(module);
6278 let errs = resolve(&package).unwrap_err();
6279 assert!(errs
6280 .iter()
6281 .any(|d| d.code == "cove::resolve::continue_outside_loop"));
6282 }
6283
6284 #[test]
6285 fn break_inside_a_lambda_cannot_reach_an_outer_loop() {
6286 let module = module_from_sources(
6287 "break_in_lambda",
6288 &["fn go() {\n for item in [1, 2] {\n \
6289 let f = fn() {\n break\n }\n \
6290 }\n}\n"],
6291 );
6292 let package = package_of(module);
6293 let errs = resolve(&package).unwrap_err();
6294 assert!(errs
6295 .iter()
6296 .any(|d| d.code == "cove::resolve::break_outside_loop"));
6297 }
6298}
6299
6300#[cfg(test)]
6301mod send_sync {
6302 use super::Program;
6303
6304 #[test]
6309 fn a_resolved_program_is_shareable_across_task_threads() {
6310 fn assert_send_sync<T: Send + Sync>() {}
6311 assert_send_sync::<Program>();
6312 }
6313}