diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py b/crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py new file mode 100644 index 00000000000000..c4a6b193092a39 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py @@ -0,0 +1,11 @@ +print([1, 2, 3][3]) # PLE0643 +print([1, 2, 3][-4]) # PLE0643 +print([1, 2, 3][2147483647]) # PLE0643 +print([1, 2, 3][-2147483647]) # PLE0643 + +print([1, 2, 3][2]) # OK +print([1, 2, 3][0]) # OK +print([1, 2, 3][-3]) # OK +print([1, 2, 3][3:]) # OK +print([1, 2, 3][2147483648]) # OK (i32 overflow, ignored) +print([1, 2, 3][-2147483648]) # OK (i32 overflow, ignored) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index f8214c6c8924d9..64e3634982114c 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -125,6 +125,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::SliceCopy) { refurb::rules::slice_copy(checker, subscript); } + if checker.enabled(Rule::PotentialIndexError) { + pylint::rules::potential_index_error(checker, value, slice); + } pandas_vet::rules::subscript(checker, value, expr); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 4975d6992aab6c..f8a30866ee2b37 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -229,6 +229,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType), (Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject), (Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat), + (Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError), (Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise), (Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument), (Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync), diff --git a/crates/ruff_linter/src/rules/pylint/mod.rs b/crates/ruff_linter/src/rules/pylint/mod.rs index 9182956716f34e..6616ac26b2ed26 100644 --- a/crates/ruff_linter/src/rules/pylint/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/mod.rs @@ -167,6 +167,7 @@ mod tests { #[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))] #[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))] #[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))] + #[test_case(Rule::PotentialIndexError, Path::new("potential_index_error.py"))] #[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))] #[test_case( Rule::UnnecessaryDictIndexLookup, diff --git a/crates/ruff_linter/src/rules/pylint/rules/mod.rs b/crates/ruff_linter/src/rules/pylint/rules/mod.rs index ba5f2916dca13e..22be5657bd22d5 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/mod.rs @@ -42,6 +42,7 @@ pub(crate) use no_self_use::*; pub(crate) use non_ascii_module_import::*; pub(crate) use non_ascii_name::*; pub(crate) use nonlocal_without_binding::*; +pub(crate) use potential_index_error::*; pub(crate) use property_with_parameters::*; pub(crate) use redefined_argument_from_local::*; pub(crate) use redefined_loop_name::*; @@ -124,6 +125,7 @@ mod no_self_use; mod non_ascii_module_import; mod non_ascii_name; mod nonlocal_without_binding; +mod potential_index_error; mod property_with_parameters; mod redefined_argument_from_local; mod redefined_loop_name; diff --git a/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs new file mode 100644 index 00000000000000..7b20c498e6722d --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs @@ -0,0 +1,77 @@ +use ruff_python_ast::{self as ast, Expr}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_text_size::TextRange; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for potential hard-coded IndexErrors, which occurs when accessing +/// a list or tuple with an index that is known to be out of bounds. +/// +/// ## Why is this bad? +/// This will cause a runtime error. +/// +/// ## Example +/// ```python +/// print([1, 2, 3][123]) +/// ``` +/// +#[violation] +pub struct PotentialIndexError; + +impl Violation for PotentialIndexError { + #[derive_message_formats] + fn message(&self) -> String { + format!("Potential IndexError") + } +} + +/// PLE0643 +pub(crate) fn potential_index_error(checker: &mut Checker, value: &Expr, slice: &Expr) { + let length = match value { + Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => { + i32::try_from(elts.len()) + } + _ => { + return; + } + }; + + let Ok(length) = length else { + return; + }; + + let (number_value, range) = match slice { + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(number_value), + range, + }) => (number_value.as_i32(), *range), + Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::USub, + operand, + range, + }) => match operand.as_ref() { + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(number_value), + .. + }) => match number_value.as_i32() { + Some(value) => (Some(-value), *range), + None => (None, TextRange::default()), + }, + _ => (None, TextRange::default()), + }, + _ => (None, TextRange::default()), + }; + + let Some(number_value) = number_value else { + return; + }; + + if number_value >= length || number_value < -length { + checker + .diagnostics + .push(Diagnostic::new(PotentialIndexError, range)); + } +} diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0643_potential_index_error.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0643_potential_index_error.py.snap new file mode 100644 index 00000000000000..8c0564d15a5ea3 --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0643_potential_index_error.py.snap @@ -0,0 +1,40 @@ +--- +source: crates/ruff_linter/src/rules/pylint/mod.rs +--- +potential_index_error.py:1:17: PLE0643 Potential IndexError + | +1 | print([1, 2, 3][3]) # PLE0643 + | ^ PLE0643 +2 | print([1, 2, 3][-4]) # PLE0643 +3 | print([1, 2, 3][2147483647]) # PLE0643 + | + +potential_index_error.py:2:17: PLE0643 Potential IndexError + | +1 | print([1, 2, 3][3]) # PLE0643 +2 | print([1, 2, 3][-4]) # PLE0643 + | ^^ PLE0643 +3 | print([1, 2, 3][2147483647]) # PLE0643 +4 | print([1, 2, 3][-2147483647]) # PLE0643 + | + +potential_index_error.py:3:17: PLE0643 Potential IndexError + | +1 | print([1, 2, 3][3]) # PLE0643 +2 | print([1, 2, 3][-4]) # PLE0643 +3 | print([1, 2, 3][2147483647]) # PLE0643 + | ^^^^^^^^^^ PLE0643 +4 | print([1, 2, 3][-2147483647]) # PLE0643 + | + +potential_index_error.py:4:17: PLE0643 Potential IndexError + | +2 | print([1, 2, 3][-4]) # PLE0643 +3 | print([1, 2, 3][2147483647]) # PLE0643 +4 | print([1, 2, 3][-2147483647]) # PLE0643 + | ^^^^^^^^^^^ PLE0643 +5 | +6 | print([1, 2, 3][2]) # OK + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 9289747f76f4c1..78e801d2790eec 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3129,6 +3129,8 @@ "PLE060", "PLE0604", "PLE0605", + "PLE064", + "PLE0643", "PLE07", "PLE070", "PLE0704",