851 lines
29 KiB
Rust
851 lines
29 KiB
Rust
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<T> = Result<T, Diagnostic>;
|
|
|
|
struct Symbols {
|
|
functions: BTreeMap<(String, usize), usize>,
|
|
types: BTreeMap<String, usize>,
|
|
packages: Vec<PackageScope>,
|
|
strict_layout: bool,
|
|
}
|
|
|
|
struct PackageScope {
|
|
name: String,
|
|
layout: SourceLayout,
|
|
bundled: bool,
|
|
implemented_ports: std::collections::BTreeSet<String>,
|
|
contract_types: std::collections::BTreeSet<String>,
|
|
}
|
|
|
|
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<Program, Vec<Diagnostic>> {
|
|
cache::check(sources, check_uncached)
|
|
}
|
|
|
|
mod cache;
|
|
|
|
fn check_uncached(sources: &SourceMap) -> Result<Program, Vec<Diagnostic>> {
|
|
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<Program, Vec<Diagnostic>> {
|
|
check_with_bindings(sources, &settings.compilation_bindings())
|
|
}
|
|
|
|
pub fn check_with_bindings(
|
|
sources: &SourceMap,
|
|
bindings: &dever_runtime::config::CompilationBindings,
|
|
) -> Result<Program, Vec<Diagnostic>> {
|
|
check_configured(sources, Some(bindings))
|
|
}
|
|
|
|
fn check_configured(
|
|
sources: &SourceMap,
|
|
settings: Option<&dever_runtime::config::CompilationBindings>,
|
|
) -> Result<Program, Vec<Diagnostic>> {
|
|
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<Function> = Vec::new();
|
|
let mut groups: Vec<Vec<&FunctionClause>> = 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::<Checked<Vec<_>>>()
|
|
})();
|
|
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<Diagnostic>,
|
|
) -> Vec<TestCase> {
|
|
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::<Vec<_>>();
|
|
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<Diagnostic>) {
|
|
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<Diagnostic>) {
|
|
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<Diagnostic>,
|
|
) {
|
|
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<Diagnostic>) {
|
|
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::<std::collections::BTreeSet<_>>();
|
|
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<Diagnostic>,
|
|
) {
|
|
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::<Vec<_>>()
|
|
.join(".")
|
|
}
|
|
|
|
fn sorted(mut errors: Vec<Diagnostic>) -> Vec<Diagnostic> {
|
|
errors.sort_by_key(|error| {
|
|
(
|
|
error.primary.source,
|
|
error.primary.start,
|
|
error.primary.end,
|
|
error.code,
|
|
)
|
|
});
|
|
errors
|
|
}
|