deno_lint 0.84.1

lint for deno
Documentation
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use super::{Context, LintRule};
use crate::handler::{Handler, Traverse};
use crate::tags::{self, Tags};
use crate::Program;
use deno_ast::swc::ast::AssignOp;
use deno_ast::swc::ast::BinaryOp;
use deno_ast::swc::ast::UpdateOp;
use deno_ast::view::{
  AssignExpr, AssignTarget, Expr, SimpleAssignTarget, UnaryOp, UpdateExpr,
};
use deno_ast::{view as ast_view, SourceRanged};

#[derive(Debug)]
pub struct ForDirection;

impl LintRule for ForDirection {
  fn tags(&self) -> Tags {
    &[tags::RECOMMENDED]
  }

  fn code(&self) -> &'static str {
    "for-direction"
  }

  fn lint_program_with_ast_view(
    &self,
    context: &mut Context,
    program: Program,
  ) {
    ForDirectionHandler.traverse(program, context);
  }
}

const MESSAGE: &str = "Update clause moves variable in the wrong direction";
const HINT: &str =
  "Flip the update clause logic or change the continuation step condition";

struct ForDirectionHandler;

fn check_update_direction(
  update_expr: &UpdateExpr,
  counter_name: impl AsRef<str>,
) -> i32 {
  let mut update_direction = 0;

  if let Expr::Ident(ident) = update_expr.arg {
    if ident.inner.as_ref() == counter_name.as_ref() {
      match update_expr.op() {
        UpdateOp::PlusPlus => {
          update_direction = 1;
        }
        UpdateOp::MinusMinus => {
          update_direction = -1;
        }
      }
    }
  }

  update_direction
}

fn check_assign_direction(
  assign_expr: &AssignExpr,
  counter_name: impl AsRef<str>,
) -> i32 {
  let update_direction = 0;

  let name = match &assign_expr.left {
    AssignTarget::Simple(SimpleAssignTarget::Ident(ident)) => {
      ident.inner.as_ref()
    }
    _ => return update_direction,
  };

  if name == counter_name.as_ref() {
    return match assign_expr.op() {
      AssignOp::AddAssign => check_assign_right_direction(assign_expr, 1),
      AssignOp::SubAssign => check_assign_right_direction(assign_expr, -1),
      _ => update_direction,
    };
  }
  update_direction
}

fn check_assign_right_direction(
  assign_expr: &AssignExpr,
  direction: i32,
) -> i32 {
  match &assign_expr.right {
    Expr::Unary(unary_expr) => {
      if unary_expr.op() == UnaryOp::Minus {
        -direction
      } else {
        direction
      }
    }
    Expr::Ident(_) => 0,
    _ => direction,
  }
}

impl Handler for ForDirectionHandler {
  fn for_stmt(&mut self, for_stmt: &ast_view::ForStmt, context: &mut Context) {
    if for_stmt.update.is_none() {
      return;
    }

    if let Some(Expr::Bin(bin_expr)) = &for_stmt.test {
      let counter_name = match &bin_expr.left {
        Expr::Ident(ident) => ident.inner.as_ref(),
        _ => return,
      };

      let wrong_direction = match &bin_expr.op() {
        BinaryOp::Lt | BinaryOp::LtEq => -1,
        BinaryOp::Gt | BinaryOp::GtEq => 1,
        _ => return,
      };

      let update = for_stmt.update.as_ref().unwrap();
      let update_direction = match &update {
        Expr::Update(update_expr) => {
          check_update_direction(update_expr, counter_name)
        }
        Expr::Assign(assign_expr) => {
          check_assign_direction(assign_expr, counter_name)
        }
        _ => return,
      };

      if update_direction == wrong_direction {
        context.add_diagnostic_with_hint(
          for_stmt.range(),
          "for-direction",
          MESSAGE,
          HINT,
        );
      }
    }
  }
}

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

  #[test]
  fn for_direction_valid() {
    assert_lint_ok! {
      ForDirection,
      "for(let i = 0; i < 2; i++) {}",
      "for(let i = 0; i < 2; ++i) {}",
      "for(let i = 0; i <= 2; i++) {}",
      "for(let i = 0; i <= 2; ++i) {}",
      "for(let i = 2; i > 2; i--) {}",
      "for(let i = 2; i > 2; --i) {}",
      "for(let i = 2; i >= 0; i--) {}",
      "for(let i = 2; i >= 0; --i) {}",
      "for(let i = 0; i < 2; i += 1) {}",
      "for(let i = 0; i <= 2; i += 1) {}",
      "for(let i = 0; i < 2; i -= -1) {}",
      "for(let i = 0; i <= 2; i -= -1) {}",
      "for(let i = 2; i > 2; i -= 1) {}",
      "for(let i = 2; i >= 0; i -= 1) {}",
      "for(let i = 2; i > 2; i += -1) {}",
      "for(let i = 2; i >= 0; i += -1) {}",
      "for(let i = 0; i < 2;) {}",
      "for(let i = 0; i <= 2;) {}",
      "for(let i = 2; i > 2;) {}",
      "for(let i = 2; i >= 0;) {}",
      "for(let i = 0; i < 2; i |= 2) {}",
      "for(let i = 0; i <= 2; i %= 2) {}",
      "for(let i = 0; i < 2; j++) {}",
      "for(let i = 0; i <= 2; j--) {}",
      "for(let i = 2; i > 2; j++) {}",
      "for(let i = 2; i >= 0; j--) {}",
      "for(let i = 0; i !== 10; i++) {}",
      "for(let i = 0; i != 10; i++) {}",
      "for(let i = 0; i === 0; i++) {}",
      "for(let i = 0; i == 0; i++) {}",
      "for(let i = 0; i < 2; ++i) { for (let j = 0; j < 2; j++) {} }",
    };
  }

  #[test]
  fn for_direction_invalid() {
    assert_lint_err! {
      ForDirection,

      // ++, --
      "for(let i = 0; i < 2; i--) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i < 2; --i) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i <= 2; i--) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i <= 2; --i) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i > 2; i++) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i > 2; ++i) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i >= 0; i++) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i >= 0; ++i) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],

      // +=, -=
      "for(let i = 0; i < 2; i -= 1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i <= 2; i -= 1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i > 2; i -= -1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i >= 0; i -= -1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i > 2; i += 1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 2; i >= 0; i += 1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i < 2; i += -1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],
      "for(let i = 0; i <= 2; i += -1) {}": [
        {
          col: 0,
          message: MESSAGE,
          hint: HINT,
        }
      ],

      // nested
      r#"
for (let i = 0; i < 2; i++) {
  for (let j = 0; j < 2; j--) {}
}
      "#: [
        {
          line: 3,
          col: 2,
          message: MESSAGE,
          hint: HINT,
        }
      ]
    };
  }
}