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 0000000000000..7f1a0b47264d5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py @@ -0,0 +1,9 @@ +print([1, 2, 3][3]) # PLE0643 +print([1, 2, 3][-4]) # PLE0643 +print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643 +print([1, 2, 3][-999999999999999999999999999999999999999999]) # 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 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index 7896a2f4f7c82..75ee29c6ea5d0 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 d073f0223f089..40bbdabb659a3 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 9182956716f34..6616ac26b2ed2 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 ba5f2916dca13..22be5657bd22d 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 0000000000000..0e4048c20e3fb --- /dev/null +++ b/crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs @@ -0,0 +1,73 @@ +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast, Expr}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for hard-coded sequence accesses that are known to be out of bounds. +/// +/// ## Why is this bad? +/// Attempting to access a sequence with an out-of-bounds index will cause an +/// `IndexError` to be raised at runtime. When the sequence and index are +/// defined statically (e.g., subscripts on `list` and `tuple` literals, with +/// integer indexes), such errors can be detected ahead of time. +/// +/// ## Example +/// ```python +/// print([0, 1, 2][3]) +/// ``` +#[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) { + // Determine the length of the sequence. + let length = match value { + Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => { + match i64::try_from(elts.len()) { + Ok(length) => length, + Err(_) => return, + } + } + _ => { + return; + } + }; + + // Determine the index value. + let index = match slice { + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(number_value), + .. + }) => number_value.as_i64(), + Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::USub, + operand, + .. + }) => match operand.as_ref() { + Expr::NumberLiteral(ast::ExprNumberLiteral { + value: ast::Number::Int(number_value), + .. + }) => number_value.as_i64().map(|number| -number), + _ => return, + }, + _ => return, + }; + + // Emit a diagnostic if the index is out of bounds. If the index can't be represented as an + // `i64`, but the length _can_, then the index is definitely out of bounds. + if index.map_or(true, |index| index >= length || index < -length) { + checker + .diagnostics + .push(Diagnostic::new(PotentialIndexError, slice.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 0000000000000..2cfa565dc5eb8 --- /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][999999999999999999999999999999999999999999]) # 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][999999999999999999999999999999999999999999]) # PLE0643 +4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # 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][999999999999999999999999999999999999999999]) # PLE0643 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643 +4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643 + | + +potential_index_error.py:4:17: PLE0643 Potential IndexError + | +2 | print([1, 2, 3][-4]) # PLE0643 +3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643 +4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643 +5 | +6 | print([1, 2, 3][2]) # OK + | + + diff --git a/ruff.schema.json b/ruff.schema.json index ef61102357472..9ea851bd26b54 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3129,6 +3129,8 @@ "PLE060", "PLE0604", "PLE0605", + "PLE064", + "PLE0643", "PLE07", "PLE070", "PLE0704",