aeri 0.2.4

Aeri is a Cardano smart contract language by Knott Dynamics, created by Trevor Knott, with its official compiler and CLI.
Documentation
use serde::Serialize;

use crate::{
    Result,
    ast::{BinaryOp, Block, Expr, ExprKind, Item, Module, Pattern, Statement, Validator},
    diagnostic::Span,
    parser::parse_module,
};

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
/// Contract-oriented lint warning emitted with source location metadata.
pub struct LintWarning {
    /// Source file where the warning was detected.
    pub file: String,
    /// 1-based source line number.
    pub line: usize,
    /// 1-based source column number.
    pub column: usize,
    /// Human-readable warning message.
    pub message: String,
}

impl LintWarning {
    fn new(file: &str, span: Span, message: impl Into<String>) -> Self {
        Self {
            file: file.to_string(),
            line: span.line,
            column: span.column,
            message: message.into(),
        }
    }
}

/// Runs contract-focused lint rules over a parsed module.
///
/// The module is first parsed and type checked so warnings are reported on a
/// validated AST.
pub fn lint_source(file: &str, source: &str) -> Result<Vec<LintWarning>> {
    crate::compile_source(file, source)?;
    let module = parse_module(file, source).map_err(|error| error.with_source(source))?;
    Ok(Linter::new(file, &module).lint())
}

struct Linter<'a> {
    file: &'a str,
    module: &'a Module,
    warnings: Vec<LintWarning>,
}

impl<'a> Linter<'a> {
    fn new(file: &'a str, module: &'a Module) -> Self {
        Self {
            file,
            module,
            warnings: Vec::new(),
        }
    }

    fn lint(mut self) -> Vec<LintWarning> {
        let validator_count = self
            .module
            .items
            .iter()
            .filter(|item| matches!(item, Item::Validator(_)))
            .count();

        if validator_count == 0 {
            self.warnings.push(LintWarning::new(
                self.file,
                Span::default(),
                "module declares no validators",
            ));
        }

        for item in &self.module.items {
            match item {
                Item::Validator(validator) => self.validator(validator),
                Item::Const(constant) => self.expr(&constant.value),
                Item::Function(function) => self.block(&function.body),
                Item::Test(test) => self.block(&test.body),
                Item::Type(_) => (),
            }
        }

        self.warnings
    }

    fn validator(&mut self, validator: &Validator) {
        if !block_has_nontrivial_require(&validator.body) {
            self.warnings.push(LintWarning::new(
                self.file,
                validator.span,
                format!(
                    "validator '{}' has no non-trivial require guard",
                    validator.name
                ),
            ));
        }

        self.block(&validator.body);
    }

    fn block(&mut self, block: &Block) {
        for statement in &block.statements {
            match statement {
                Statement::Let { value, .. }
                | Statement::Return { value, .. }
                | Statement::Expr { value, .. } => self.expr(value),
                Statement::Require { condition, span } => {
                    if let Some(value) = constant_bool(condition) {
                        let result = if value { "true" } else { "false" };
                        self.warnings.push(LintWarning::new(
                            self.file,
                            *span,
                            format!("require guard is always {result}"),
                        ));
                    }
                    self.expr(condition);
                }
                Statement::Trace { message, .. } => self.expr(message),
            }
        }
    }

    fn expr(&mut self, expr: &Expr) {
        match &expr.kind {
            ExprKind::ByteArray(hex) => {
                if is_placeholder_bytes(hex) {
                    self.warnings.push(LintWarning::new(
                        self.file,
                        expr.span,
                        "placeholder all-zero byte array",
                    ));
                }
            }
            ExprKind::Binary { left, op, right } => {
                if matches!(op, BinaryOp::Equal) && same_expr(left, right) {
                    self.warnings.push(LintWarning::new(
                        self.file,
                        expr.span,
                        "comparison is always true because both sides are identical",
                    ));
                }
                if matches!(op, BinaryOp::Divide | BinaryOp::Remainder) && is_zero_int(right) {
                    self.warnings.push(LintWarning::new(
                        self.file,
                        right.span,
                        "arithmetic operation has a literal zero divisor",
                    ));
                }
                self.expr(left);
                self.expr(right);
            }
            ExprKind::Call { callee, args } => {
                if callee == "datum_equals" && args.len() == 2 && same_expr(&args[0], &args[1]) {
                    self.warnings.push(LintWarning::new(
                        self.file,
                        expr.span,
                        "datum_equals compares a value with itself",
                    ));
                }
                for arg in args {
                    self.expr(arg);
                }
            }
            ExprKind::Unary { expr, .. } => self.expr(expr),
            ExprKind::If {
                condition,
                then_branch,
                else_branch,
            } => {
                self.expr(condition);
                self.block(then_branch);
                self.block(else_branch);
            }
            ExprKind::Match { subject, arms } => {
                self.expr(subject);
                for arm in arms {
                    self.pattern(&arm.pattern);
                    self.block(&arm.body);
                }
            }
            ExprKind::List(items) => {
                for item in items {
                    self.expr(item);
                }
            }
            ExprKind::Bool(_)
            | ExprKind::Int(_)
            | ExprKind::String(_)
            | ExprKind::Unit
            | ExprKind::Fail
            | ExprKind::Variable(_) => (),
        }
    }

    fn pattern(&mut self, pattern: &Pattern) {
        match pattern {
            Pattern::ByteArray { hex, span } if is_placeholder_bytes(hex) => {
                self.warnings.push(LintWarning::new(
                    self.file,
                    *span,
                    "placeholder all-zero byte array in pattern",
                ));
            }
            Pattern::Wildcard { .. }
            | Pattern::Variable { .. }
            | Pattern::Constructor { .. }
            | Pattern::Bool { .. }
            | Pattern::Int { .. }
            | Pattern::ByteArray { .. }
            | Pattern::String { .. }
            | Pattern::Unit { .. } => (),
        }
    }
}

fn block_has_nontrivial_require(block: &Block) -> bool {
    block
        .statements
        .iter()
        .any(statement_has_nontrivial_require)
}

fn statement_has_nontrivial_require(statement: &Statement) -> bool {
    match statement {
        Statement::Require { condition, .. } => constant_bool(condition).is_none(),
        Statement::Let { value, .. }
        | Statement::Return { value, .. }
        | Statement::Expr { value, .. } => expr_has_nontrivial_require(value),
        Statement::Trace { message, .. } => expr_has_nontrivial_require(message),
    }
}

fn expr_has_nontrivial_require(expr: &Expr) -> bool {
    match &expr.kind {
        ExprKind::Unary { expr, .. } => expr_has_nontrivial_require(expr),
        ExprKind::Binary { left, right, .. } => {
            expr_has_nontrivial_require(left) || expr_has_nontrivial_require(right)
        }
        ExprKind::Call { args, .. } | ExprKind::List(args) => {
            args.iter().any(expr_has_nontrivial_require)
        }
        ExprKind::If {
            condition,
            then_branch,
            else_branch,
        } => {
            expr_has_nontrivial_require(condition)
                || block_has_nontrivial_require(then_branch)
                || block_has_nontrivial_require(else_branch)
        }
        ExprKind::Match { subject, arms } => {
            expr_has_nontrivial_require(subject)
                || arms
                    .iter()
                    .any(|arm| block_has_nontrivial_require(&arm.body))
        }
        ExprKind::Bool(_)
        | ExprKind::Int(_)
        | ExprKind::String(_)
        | ExprKind::ByteArray(_)
        | ExprKind::Unit
        | ExprKind::Fail
        | ExprKind::Variable(_) => false,
    }
}

fn constant_bool(expr: &Expr) -> Option<bool> {
    match &expr.kind {
        ExprKind::Bool(value) => Some(*value),
        ExprKind::Unary { op, expr } => match op {
            crate::ast::UnaryOp::Not => constant_bool(expr).map(|value| !value),
            crate::ast::UnaryOp::Negate => None,
        },
        ExprKind::Binary { left, op, right } if same_expr(left, right) => match op {
            BinaryOp::Equal => Some(true),
            BinaryOp::NotEqual => Some(false),
            _ => None,
        },
        ExprKind::Call { callee, args }
            if callee == "datum_equals" && args.len() == 2 && same_expr(&args[0], &args[1]) =>
        {
            Some(true)
        }
        _ => None,
    }
}

fn is_placeholder_bytes(hex: &str) -> bool {
    hex.len() >= 16 && hex.chars().all(|ch| ch == '0')
}

fn is_zero_int(expr: &Expr) -> bool {
    matches!(expr.kind, ExprKind::Int(0))
}

fn same_expr(left: &Expr, right: &Expr) -> bool {
    match (&left.kind, &right.kind) {
        (ExprKind::Bool(left), ExprKind::Bool(right)) => left == right,
        (ExprKind::Int(left), ExprKind::Int(right)) => left == right,
        (ExprKind::String(left), ExprKind::String(right)) => left == right,
        (ExprKind::ByteArray(left), ExprKind::ByteArray(right)) => left == right,
        (ExprKind::Unit, ExprKind::Unit) => true,
        (ExprKind::Fail, ExprKind::Fail) => true,
        (ExprKind::Variable(left), ExprKind::Variable(right)) => left == right,
        (ExprKind::List(left), ExprKind::List(right)) => {
            left.len() == right.len()
                && left
                    .iter()
                    .zip(right.iter())
                    .all(|(left, right)| same_expr(left, right))
        }
        _ => false,
    }
}