horkos 0.2.0

Cloud infrastructure language where insecure code won't compile
Documentation
//! Standard library for Horkos.
//!
//! This module provides type resolution for resource functions.
//! Resource definitions live in `resources/` module.
//!
//! Key principle: All resources are secure by default. To weaken
//! security, you must use `unsafe` with a justification.

use crate::ast::Literal;
use crate::resources::{registry, ParamValue, ResourceDefinition};
use crate::types::{ResolvedType, TypedArg, TypedExprKind};

/// Resolve a member access on a module (e.g., S3.createBucket).
pub fn resolve_member(module: &str, member: &str) -> ResolvedType {
    let registry = registry::get();

    // First check if this is a known resource function
    if let Some(def) = registry.get(module, member) {
        return build_function_type(def);
    }

    // Fall back to legacy resolution for non-resource functions
    match module {
        "IAM" => resolve_iam_member(member),
        "EC2" => resolve_ec2_member(member),
        "Secret" => resolve_secret_member(member),
        _ => ResolvedType::Unknown,
    }
}

/// Build a function type from a resource definition.
fn build_function_type(def: &ResourceDefinition) -> ResolvedType {
    let params: Vec<(String, ResolvedType)> = def
        .all_params()
        .iter()
        .map(|p| (p.name.to_string(), p.param_type.clone()))
        .collect();

    ResolvedType::Function {
        params,
        returns: Box::new(def.returns.clone()),
    }
}

/// Resolve the return type of a function call.
pub fn resolve_return_type(
    module_type: &ResolvedType,
    function: &str,
    _args: &[TypedArg],
) -> ResolvedType {
    match module_type {
        ResolvedType::Module(name) => {
            let registry = registry::get();
            if let Some(def) = registry.get(name, function) {
                return def.returns.clone();
            }

            // Fall back to legacy resolution
            match name.as_str() {
                "IAM" => resolve_iam_return(function),
                "EC2" => resolve_ec2_return(function),
                "Secret" => resolve_secret_return(function),
                _ => ResolvedType::Unknown,
            }
        }
        _ => ResolvedType::Unknown,
    }
}

/// Get the full function signature for a module member.
pub fn get_function_signature(
    module: &str,
    function: &str,
) -> Option<(Vec<(String, ResolvedType)>, ResolvedType)> {
    let member_type = resolve_member(module, function);
    match member_type {
        ResolvedType::Function { params, returns } => Some((params, *returns)),
        _ => None,
    }
}

/// Get resource definition for a module function.
pub fn get_resource_definition(
    module: &str,
    function: &str,
) -> Option<&'static ResourceDefinition> {
    registry::get().get(module, function)
}

/// Info about a security-weakening parameter.
#[derive(Debug, Clone)]
pub struct SecurityParamInfo {
    pub param_name: &'static str,
    pub span: crate::ast::Span,
}

/// Check if a function call has security-weakening parameters.
/// Returns info about the first security param that requires unsafe.
pub fn check_security_params(
    module: &str,
    function: &str,
    args: &[TypedArg],
) -> Option<SecurityParamInfo> {
    let registry = registry::get();
    let def = registry.get(module, function)?;

    for arg in args {
        if let Some(ref name) = arg.name {
            // Convert typed expression to ParamValue
            if let Some(value) = typed_expr_to_param_value(&arg.value.kind) {
                if let Some(param_name) = def.check_security(name, &value) {
                    return Some(SecurityParamInfo {
                        param_name,
                        span: arg.value.span,
                    });
                }
            }
        }
    }
    None
}

/// Info about a preferred parameter that was overridden.
#[derive(Debug, Clone)]
pub struct PreferredParamInfo {
    pub param_name: &'static str,
    pub recommended: String,
    pub span: crate::ast::Span, // Span of the specific argument
}

/// Check if a function call overrides preferred parameters.
/// Returns info about each preferred param that differs from its recommended default.
pub fn check_preferred_params(
    module: &str,
    function: &str,
    args: &[TypedArg],
) -> Vec<PreferredParamInfo> {
    let registry = registry::get();
    let def = match registry.get(module, function) {
        Some(d) => d,
        None => return Vec::new(),
    };

    let mut overrides = Vec::new();

    for arg in args {
        if let Some(ref name) = arg.name {
            if let Some(value) = typed_expr_to_param_value(&arg.value.kind) {
                if let Some((param_name, recommended)) = def.check_preferred(name, &value) {
                    let recommended_str = match recommended {
                        ParamValue::Bool(b) => b.to_string(),
                        ParamValue::String(s) => s.clone(),
                        ParamValue::Number(n) => n.to_string(),
                        ParamValue::None => "none".to_string(),
                    };
                    overrides.push(PreferredParamInfo {
                        param_name,
                        recommended: recommended_str,
                        span: arg.value.span, // Capture the argument's span
                    });
                }
            }
        }
    }

    overrides
}

/// Info about an unknown parameter.
#[derive(Debug, Clone)]
pub struct UnknownParamInfo {
    pub param_name: String,
    pub span: crate::ast::Span,
    pub suggestion: Option<&'static str>,
    pub known_params: Vec<&'static str>,
}

/// Check if a function call has unknown parameters.
/// Returns info about each unknown parameter found.
pub fn check_unknown_params(
    module: &str,
    function: &str,
    args: &[TypedArg],
) -> Vec<UnknownParamInfo> {
    let registry = registry::get();
    let def = match registry.get(module, function) {
        Some(d) => d,
        None => return Vec::new(),
    };

    let mut unknown = Vec::new();

    // Collect all known parameter names
    let known_params: Vec<&'static str> = def
        .required_params
        .iter()
        .map(|p| p.name)
        .chain(def.optional_params.iter().map(|p| p.name))
        .chain(def.security_params.iter().map(|p| p.param.name))
        .chain(def.preferred_params.iter().map(|p| p.param.name))
        .collect();

    for arg in args {
        if let Some(ref name) = arg.name {
            // Check if this parameter name is known
            if !known_params.contains(&name.as_str()) {
                // Try to find a similar parameter name (did you mean?)
                let suggestion = find_similar_param(name, &known_params);
                unknown.push(UnknownParamInfo {
                    param_name: name.clone(),
                    span: arg.value.span,
                    suggestion,
                    known_params: known_params.clone(),
                });
            }
        }
    }

    unknown
}

/// Find a similar parameter name for "did you mean" suggestions.
fn find_similar_param<'a>(name: &str, known: &[&'a str]) -> Option<&'a str> {
    let name_lower = name.to_lowercase();

    for &param in known {
        let param_lower = param.to_lowercase();

        // Check for common typos:
        // 1. Off by one character (edit distance 1-2)
        // 2. Swapped characters
        // 3. Missing/extra character

        let distance = levenshtein_distance(&name_lower, &param_lower);
        if distance <= 2 && distance < name.len() / 2 {
            return Some(param);
        }
    }
    None
}

/// Simple Levenshtein distance for typo detection.
fn levenshtein_distance(a: &str, b: &str) -> usize {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let m = a_chars.len();
    let n = b_chars.len();

    if m == 0 {
        return n;
    }
    if n == 0 {
        return m;
    }

    let mut prev: Vec<usize> = (0..=n).collect();
    let mut curr = vec![0; n + 1];

    for i in 1..=m {
        curr[0] = i;
        for j in 1..=n {
            let cost = if a_chars[i - 1] == b_chars[j - 1] {
                0
            } else {
                1
            };
            curr[j] = (prev[j] + 1).min((curr[j - 1] + 1).min(prev[j - 1] + cost));
        }
        std::mem::swap(&mut prev, &mut curr);
    }

    prev[n]
}

/// Convert a typed expression to a ParamValue for security checking.
fn typed_expr_to_param_value(kind: &TypedExprKind) -> Option<ParamValue> {
    match kind {
        TypedExprKind::Literal(Literal::Bool(b)) => Some(ParamValue::Bool(*b)),
        TypedExprKind::Literal(Literal::String(s)) => Some(ParamValue::String(s.clone())),
        TypedExprKind::Literal(Literal::Number(n)) => Some(ParamValue::Number(*n)),
        _ => None,
    }
}

// === Legacy IAM Module (to be migrated) ===

fn resolve_iam_member(member: &str) -> ResolvedType {
    match member {
        "createRole" => ResolvedType::Function {
            params: vec![
                ("name".to_string(), ResolvedType::String),
                ("assumeRolePolicy".to_string(), ResolvedType::String),
            ],
            returns: Box::new(ResolvedType::IamRole),
        },
        "createPolicy" => ResolvedType::Function {
            params: vec![
                ("name".to_string(), ResolvedType::String),
                ("document".to_string(), ResolvedType::String),
            ],
            returns: Box::new(ResolvedType::IamPolicy),
        },
        "attachPolicy" => ResolvedType::Function {
            params: vec![
                ("role".to_string(), ResolvedType::IamRole),
                ("policy".to_string(), ResolvedType::IamPolicy),
            ],
            returns: Box::new(ResolvedType::Void),
        },
        _ => ResolvedType::Unknown,
    }
}

fn resolve_iam_return(function: &str) -> ResolvedType {
    match function {
        "createRole" => ResolvedType::IamRole,
        "createPolicy" => ResolvedType::IamPolicy,
        "attachPolicy" => ResolvedType::Void,
        _ => ResolvedType::Unknown,
    }
}

// === Legacy EC2 Module (to be migrated) ===

fn resolve_ec2_member(member: &str) -> ResolvedType {
    match member {
        "attachInstanceProfile" => ResolvedType::Function {
            params: vec![
                ("instance".to_string(), ResolvedType::String),
                ("profile".to_string(), ResolvedType::IamRole),
            ],
            returns: Box::new(ResolvedType::Void),
        },
        _ => ResolvedType::Unknown,
    }
}

fn resolve_ec2_return(function: &str) -> ResolvedType {
    match function {
        "attachInstanceProfile" => ResolvedType::Void,
        _ => ResolvedType::Unknown,
    }
}

// === Legacy Secret Module (to be migrated) ===

fn resolve_secret_member(member: &str) -> ResolvedType {
    match member {
        "get" => ResolvedType::Function {
            params: vec![("path".to_string(), ResolvedType::String)],
            returns: Box::new(ResolvedType::String),
        },
        _ => ResolvedType::Unknown,
    }
}

fn resolve_secret_return(function: &str) -> ResolvedType {
    match function {
        "get" => ResolvedType::String,
        _ => ResolvedType::Unknown,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_s3_member_resolution() {
        let t = resolve_member("S3", "createBucket");
        assert!(matches!(t, ResolvedType::Function { .. }));
    }

    #[test]
    fn test_s3_return_type() {
        let module_type = ResolvedType::Module("S3".to_string());
        let t = resolve_return_type(&module_type, "createBucket", &[]);
        assert_eq!(t, ResolvedType::Bucket);
    }

    #[test]
    fn test_unknown_member() {
        let t = resolve_member("Unknown", "foo");
        assert_eq!(t, ResolvedType::Unknown);
    }

    #[test]
    fn test_resource_definition_lookup() {
        let def = get_resource_definition("S3", "createBucket");
        assert!(def.is_some());

        let def = def.unwrap();
        assert_eq!(def.required_params.len(), 1);
        assert_eq!(def.security_params.len(), 2); // encryption, publicAccess
        assert_eq!(def.preferred_params.len(), 2); // versioning, logging
    }
}