Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use std::cmp::Ordering;
use std::collections::HashSet;

pub mod adjacent_overload_signatures;
pub mod bad_char_at_comparison;
pub mod bad_object_literal_comparison;
pub mod ban_ts_comment;
pub mod ban_types;
pub mod ban_unknown_rule_code;
Expand Down Expand Up @@ -116,6 +118,7 @@ pub mod no_window;
pub mod no_window_prefix;
pub mod no_with;
pub mod node_builtin_specifier;
pub mod number_arg_out_of_range;
pub mod prefer_as_const;
pub mod prefer_ascii;
pub mod prefer_const;
Expand Down Expand Up @@ -253,6 +256,8 @@ pub(crate) fn sort_rules_by_priority(rules: &mut [Box<dyn LintRule>]) {
fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
vec![
Box::new(adjacent_overload_signatures::AdjacentOverloadSignatures),
Box::new(bad_char_at_comparison::BadCharAtComparison),
Box::new(bad_object_literal_comparison::BadObjectLiteralComparison),
Box::new(ban_ts_comment::BanTsComment),
Box::new(ban_types::BanTypes),
Box::new(ban_unknown_rule_code::BanUnknownRuleCode),
Expand Down Expand Up @@ -364,6 +369,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
Box::new(no_window_prefix::NoWindowPrefix),
Box::new(no_with::NoWith),
Box::new(node_builtin_specifier::NodeBuiltinsSpecifier),
Box::new(number_arg_out_of_range::NumberArgOutOfRange),
Box::new(prefer_as_const::PreferAsConst),
Box::new(prefer_ascii::PreferAscii),
Box::new(prefer_const::PreferConst),
Expand Down
156 changes: 156 additions & 0 deletions src/rules/bad_char_at_comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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::view::BinaryOp;
use deno_ast::view::{BinExpr, Callee, Expr, Lit, MemberProp};
use deno_ast::SourceRanged;
use derive_more::Display;

#[derive(Debug)]
pub struct BadCharAtComparison;

const CODE: &str = "bad-char-at-comparison";

#[derive(Display)]
enum BadCharAtComparisonMessage {
#[display(
fmt = "`charAt` returns a single character, so comparing its result against a string of a different length is always false"
)]
Unexpected,
}

#[derive(Display)]
enum BadCharAtComparisonHint {
#[display(
fmt = "Compare against a single character, or use `startsWith`/`slice` to compare longer substrings"
)]
Fix,
}

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

fn code(&self) -> &'static str {
CODE
}

fn lint_program_with_ast_view<'view>(
&self,
context: &mut Context,
program: Program<'_>,
) {
BadCharAtComparisonHandler.traverse(program, context);
}
}

struct BadCharAtComparisonHandler;

impl Handler for BadCharAtComparisonHandler {
fn bin_expr(&mut self, bin_expr: &BinExpr, ctx: &mut Context) {
if !matches!(
bin_expr.op(),
BinaryOp::EqEq | BinaryOp::NotEq | BinaryOp::EqEqEq | BinaryOp::NotEqEq
) {
return;
}

let bad = (is_char_at_call(&bin_expr.left)
&& is_bad_comparison_string(&bin_expr.right))
|| (is_char_at_call(&bin_expr.right)
&& is_bad_comparison_string(&bin_expr.left));

if bad {
ctx.add_diagnostic_with_hint(
bin_expr.range(),
CODE,
BadCharAtComparisonMessage::Unexpected,
BadCharAtComparisonHint::Fix,
);
}
}
}

fn is_char_at_call(expr: &Expr) -> bool {
let Expr::Call(call) = expr else {
return false;
};
let Callee::Expr(callee) = call.callee else {
return false;
};
let Expr::Member(member) = callee else {
return false;
};
let MemberProp::Ident(prop) = member.prop else {
return false;
};
prop.sym() == "charAt" && call.args.len() == 1
}

fn is_bad_comparison_string(expr: &Expr) -> bool {
let Expr::Lit(Lit::Str(s)) = expr else {
return false;
};
let value = s.value().to_string_lossy();
// `charAt` yields exactly one UTF-16 code unit. An empty string or a single
// character may legitimately be compared; anything longer can never match.
value.len() >= 2 && value.chars().count() != 1
}

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

// Some tests are derived from
// https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/rules/oxc/bad_char_at_comparison.rs
// MIT Licensed.

#[test]
fn bad_char_at_comparison_valid() {
assert_lint_ok! {
BadCharAtComparison,
"a.charAt(4) === 'a';",
"a.charAt(4) === '\\n';",
"a.charAt(4) !== 'b';",
// Not a `charAt` call.
"chatAt(4) === 'a2';",
"a.foo(4) === 'a2';",
// Right-hand side is not a string literal.
"a.charAt(4) === 'a' + 'b';",
"a.charAt(4) === x;",
};
}

#[test]
fn bad_char_at_comparison_invalid() {
assert_lint_err! {
BadCharAtComparison,
"a.charAt(4) === 'aa';": [
{
col: 0,
message: BadCharAtComparisonMessage::Unexpected,
hint: BadCharAtComparisonHint::Fix,
}
],
"a.charAt(822) !== 'foo';": [
{
col: 0,
message: BadCharAtComparisonMessage::Unexpected,
hint: BadCharAtComparisonHint::Fix,
}
],
// Reversed operand order.
"'aa' === a.charAt(4);": [
{
col: 0,
message: BadCharAtComparisonMessage::Unexpected,
hint: BadCharAtComparisonHint::Fix,
}
]
};
}
}
154 changes: 154 additions & 0 deletions src/rules/bad_object_literal_comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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::view::BinaryOp;
use deno_ast::view::{BinExpr, Expr};
use deno_ast::SourceRanged;
use derive_more::Display;

#[derive(Debug)]
pub struct BadObjectLiteralComparison;

const CODE: &str = "bad-object-literal-comparison";

#[derive(Display)]
enum BadObjectLiteralComparisonMessage {
#[display(
fmt = "Comparing against an empty object literal is always false (or always true for `!=`/`!==`); object references are never equal"
)]
Object,
#[display(
fmt = "Comparing against an empty array literal is always false (or always true for `!=`/`!==`); array references are never equal"
)]
Array,
}

#[derive(Display)]
enum BadObjectLiteralComparisonHint {
#[display(
fmt = "To check for an empty object, use `Object.keys(x).length === 0`"
)]
Object,
#[display(
fmt = "To check for an empty array, use `Array.isArray(x) && x.length === 0`"
)]
Array,
}

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

fn code(&self) -> &'static str {
CODE
}

fn lint_program_with_ast_view<'view>(
&self,
context: &mut Context,
program: Program<'_>,
) {
BadObjectLiteralComparisonHandler.traverse(program, context);
}
}

struct BadObjectLiteralComparisonHandler;

impl Handler for BadObjectLiteralComparisonHandler {
fn bin_expr(&mut self, bin_expr: &BinExpr, ctx: &mut Context) {
if !matches!(
bin_expr.op(),
BinaryOp::EqEq | BinaryOp::NotEq | BinaryOp::EqEqEq | BinaryOp::NotEqEq
) {
return;
}

if is_empty_object(&bin_expr.left) || is_empty_object(&bin_expr.right) {
ctx.add_diagnostic_with_hint(
bin_expr.range(),
CODE,
BadObjectLiteralComparisonMessage::Object,
BadObjectLiteralComparisonHint::Object,
);
} else if is_empty_array(&bin_expr.left) || is_empty_array(&bin_expr.right)
{
ctx.add_diagnostic_with_hint(
bin_expr.range(),
CODE,
BadObjectLiteralComparisonMessage::Array,
BadObjectLiteralComparisonHint::Array,
);
}
}
}

fn is_empty_object(expr: &Expr) -> bool {
matches!(expr, Expr::Object(obj) if obj.props.is_empty())
}

fn is_empty_array(expr: &Expr) -> bool {
matches!(expr, Expr::Array(arr) if arr.elems.is_empty())
}

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

// Some tests are derived from
// https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/rules/oxc/bad_object_literal_comparison.rs
// MIT Licensed.

#[test]
fn bad_object_literal_comparison_valid() {
assert_lint_ok! {
BadObjectLiteralComparison,
"if (x === null) {}",
"if (x === undefined) {}",
"if (typeof x === 'object') {}",
"if (x === y) {}",
// Non-empty literals are not flagged.
"if (x === { a: 1 }) {}",
"if (x !== [1]) {}",
};
}

#[test]
fn bad_object_literal_comparison_invalid() {
assert_lint_err! {
BadObjectLiteralComparison,
"if (x === {}) {}": [
{
col: 4,
message: BadObjectLiteralComparisonMessage::Object,
hint: BadObjectLiteralComparisonHint::Object,
}
],
"if (x != {}) {}": [
{
col: 4,
message: BadObjectLiteralComparisonMessage::Object,
hint: BadObjectLiteralComparisonHint::Object,
}
],
"if (arr !== []) {}": [
{
col: 4,
message: BadObjectLiteralComparisonMessage::Array,
hint: BadObjectLiteralComparisonHint::Array,
}
],
// Reversed operand order.
"[] === x;": [
{
col: 0,
message: BadObjectLiteralComparisonMessage::Array,
hint: BadObjectLiteralComparisonHint::Array,
}
]
};
}
}
Loading