use std::collections::BTreeMap; use crate::diagnostic::Diagnostic; use crate::hir::{Function, Program, TestCase}; use crate::source::{SourceLayout, SourceMap, SourceRole}; use crate::syntax::{Declaration, FunctionClause, Package}; use crate::types::{Definition, DefinitionKind, Shape}; mod api; mod auth; mod body; mod clauses; mod collections; mod components; mod concurrency; mod constants; mod cycles; mod expressions; mod intrinsics; mod jobs; mod layout; mod model; mod names; mod operators; mod orm; mod ports; mod sql_read; mod upload; pub(crate) use ports::test_program; mod symbols; mod values; pub(crate) use body::check_suspensions; pub(crate) use cycles::{visit, visit_expression}; type Checked = Result; struct Symbols { functions: BTreeMap<(String, usize), usize>, types: BTreeMap, packages: Vec, strict_layout: bool, } struct PackageScope { name: String, layout: SourceLayout, bundled: bool, implemented_ports: std::collections::BTreeSet, contract_types: std::collections::BTreeSet, } struct Context<'a> { symbols: &'a Symbols, types: &'a [Definition], functions: &'a [Function], models: &'a [crate::model::ModelSchema], owner: usize, } /// Checks every declaration, including bundled standard source and unused functions. pub fn check(sources: &SourceMap) -> Result> { cache::check(sources, check_uncached) } mod cache; fn check_uncached(sources: &SourceMap) -> Result> { check_configured(sources, None) } /// Deployment bindings participate in checking before ambient identity types resolve. /// They intentionally bypass the source-only cache; secrets are never retained in HIR. pub fn check_with_settings( sources: &SourceMap, settings: &dever_runtime::config::Settings, ) -> Result> { check_with_bindings(sources, &settings.compilation_bindings()) } pub fn check_with_bindings( sources: &SourceMap, bindings: &dever_runtime::config::CompilationBindings, ) -> Result> { check_configured(sources, Some(bindings)) } fn check_configured( sources: &SourceMap, settings: Option<&dever_runtime::config::CompilationBindings>, ) -> Result> { let mut errors = layout::validate(sources); if !errors.is_empty() { return Err(sorted(errors)); } let packages: Vec<_> = sources .all_files() .filter_map(|source| match crate::parse(source) { Ok(package) => Some(package), Err(diagnostics) => { errors.extend(diagnostics); None } }) .collect(); if !errors.is_empty() { return Err(sorted(errors)); } let mut symbols = Symbols { functions: BTreeMap::new(), types: BTreeMap::new(), packages: Vec::new(), strict_layout: sources.strict_layout(), }; let mut types = Vec::new(); let mut type_sources = Vec::new(); let mut functions: Vec = Vec::new(); let mut groups: Vec> = Vec::new(); for (owner, package) in packages.iter().enumerate() { if let Err(error) = names::check(package) { errors.push(error); } check_package(package, sources.strict_layout(), &mut errors); let package_name = path_name(&package.name); if symbols .packages .iter() .any(|scope| scope.name == package_name) { errors.push(Diagnostic::error("C002", "duplicate package", package.span)); } let bundled = SourceMap::is_standard(package.span.source); symbols.packages.push(PackageScope { name: package_name.clone(), layout: package.layout.clone(), bundled, implemented_ports: package .declarations .iter() .filter_map(|declaration| { let Declaration::Function(function) = declaration else { return None; }; let (port, _) = function.name.text.rsplit_once('.')?; Some(format!("{}.{}", package.layout.app_prefix()?, port)) }) .collect(), contract_types: std::collections::BTreeSet::new(), }); let mut last_function = None; for declaration in &package.declarations { match declaration { Declaration::Type(ty) => { let qualified = declaration_name(package, &package_name, &ty.name.text); let public = declaration_public(package, bundled, ty.public); last_function = None; if symbols .types .insert(qualified.clone(), types.len()) .is_some() { errors.push(Diagnostic::error("C002", "duplicate type", ty.span)); } types.push(Definition { name: qualified, owner, public, shape: Shape::Record(Vec::new()), span: ty.span, properties: Default::default(), kind: DefinitionKind::Regular, }); type_sources.push(Some(ty)); } Declaration::Function(clause) => { let qualified = declaration_name(package, &package_name, &clause.name.text); let public = declaration_public(package, bundled, clause.public); let key = (qualified.clone(), clause.inputs.len()); if let Some(id) = symbols.functions.get(&key).copied() { if functions[id].owner != owner { errors.push(Diagnostic::error( "C002", format!("duplicate App function '{qualified}'"), clause.span, )); last_function = None; continue; } if functions[id].public != public { errors.push(Diagnostic::error( "C006", "all clauses of a function must use the same public visibility", clause.span, )); } if last_function != Some(id) { errors.push(Diagnostic::error( "C002", "function clauses must be contiguous", clause.span, )); } groups[id].push(clause); last_function = Some(id); } else { let id = functions.len(); symbols.functions.insert(key, id); functions.push(Function { name: qualified, owner, public, parameters: Vec::new(), outputs: Vec::new(), clauses: Vec::new(), span: clause.span, suspends: false, pure: clause.pure, recovery: clause.recovery.clone(), kind: clause.kind, port: None, implementation: clause.name.text.contains('.'), setting: None, external: None, }); groups.push(vec![clause]); last_function = Some(id); } } Declaration::Database(_) | Declaration::Schedule(_) | Declaration::ModelIndex(_) | Declaration::Relation(_) | Declaration::Seed(_) | Declaration::Migration(_) | Declaration::ModelSql(_) | Declaration::Api(_) | Declaration::Rest(_) | Declaration::External(_) => last_function = None, } } } let model_registrations = model::register( &packages, sources, &mut symbols, &mut types, &mut type_sources, &mut errors, ); if !errors.is_empty() { return Err(sorted(errors)); } ports::expand_contract_types(&packages, &mut symbols, &types, &type_sources); for (id, source) in type_sources.iter().enumerate() { let Some(source) = source else { continue; }; let context = Context { symbols: &symbols, types: &types, functions: &functions, models: &[], owner: types[id].owner, }; match context.shape(&source.shape) { Ok(shape) => types[id].shape = shape, Err(error) => errors.push(error), } } model::synthesize_fields(&model_registrations, &mut types, &mut errors); if !errors.is_empty() { return Err(sorted(errors)); } let models = model::schemas( &model_registrations, &packages, &symbols, &types, &mut errors, ); let model_rest_sources = model::rest_sources(&models, &packages, &types); if !errors.is_empty() { return Err(sorted(errors)); } model::synthesize_relation_fields(&models, &mut types, &mut errors); symbols::validate_types(&types, &mut errors); if !errors.is_empty() { return Err(sorted(errors)); } for (id, group) in groups.iter().enumerate() { let context = Context { symbols: &symbols, types: &types, functions: &functions, models: &models, owner: functions[id].owner, }; match clauses::signature(group, &context) { Ok((parameters, outputs)) => { functions[id].parameters = parameters; functions[id].outputs = outputs; } Err(error) => errors.push(error), } } if !errors.is_empty() { return Err(sorted(errors)); } let mut adapters = ports::register( &packages, &groups, &symbols, &types, &mut functions, &mut errors, ); ports::settings( &packages, &types, &mut functions, &mut adapters, &mut errors, ); let tests = test_cases(&packages, &functions, &mut errors); let api::Entries { routes: mut api_routes, commands: api_commands, rest: mut api_rest, } = api::register( &packages, &symbols, &functions, &types, &models, &model_rest_sources, &mut errors, ); upload::validate_signatures(&functions, &api_routes, &mut errors); let jobs = jobs::register(&packages, &functions, &types, &mut errors); let auth::Registration { hooks: auth, permissions, } = auth::register( settings, &packages, &functions, &types, &mut api_routes, &mut api_rest, &mut errors, ); validate_public_boundaries(&functions, &types, &mut errors); validate_sensitive_boundaries(&packages, &functions, &types, &mut errors); // Markdown documents the unified function interface, after clause patterns resolve to types. for (owner, package) in packages.iter().enumerate() { let source = sources.get(package.span.source); if source.is_markdown() { errors.extend(crate::markdown::validate_contract( source, package, owner, &types, &functions, )); } } if !errors.is_empty() { return Err(sorted(errors)); } let mut warnings = Vec::new(); for (id, group) in groups.iter().enumerate() { if functions[id].port.is_some() { continue; } let context = Context { symbols: &symbols, types: &types, functions: &functions, models: &models, owner: functions[id].owner, }; let result = (|| { let patterns = clauses::check(group, &functions[id].parameters, &context)?; group .iter() .zip(patterns) .map(|(source, domains)| { body::check(source, domains, &functions[id], &context, &mut warnings) }) .collect::>>() })(); match result { Ok(clauses) => functions[id].clauses = clauses, Err(error) => errors.push(error), } } if errors.is_empty() { cycles::check(&functions, &types, &symbols, &mut errors); } if errors.is_empty() { let mut program = Program { types, functions, warnings: sorted(warnings), effects: Vec::new(), recoveries: Vec::new(), suspension_handlers: Vec::new(), failures: Vec::new(), failure_handlers: Vec::new(), database_effects: Vec::new(), models, jobs, api_routes, api_commands, api_rest, permissions, tenant_components: Vec::new(), tests, adapters, auth, packages: packages .iter() .map(|package| crate::hir::PackageContract { bundled: SourceMap::is_standard(package.span.source), domain: package .layout .domain() .map(|(component, domain)| (component.to_owned(), domain.to_owned())), role: package.layout.role(), }) .collect(), }; // Test bodies are checked under their own fake bindings, never production effects. let test_owners: std::collections::BTreeSet<_> = program .tests .iter() .map(|test| program.functions[test.function].owner) .collect(); let test_bodies: Vec<_> = program .functions .iter_mut() .enumerate() .filter(|(_, function)| test_owners.contains(&function.owner)) .map(|(id, function)| (id, std::mem::take(&mut function.clauses))) .collect(); crate::contracts::check(&mut program, &mut errors).map_err(|error| vec![error])?; components::register(&mut program, &mut errors); api::validate_rest_bindings(&program, &mut errors); api::validate_get_effects(&program, &mut errors); api::validate_write_transactions(&program, &mut errors); api::validate_context_roots(&program, &mut errors); api::validate_context_detachment(&program, &mut errors); auth::validate(&program, settings.is_some(), &mut errors); ports::validate_failures(&program, &mut errors); validate_exported_failures(&program, &mut errors); for (id, clauses) in test_bodies { program.functions[id].clauses = clauses; } finalize_tests(&mut program, &mut errors); program.warnings = sorted(program.warnings); if errors.is_empty() { Ok(program) } else { Err(sorted(errors)) } } else { Err(sorted(errors)) } } fn declaration_name(package: &Package, physical: &str, name: &str) -> String { match package.layout.role() { Some(SourceRole::App) => format!("{}.{}", package.layout.app_prefix().unwrap(), name), _ => format!("{physical}.{name}"), } } fn declaration_public(package: &Package, bundled: bool, explicit: bool) -> bool { if bundled || matches!(&package.layout, SourceLayout::Main | SourceLayout::Loose) { explicit } else { package.layout.role() == Some(SourceRole::App) } } fn test_cases( packages: &[Package], functions: &[Function], errors: &mut Vec, ) -> Vec { let mut tests = Vec::new(); for (owner, package) in packages.iter().enumerate() { let SourceLayout::Test { component, domain, topic, } = &package.layout else { continue; }; let entry_name = format!("{}.{}", path_name(&package.name), topic); let matching = functions .iter() .enumerate() .filter(|(_, function)| function.owner == owner && function.name == entry_name) .collect::>(); let [(function, entry)] = matching.as_slice() else { errors.push(Diagnostic::error( "C005", if matching.is_empty() { format!("test file must declare the ordinary '{topic}() ()' entry") } else { format!("test entry '{topic}' cannot be overloaded") }, package.span, )); continue; }; if entry.kind != crate::syntax::FunctionKind::Ordinary || !entry.parameters.is_empty() || !entry.outputs.is_empty() { errors.push(Diagnostic::error( "C005", format!( "test entry '{topic}' must be an ordinary zero-input, zero-output function" ), entry.span, )); continue; } tests.push(TestCase { name: format!("{component}/{domain}/{topic}"), function: *function, span: entry.span, database_connections: Vec::new(), port_bindings: BTreeMap::new(), }); } tests.sort_by(|left, right| left.name.cmp(&right.name)); tests } fn finalize_tests(program: &mut Program, errors: &mut Vec) { ports::bind_tests(program); if !errors.is_empty() { return; } for index in 0..program.tests.len() { let case = match ports::test_program(program, index) { Ok(case) => case, Err(case_errors) => { errors.extend(case_errors); continue; } }; let function = program.tests[index].function; let span = program.tests[index].span; let root = crate::specialize::Specialization { function, handlers: Vec::new(), }; let instances = match crate::specialize::reachable(&case.functions, [root.clone()]) { Ok(instances) => instances, Err(cycle) => { errors.push(Diagnostic::error( "C007", "recursive specialized test calls are not allowed", cycle, )); continue; } }; if instances.iter().any(|instance| { matches!( program.functions[instance.function].name.as_str(), "dever.api.serve" | "dever.job.serve" ) }) { errors.push(Diagnostic::error( "C006", "tests cannot start the application HTTP API or Job worker", span, )); } if crate::specialize::has_effect(&case, &root, "identity") { errors.push(Diagnostic::error( "C006", "HTTP identity and site capabilities are not available from tests", span, )); } let models = crate::specialize::database_effects(&case, &root); if models.is_empty() { continue; } let mut connections = std::collections::BTreeSet::from(["default".to_owned()]); for model in &program.models { if let crate::model::ConnectionSelector::Explicit(name) = &model.connection { connections.insert(name.clone()); } } for job in &program.jobs { if let crate::model::ConnectionSelector::Explicit(name) = &job.connection { connections.insert(name.clone()); } } program.tests[index].database_connections = connections.into_iter().collect(); } } fn check_package(package: &Package, strict_layout: bool, errors: &mut Vec) { let name = path_name(&package.name); if !SourceMap::is_standard(package.span.source) && (package.name[0].text == "dever" || matches!(name.as_str(), "text" | "math" | "decimal" | "float" | "int")) { errors.push(Diagnostic::error( "C003", "reserved standard package namespace", package.name[0].span, )); } match package.layout.role() { None if strict_layout && matches!(&package.layout, SourceLayout::Main) => { for declaration in &package.declarations { if !matches!(declaration, Declaration::Function(function) if function.name.text == "main" && function.kind == crate::syntax::FunctionKind::Ordinary) { errors.push(Diagnostic::error( "C005", "main.dever may contain only the ordinary main() boot function", crate::markdown::declaration_span(declaration), )); } } } Some(SourceRole::Model) => { for declaration in &package.declarations { if let Declaration::Function(function) = declaration { errors.push(Diagnostic::error( "C005", "model source declares storage only; move business behavior to app or domain", function.span, )); } } } Some(SourceRole::Port) => { for declaration in &package.declarations { if !matches!(declaration, Declaration::Type(_) | Declaration::Function(_)) { errors.push(Diagnostic::error( "C005", "Port declares only types and bodyless contracts", crate::markdown::declaration_span(declaration), )); } } } Some(SourceRole::Adapter) => { let externals = package .declarations .iter() .filter(|declaration| matches!(declaration, Declaration::External(_))) .count(); if externals > 1 { errors.push(Diagnostic::error( "C002", "Adapter source may declare only one external implementation", package.span, )); } if externals == 1 { for declaration in &package.declarations { let allowed = matches!(declaration, Declaration::External(_)) || matches!(declaration, Declaration::Type(ty) if ty.setting); if !allowed { errors.push(Diagnostic::error( "C005", "external Adapter may contain only its external declaration and optional setting", crate::markdown::declaration_span(declaration), )); } } } } None if matches!(package.layout, SourceLayout::Test { .. }) => { for declaration in &package.declarations { if let Declaration::Function(function) = declaration && matches!( function.name.text.as_str(), "assert" | "assert_eq" | "secret" ) { errors.push(Diagnostic::error( "C002", "assert, assert_eq and secret are reserved test primitives", function.name.span, )); } } } _ => {} } } fn validate_public_boundaries( functions: &[Function], types: &[Definition], errors: &mut Vec, ) { let mut check_type = |ty: &crate::types::Type, name: &str, span| { symbols::referenced_types(ty, &mut |id| { if !types[id].public { errors.push(Diagnostic::error( "C006", format!( "exported declaration '{name}' cannot expose private type '{}'", types[id].name, ), span, )); } }); }; for definition in types.iter().filter(|definition| definition.public) { for field in symbols::shape_fields(&definition.shape) .into_iter() .filter(|field| !field.private) { check_type(&field.ty, &definition.name, definition.span); } } for function in functions.iter().filter(|function| function.public) { let mut check_type = |ty| check_type(ty, &function.name, function.span); for parameter in &function.parameters { match parameter { crate::types::Parameter::Value(ty) => check_type(ty), crate::types::Parameter::Handler(signature) => { for ty in &signature.parameters { check_type(ty); } for field in &signature.outputs { check_type(&field.ty); } } } } for output in &function.outputs { check_type(&output.ty); } } } fn validate_exported_failures(program: &Program, errors: &mut Vec) { for (id, function) in program.functions.iter().enumerate() { if program.packages[function.owner].role != Some(SourceRole::App) { continue; } let private_types = program.failures[id] .iter() .map(|failure| failure.ty) .filter(|ty| !program.types[*ty].public) .collect::>(); for ty in private_types { errors.push(Diagnostic::error( "C006", format!( "exported function '{}' cannot expose private error type '{}'", function.name, program.types[ty].name, ), function.span, )); } } } fn validate_sensitive_boundaries( packages: &[crate::syntax::Package], functions: &[Function], types: &[Definition], errors: &mut Vec, ) { for definition in types { if let crate::types::Shape::Choice(variants) = &definition.shape { for variant in variants.iter().filter(|variant| variant.error) { if variant .fields .iter() .any(|field| !field.ty.observable(types)) { errors.push(Diagnostic::error( "C006", "error payload cannot contain Secret or private Model data", definition.span, )); } } } } for function in functions { let app = packages[function.owner].layout.role() == Some(SourceRole::App); let entry = function.name == "main.main" || (function.public && !crate::source::SourceMap::is_standard(function.span.source)); if !app && !entry { continue; } let sensitive_input = function.parameters.iter().any(|parameter| match parameter { crate::types::Parameter::Value(ty) => ty.contains_private_model(types), crate::types::Parameter::Handler(signature) => { signature .parameters .iter() .any(|ty| ty.contains_private_model(types)) || signature .outputs .iter() .any(|field| !field.ty.observable(types)) } }); if sensitive_input || function .outputs .iter() .any(|field| !field.ty.observable(types)) { errors.push(Diagnostic::error( "C006", "App and entry boundaries cannot expose private Model data or output Secret; return an explicit View", function.span, )); } } } fn path_name(path: &[crate::syntax::Name]) -> String { path.iter() .map(|name| name.text.as_str()) .collect::>() .join(".") } fn sorted(mut errors: Vec) -> Vec { errors.sort_by_key(|error| { ( error.primary.source, error.primary.start, error.primary.end, error.code, ) }); errors }