472 lines
18 KiB
Rust
472 lines
18 KiB
Rust
//! Main architectural checker — walks the AST and applies all rules.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use el_parser::{Expr, Program, Stmt, TypeExpr};
|
|
|
|
use crate::error::{ArchDiagnostic, Severity};
|
|
use crate::rule::{ArchRule, CallInfo, FnContext};
|
|
use crate::rules::{
|
|
graph::{DuplicateActivateType, N1Detection},
|
|
security::{AuthnWithoutAuthz, PublicFnWithActivate, SealedInLoop},
|
|
swarm::{SwarmAgentIsolation, SwarmAgentNoSharedState, SwarmAgentNoSpawn},
|
|
vbd::{AccessorMustNotCallManager, ExperienceMustNotCallExperience, ExperienceShouldReturnResult},
|
|
};
|
|
|
|
/// The main architectural checker. Instantiate once, call `check` per program.
|
|
pub struct ArchChecker {
|
|
rules: Vec<Box<dyn ArchRule>>,
|
|
}
|
|
|
|
impl ArchChecker {
|
|
/// Create an `ArchChecker` with all built-in rules registered.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
rules: vec![
|
|
Box::new(AccessorMustNotCallManager),
|
|
Box::new(ExperienceMustNotCallExperience),
|
|
Box::new(ExperienceShouldReturnResult),
|
|
Box::new(PublicFnWithActivate),
|
|
Box::new(SealedInLoop),
|
|
Box::new(AuthnWithoutAuthz),
|
|
Box::new(N1Detection),
|
|
Box::new(DuplicateActivateType),
|
|
Box::new(SwarmAgentIsolation),
|
|
Box::new(SwarmAgentNoSpawn),
|
|
Box::new(SwarmAgentNoSharedState),
|
|
],
|
|
}
|
|
}
|
|
|
|
/// Returns a reference to the registered rules (useful for introspection in tests).
|
|
pub fn rules(&self) -> &[Box<dyn ArchRule>] {
|
|
&self.rules
|
|
}
|
|
|
|
/// Run all rules against a parsed program and collect all diagnostics.
|
|
pub fn check(&self, program: &Program) -> Vec<ArchDiagnostic> {
|
|
// Step 1: build global fn_name → decorator names map (including impl methods).
|
|
let all_fn_annotations = collect_fn_annotations(&program.stmts);
|
|
|
|
// Step 2: gather all top-level + impl FnDef statements.
|
|
let fn_defs = collect_fn_defs(&program.stmts);
|
|
|
|
// Step 3: for each function, build FnContext and run every rule.
|
|
let mut diagnostics = Vec::new();
|
|
for (fn_name, decorators, return_type, body) in &fn_defs {
|
|
let annotations: Vec<String> =
|
|
decorators.iter().map(|d| d.name.clone()).collect();
|
|
let body_calls = extract_calls(body, false);
|
|
let activate_types = extract_activate_types(body, false);
|
|
let has_sealed_in_loop = has_sealed_in_loop_body(body, false);
|
|
let return_type_name = type_expr_name(return_type);
|
|
|
|
let ctx = FnContext {
|
|
fn_name: fn_name.as_str(),
|
|
annotations: &annotations,
|
|
body_calls: &body_calls,
|
|
all_fn_annotations: &all_fn_annotations,
|
|
activate_types: &activate_types,
|
|
has_sealed_in_loop,
|
|
return_type_name: &return_type_name,
|
|
};
|
|
|
|
for rule in &self.rules {
|
|
diagnostics.extend(rule.check(&ctx));
|
|
}
|
|
}
|
|
|
|
diagnostics
|
|
}
|
|
|
|
/// Returns true if any of the given diagnostics are errors.
|
|
pub fn has_errors(diagnostics: &[ArchDiagnostic]) -> bool {
|
|
diagnostics.iter().any(|d| d.severity == Severity::Error)
|
|
}
|
|
}
|
|
|
|
impl Default for ArchChecker {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
// ── AST traversal helpers ─────────────────────────────────────────────────────
|
|
|
|
/// A collected function definition: (name, decorators, return_type, body).
|
|
type FnDef<'a> = (
|
|
&'a String,
|
|
&'a Vec<el_parser::Decorator>,
|
|
&'a TypeExpr,
|
|
&'a Vec<Stmt>,
|
|
);
|
|
|
|
/// Collect all FnDef statements from top-level and impl blocks.
|
|
fn collect_fn_defs<'a>(stmts: &'a [Stmt]) -> Vec<FnDef<'a>> {
|
|
let mut out = Vec::new();
|
|
for stmt in stmts {
|
|
match stmt {
|
|
Stmt::FnDef { name, decorators, return_type, body, .. } => {
|
|
out.push((name, decorators, return_type, body));
|
|
}
|
|
Stmt::ImplDef { methods, .. } => {
|
|
for m in methods {
|
|
if let Stmt::FnDef { name, decorators, return_type, body, .. } = m {
|
|
out.push((name, decorators, return_type, body));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Build a map from function name → list of decorator names, for all functions in the program.
|
|
fn collect_fn_annotations(stmts: &[Stmt]) -> HashMap<String, Vec<String>> {
|
|
let mut map = HashMap::new();
|
|
for stmt in stmts {
|
|
match stmt {
|
|
Stmt::FnDef { name, decorators, .. } => {
|
|
let anns: Vec<String> = decorators.iter().map(|d| d.name.clone()).collect();
|
|
map.insert(name.clone(), anns);
|
|
}
|
|
Stmt::ImplDef { methods, .. } => {
|
|
for m in methods {
|
|
if let Stmt::FnDef { name, decorators, .. } = m {
|
|
let anns: Vec<String> = decorators.iter().map(|d| d.name.clone()).collect();
|
|
map.insert(name.clone(), anns);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
map
|
|
}
|
|
|
|
/// Extract all function calls from a statement list.
|
|
/// `in_loop` tracks whether we are currently inside a for/while loop body.
|
|
fn extract_calls(stmts: &[Stmt], in_loop: bool) -> Vec<CallInfo> {
|
|
let mut calls = Vec::new();
|
|
for stmt in stmts {
|
|
extract_calls_from_stmt(stmt, in_loop, &mut calls);
|
|
}
|
|
calls
|
|
}
|
|
|
|
fn extract_calls_from_stmt(stmt: &Stmt, in_loop: bool, out: &mut Vec<CallInfo>) {
|
|
match stmt {
|
|
Stmt::Let { value, .. } => extract_calls_from_expr(value, in_loop, out),
|
|
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
|
|
extract_calls_from_expr(expr, in_loop, out);
|
|
}
|
|
Stmt::FnDef { body, .. } => {
|
|
// Nested function defs: walk but don't count as callee of the outer fn.
|
|
for s in body {
|
|
extract_calls_from_stmt(s, false, out);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn extract_calls_from_expr(expr: &Expr, in_loop: bool, out: &mut Vec<CallInfo>) {
|
|
match expr {
|
|
Expr::Call { func, args } => {
|
|
// Extract callee name
|
|
let callee = expr_as_call_name(func);
|
|
if let Some(name) = callee {
|
|
out.push(CallInfo { callee: name, is_in_loop: in_loop });
|
|
}
|
|
// Recurse into func expression and args
|
|
extract_calls_from_expr(func, in_loop, out);
|
|
for a in args {
|
|
extract_calls_from_expr(a, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Activate { type_name, .. } => {
|
|
// Encode activate as a synthetic call so GRAPH-001 can detect in-loop activates.
|
|
out.push(CallInfo {
|
|
callee: format!("__activate__{type_name}"),
|
|
is_in_loop: in_loop,
|
|
});
|
|
}
|
|
Expr::BinOp { left, right, .. } => {
|
|
extract_calls_from_expr(left, in_loop, out);
|
|
extract_calls_from_expr(right, in_loop, out);
|
|
}
|
|
Expr::UnaryNot(inner) | Expr::Try(inner) => {
|
|
extract_calls_from_expr(inner, in_loop, out);
|
|
}
|
|
Expr::Block(stmts) => {
|
|
for s in stmts {
|
|
extract_calls_from_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Sealed(stmts) => {
|
|
// Sealed blocks are scanned but tracked separately for sealed-in-loop.
|
|
for s in stmts {
|
|
extract_calls_from_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::If { cond, then, else_ } => {
|
|
extract_calls_from_expr(cond, in_loop, out);
|
|
extract_calls_from_expr(then, in_loop, out);
|
|
if let Some(e) = else_ {
|
|
extract_calls_from_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Match { subject, arms } => {
|
|
extract_calls_from_expr(subject, in_loop, out);
|
|
for arm in arms {
|
|
extract_calls_from_expr(&arm.body, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Field { object, .. } => extract_calls_from_expr(object, in_loop, out),
|
|
Expr::Array(elems) => {
|
|
for e in elems {
|
|
extract_calls_from_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Index { object, index } => {
|
|
extract_calls_from_expr(object, in_loop, out);
|
|
extract_calls_from_expr(index, in_loop, out);
|
|
}
|
|
Expr::Closure { body, .. } => {
|
|
extract_calls_from_expr(body, in_loop, out);
|
|
}
|
|
Expr::MapLiteral(pairs) => {
|
|
for (k, v) in pairs {
|
|
extract_calls_from_expr(k, in_loop, out);
|
|
extract_calls_from_expr(v, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => {}
|
|
Expr::StructLit { fields, .. } => {
|
|
for (_, e) in fields {
|
|
extract_calls_from_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::With { base, updates } => {
|
|
extract_calls_from_expr(base, in_loop, out);
|
|
for (_, e) in updates {
|
|
extract_calls_from_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Reason { .. } => {}
|
|
Expr::Parallel { entries } => {
|
|
for (_, e) in entries {
|
|
extract_calls_from_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Trace { body, .. } => {
|
|
for s in body {
|
|
extract_calls_from_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::UnaryBitNot(inner) => extract_calls_from_expr(inner, in_loop, out),
|
|
}
|
|
}
|
|
|
|
/// Try to extract a simple callee name from a Call's `func` expression.
|
|
fn expr_as_call_name(expr: &Expr) -> Option<String> {
|
|
match expr {
|
|
Expr::Ident(name) => Some(name.clone()),
|
|
Expr::Field { field, .. } => Some(field.clone()),
|
|
Expr::Path { segments } => segments.last().cloned(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Collect all `activate TypeName` type names from a statement list.
|
|
/// `in_loop` indicates whether we're currently inside a loop.
|
|
fn extract_activate_types(stmts: &[Stmt], in_loop: bool) -> Vec<String> {
|
|
let mut types = Vec::new();
|
|
for stmt in stmts {
|
|
extract_activate_types_stmt(stmt, in_loop, &mut types);
|
|
}
|
|
types
|
|
}
|
|
|
|
fn extract_activate_types_stmt(stmt: &Stmt, in_loop: bool, out: &mut Vec<String>) {
|
|
match stmt {
|
|
Stmt::Let { value, .. } => extract_activate_types_expr(value, in_loop, out),
|
|
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
|
|
extract_activate_types_expr(expr, in_loop, out);
|
|
}
|
|
Stmt::FnDef { body, .. } => {
|
|
for s in body {
|
|
extract_activate_types_stmt(s, false, out);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn extract_activate_types_expr(expr: &Expr, in_loop: bool, out: &mut Vec<String>) {
|
|
match expr {
|
|
Expr::Activate { type_name, .. } => {
|
|
out.push(type_name.clone());
|
|
}
|
|
Expr::Call { func, args } => {
|
|
extract_activate_types_expr(func, in_loop, out);
|
|
for a in args {
|
|
extract_activate_types_expr(a, in_loop, out);
|
|
}
|
|
}
|
|
Expr::BinOp { left, right, .. } => {
|
|
extract_activate_types_expr(left, in_loop, out);
|
|
extract_activate_types_expr(right, in_loop, out);
|
|
}
|
|
Expr::UnaryNot(inner) | Expr::Try(inner) => {
|
|
extract_activate_types_expr(inner, in_loop, out);
|
|
}
|
|
Expr::Block(stmts) => {
|
|
for s in stmts {
|
|
extract_activate_types_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Sealed(stmts) => {
|
|
for s in stmts {
|
|
extract_activate_types_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::If { cond, then, else_ } => {
|
|
extract_activate_types_expr(cond, in_loop, out);
|
|
extract_activate_types_expr(then, in_loop, out);
|
|
if let Some(e) = else_ {
|
|
extract_activate_types_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Match { subject, arms } => {
|
|
extract_activate_types_expr(subject, in_loop, out);
|
|
for arm in arms {
|
|
extract_activate_types_expr(&arm.body, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Field { object, .. } => extract_activate_types_expr(object, in_loop, out),
|
|
Expr::Array(elems) => {
|
|
for e in elems {
|
|
extract_activate_types_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Index { object, index } => {
|
|
extract_activate_types_expr(object, in_loop, out);
|
|
extract_activate_types_expr(index, in_loop, out);
|
|
}
|
|
Expr::Closure { body, .. } => {
|
|
extract_activate_types_expr(body, in_loop, out);
|
|
}
|
|
Expr::MapLiteral(pairs) => {
|
|
for (k, v) in pairs {
|
|
extract_activate_types_expr(k, in_loop, out);
|
|
extract_activate_types_expr(v, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => {}
|
|
Expr::StructLit { fields, .. } => {
|
|
for (_, e) in fields {
|
|
extract_activate_types_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::With { base, updates } => {
|
|
extract_activate_types_expr(base, in_loop, out);
|
|
for (_, e) in updates {
|
|
extract_activate_types_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Reason { .. } => {}
|
|
Expr::Parallel { entries } => {
|
|
for (_, e) in entries {
|
|
extract_activate_types_expr(e, in_loop, out);
|
|
}
|
|
}
|
|
Expr::Trace { body, .. } => {
|
|
for s in body {
|
|
extract_activate_types_stmt(s, in_loop, out);
|
|
}
|
|
}
|
|
Expr::UnaryBitNot(inner) => extract_activate_types_expr(inner, in_loop, out),
|
|
}
|
|
}
|
|
|
|
/// Returns true if any `sealed { }` block appears inside a loop in the given body.
|
|
fn has_sealed_in_loop_body(stmts: &[Stmt], in_loop: bool) -> bool {
|
|
stmts.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop))
|
|
}
|
|
|
|
fn has_sealed_in_loop_stmt(stmt: &Stmt, in_loop: bool) -> bool {
|
|
match stmt {
|
|
Stmt::Let { value, .. } => has_sealed_in_loop_expr(value, in_loop),
|
|
Stmt::Return(expr, _) | Stmt::Expr(expr, _) | Stmt::Assert(expr, _) => {
|
|
has_sealed_in_loop_expr(expr, in_loop)
|
|
}
|
|
Stmt::FnDef { body, .. } => {
|
|
// Inner function defs reset loop context.
|
|
body.iter().any(|s| has_sealed_in_loop_stmt(s, false))
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn has_sealed_in_loop_expr(expr: &Expr, in_loop: bool) -> bool {
|
|
match expr {
|
|
Expr::Sealed(_) => in_loop,
|
|
Expr::Call { func, args } => {
|
|
has_sealed_in_loop_expr(func, in_loop)
|
|
|| args.iter().any(|a| has_sealed_in_loop_expr(a, in_loop))
|
|
}
|
|
Expr::BinOp { left, right, .. } => {
|
|
has_sealed_in_loop_expr(left, in_loop) || has_sealed_in_loop_expr(right, in_loop)
|
|
}
|
|
Expr::UnaryNot(inner) | Expr::Try(inner) => has_sealed_in_loop_expr(inner, in_loop),
|
|
Expr::Block(stmts) => stmts.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)),
|
|
Expr::If { cond, then, else_ } => {
|
|
has_sealed_in_loop_expr(cond, in_loop)
|
|
|| has_sealed_in_loop_expr(then, in_loop)
|
|
|| else_.as_deref().is_some_and(|e| has_sealed_in_loop_expr(e, in_loop))
|
|
}
|
|
Expr::Match { subject, arms } => {
|
|
has_sealed_in_loop_expr(subject, in_loop)
|
|
|| arms.iter().any(|a| has_sealed_in_loop_expr(&a.body, in_loop))
|
|
}
|
|
Expr::Field { object, .. } => has_sealed_in_loop_expr(object, in_loop),
|
|
Expr::Array(elems) => elems.iter().any(|e| has_sealed_in_loop_expr(e, in_loop)),
|
|
Expr::Index { object, index } => {
|
|
has_sealed_in_loop_expr(object, in_loop) || has_sealed_in_loop_expr(index, in_loop)
|
|
}
|
|
Expr::Closure { body, .. } => has_sealed_in_loop_expr(body, in_loop),
|
|
Expr::MapLiteral(pairs) => pairs
|
|
.iter()
|
|
.any(|(k, v)| has_sealed_in_loop_expr(k, in_loop) || has_sealed_in_loop_expr(v, in_loop)),
|
|
Expr::Activate { .. } | Expr::Literal(_) | Expr::Ident(_) | Expr::Path { .. } => false,
|
|
Expr::StructLit { fields, .. } => fields
|
|
.iter()
|
|
.any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
|
|
Expr::With { base, updates } => {
|
|
has_sealed_in_loop_expr(base, in_loop)
|
|
|| updates.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop))
|
|
}
|
|
Expr::Reason { .. } => false,
|
|
Expr::Parallel { entries } => entries.iter().any(|(_, e)| has_sealed_in_loop_expr(e, in_loop)),
|
|
Expr::Trace { body, .. } => body.iter().any(|s| has_sealed_in_loop_stmt(s, in_loop)),
|
|
Expr::UnaryBitNot(inner) => has_sealed_in_loop_expr(inner, in_loop),
|
|
}
|
|
}
|
|
|
|
/// Convert a `TypeExpr` to a display string for the return-type name check.
|
|
fn type_expr_name(te: &TypeExpr) -> String {
|
|
match te {
|
|
TypeExpr::Named(n) => n.clone(),
|
|
TypeExpr::Result { .. } => "Result".to_string(),
|
|
TypeExpr::Array(inner) => format!("[{}]", type_expr_name(inner)),
|
|
TypeExpr::Optional(inner) => format!("{}?", type_expr_name(inner)),
|
|
TypeExpr::Map { key, value } => {
|
|
format!("Map<{}, {}>", type_expr_name(key), type_expr_name(value))
|
|
}
|
|
TypeExpr::Fn { .. } => "fn".to_string(),
|
|
TypeExpr::TypeParam(n) => n.clone(),
|
|
}
|
|
}
|