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)]
pub struct LintWarning {
pub file: String,
pub line: usize,
pub column: usize,
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(),
}
}
}
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,
}
}