[pylint] Implement potential-index-error (PLE0643) (#9545)

## Summary

add `potential-index-error` rule (`PLE0643`)

See: #970 

## Test Plan

`cargo test`
This commit is contained in:
Steve C 2024-01-20 22:59:48 -05:00 committed by GitHub
parent 866bea60a5
commit 49a445a23d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 131 additions and 0 deletions

View file

@ -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

View file

@ -125,6 +125,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SliceCopy) { if checker.enabled(Rule::SliceCopy) {
refurb::rules::slice_copy(checker, subscript); 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); pandas_vet::rules::subscript(checker, value, expr);
} }

View file

@ -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, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType),
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject), (Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat), (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, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument), (Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync), (Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),

View file

@ -167,6 +167,7 @@ mod tests {
#[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))] #[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))] #[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))]
#[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.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::SuperWithoutBrackets, Path::new("super_without_brackets.py"))]
#[test_case( #[test_case(
Rule::UnnecessaryDictIndexLookup, Rule::UnnecessaryDictIndexLookup,

View file

@ -42,6 +42,7 @@ pub(crate) use no_self_use::*;
pub(crate) use non_ascii_module_import::*; pub(crate) use non_ascii_module_import::*;
pub(crate) use non_ascii_name::*; pub(crate) use non_ascii_name::*;
pub(crate) use nonlocal_without_binding::*; pub(crate) use nonlocal_without_binding::*;
pub(crate) use potential_index_error::*;
pub(crate) use property_with_parameters::*; pub(crate) use property_with_parameters::*;
pub(crate) use redefined_argument_from_local::*; pub(crate) use redefined_argument_from_local::*;
pub(crate) use redefined_loop_name::*; pub(crate) use redefined_loop_name::*;
@ -124,6 +125,7 @@ mod no_self_use;
mod non_ascii_module_import; mod non_ascii_module_import;
mod non_ascii_name; mod non_ascii_name;
mod nonlocal_without_binding; mod nonlocal_without_binding;
mod potential_index_error;
mod property_with_parameters; mod property_with_parameters;
mod redefined_argument_from_local; mod redefined_argument_from_local;
mod redefined_loop_name; mod redefined_loop_name;

View file

@ -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()));
}
}

View file

@ -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
|

2
ruff.schema.json generated
View file

@ -3129,6 +3129,8 @@
"PLE060", "PLE060",
"PLE0604", "PLE0604",
"PLE0605", "PLE0605",
"PLE064",
"PLE0643",
"PLE07", "PLE07",
"PLE070", "PLE070",
"PLE0704", "PLE0704",