feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,850 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user