From 47ad7b4500d37a67c48edc22dd9f5ed8b2a09e6c Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Fri, 19 Jan 2024 17:39:37 +0100 Subject: [PATCH 1/5] Approximate tokens len (#9546) --- crates/ruff_benchmark/benches/formatter.rs | 4 +-- .../ruff_python_index/src/comment_ranges.rs | 4 +-- crates/ruff_python_parser/src/lib.rs | 34 ++++++++++++++++--- crates/ruff_python_parser/src/parser.rs | 5 ++- crates/ruff_wasm/src/lib.rs | 4 +-- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs index 7d415c2bde0310..98c3a97f2c956e 100644 --- a/crates/ruff_benchmark/benches/formatter.rs +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -7,7 +7,7 @@ use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError}; use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions}; use ruff_python_index::CommentRangesBuilder; use ruff_python_parser::lexer::lex; -use ruff_python_parser::{parse_tokens, Mode}; +use ruff_python_parser::{allocate_tokens_vec, parse_tokens, Mode}; #[cfg(target_os = "windows")] #[global_allocator] @@ -52,7 +52,7 @@ fn benchmark_formatter(criterion: &mut Criterion) { BenchmarkId::from_parameter(case.name()), &case, |b, case| { - let mut tokens = Vec::new(); + let mut tokens = allocate_tokens_vec(case.code()); let mut comment_ranges = CommentRangesBuilder::default(); for result in lex(case.code(), Mode::Module) { diff --git a/crates/ruff_python_index/src/comment_ranges.rs b/crates/ruff_python_index/src/comment_ranges.rs index 11e6496a38b18e..e9ef4c04620bf1 100644 --- a/crates/ruff_python_index/src/comment_ranges.rs +++ b/crates/ruff_python_index/src/comment_ranges.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use ruff_python_ast::PySourceType; use ruff_python_parser::lexer::{lex, LexResult, LexicalError}; -use ruff_python_parser::{AsMode, Tok}; +use ruff_python_parser::{allocate_tokens_vec, AsMode, Tok}; use ruff_python_trivia::CommentRanges; use ruff_text_size::TextRange; @@ -28,7 +28,7 @@ pub fn tokens_and_ranges( source: &str, source_type: PySourceType, ) -> Result<(Vec, CommentRanges), LexicalError> { - let mut tokens = Vec::new(); + let mut tokens = allocate_tokens_vec(source); let mut comment_ranges = CommentRangesBuilder::default(); for result in lex(source, source_type.as_mode()) { diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index 0217331fe21ffe..2f95c684e87d97 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -78,14 +78,14 @@ //! These tokens can be directly fed into the `ruff_python_parser` to generate an AST: //! //! ``` -//! use ruff_python_parser::{lexer::lex, Mode, parse_tokens}; +//! use ruff_python_parser::{Mode, parse_tokens, tokenize_all}; //! //! let python_source = r#" //! def is_odd(i): //! return bool(i & 1) //! "#; -//! let tokens = lex(python_source, Mode::Module); -//! let ast = parse_tokens(tokens.collect(), python_source, Mode::Module); +//! let tokens = tokenize_all(python_source, Mode::Module); +//! let ast = parse_tokens(tokens, python_source, Mode::Module); //! //! assert!(ast.is_ok()); //! ``` @@ -133,7 +133,7 @@ pub mod typing; /// Collect tokens up to and including the first error. pub fn tokenize(contents: &str, mode: Mode) -> Vec { - let mut tokens: Vec = vec![]; + let mut tokens: Vec = allocate_tokens_vec(contents); for tok in lexer::lex(contents, mode) { let is_err = tok.is_err(); tokens.push(tok); @@ -141,9 +141,35 @@ pub fn tokenize(contents: &str, mode: Mode) -> Vec { break; } } + + tokens +} + +/// Tokenizes all tokens. +/// +/// It differs from [`tokenize`] in that it tokenizes all tokens and doesn't stop +/// after the first `Err`. +pub fn tokenize_all(contents: &str, mode: Mode) -> Vec { + let mut tokens = allocate_tokens_vec(contents); + for token in lexer::lex(contents, mode) { + tokens.push(token); + } tokens } +/// Allocates a [`Vec`] with an approximated capacity to fit all tokens +/// of `contents`. +/// +/// See [#9546](https://github.com/astral-sh/ruff/pull/9546) for a more detailed explanation. +pub fn allocate_tokens_vec(contents: &str) -> Vec { + Vec::with_capacity(approximate_tokens_lower_bound(contents)) +} + +/// Approximates the number of tokens when lexing `contents`. +fn approximate_tokens_lower_bound(contents: &str) -> usize { + contents.len().saturating_mul(15) / 100 +} + /// Parse a full Python program from its tokens. pub fn parse_program_tokens( tokens: Vec, diff --git a/crates/ruff_python_parser/src/parser.rs b/crates/ruff_python_parser/src/parser.rs index e158dadfcff4da..c0f6c7d18d2cbb 100644 --- a/crates/ruff_python_parser/src/parser.rs +++ b/crates/ruff_python_parser/src/parser.rs @@ -31,7 +31,7 @@ use crate::{ lexer::{self, LexicalError, LexicalErrorType}, python, token::Tok, - Mode, + tokenize_all, Mode, }; /// Parse a full Python program usually consisting of multiple lines. @@ -55,8 +55,7 @@ use crate::{ /// assert!(program.is_ok()); /// ``` pub fn parse_program(source: &str) -> Result { - let lexer = lex(source, Mode::Module); - match parse_tokens(lexer.collect(), source, Mode::Module)? { + match parse_tokens(tokenize_all(source, Mode::Module), source, Mode::Module)? { Mod::Module(m) => Ok(m), Mod::Expression(_) => unreachable!("Mode::Module doesn't return other variant"), } diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 7ebfd67ca327e7..f83ed36b79b45f 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -17,7 +17,7 @@ use ruff_python_codegen::Stylist; use ruff_python_formatter::{format_module_ast, pretty_comments, PyFormatContext, QuoteStyle}; use ruff_python_index::{CommentRangesBuilder, Indexer}; use ruff_python_parser::lexer::LexResult; -use ruff_python_parser::{parse_tokens, AsMode, Mode}; +use ruff_python_parser::{parse_tokens, tokenize_all, AsMode, Mode}; use ruff_python_trivia::CommentRanges; use ruff_source_file::{Locator, SourceLocation}; use ruff_text_size::Ranged; @@ -272,7 +272,7 @@ struct ParsedModule<'a> { impl<'a> ParsedModule<'a> { fn from_source(source_code: &'a str) -> Result { - let tokens: Vec<_> = ruff_python_parser::lexer::lex(source_code, Mode::Module).collect(); + let tokens: Vec<_> = tokenize_all(source_code, Mode::Module); let mut comment_ranges = CommentRangesBuilder::default(); for (token, range) in tokens.iter().flatten() { From df617c3093f518554de675da542f25a6cab85b16 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 19 Jan 2024 11:58:31 -0500 Subject: [PATCH 2/5] [`flake8-blind-except`] Document exceptions to `blind-except` rule (#9580) Closes https://github.com/astral-sh/ruff/issues/9571. --- .../flake8_blind_except/rules/blind_except.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs index 102f40be9913e8..3eab03f29ab427 100644 --- a/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs +++ b/crates/ruff_linter/src/rules/flake8_blind_except/rules/blind_except.rs @@ -34,6 +34,25 @@ use crate::checkers::ast::Checker; /// ... /// ``` /// +/// Exceptions that are re-raised will _not_ be flagged, as they're expected to +/// be caught elsewhere: +/// ```python +/// try: +/// foo() +/// except BaseException: +/// raise +/// ``` +/// +/// Exceptions that are logged via `logging.exception()` or `logging.error()` +/// with `exc_info` enabled will _not_ be flagged, as this is a common pattern +/// for propagating exception traces: +/// ```python +/// try: +/// foo() +/// except BaseException: +/// logging.exception("Something went wrong") +/// ``` +/// /// ## References /// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement) /// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) From 866bea60a5de3c59d2537b0f3a634ae0ac9afd94 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 19 Jan 2024 12:54:39 -0500 Subject: [PATCH 3/5] Bump version to v0.1.14 (#9581) --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++ Cargo.lock | 6 ++-- README.md | 2 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_linter/Cargo.toml | 2 +- crates/ruff_shrinking/Cargo.toml | 2 +- docs/integrations.md | 6 ++-- pyproject.toml | 2 +- scripts/benchmarks/pyproject.toml | 2 +- 9 files changed, 60 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 884f73300713d7..72c8adf98671df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 0.1.14 + +### Preview features + +- \[`flake8-bugbear`\] Add fix for `duplicate-value` (`B033`) ([#9510](https://github.com/astral-sh/ruff/pull/9510)) +- \[`flake8-simplify`\] Implement `enumerate-for-loop` (`SIM113`) ([#7777](https://github.com/astral-sh/ruff/pull/7777)) +- \[`pygrep_hooks`\] Add fix for `deprecated-log-warn` (`PGH002`) ([#9519](https://github.com/astral-sh/ruff/pull/9519)) +- \[`pylint`\] Implement `import-private-name` (`C2701`) ([#5920](https://github.com/astral-sh/ruff/pull/5920)) +- \[`refurb`\] Implement `regex-flag-alias` with fix (`FURB167`) ([#9516](https://github.com/astral-sh/ruff/pull/9516)) +- \[`ruff`\] Add rule and fix to sort contents of `__all__` (`RUF022`) ([#9474](https://github.com/astral-sh/ruff/pull/9474)) +- \[`tryceratops`\] Add fix for `error-instead-of-exception` (`TRY400`) ([#9520](https://github.com/astral-sh/ruff/pull/9520)) + +### Rule changes + +- \[`flake8-pyi`\] Fix `PYI047` false negatives on PEP-695 type aliases ([#9566](https://github.com/astral-sh/ruff/pull/9566)) +- \[`flake8-pyi`\] Fix `PYI049` false negatives on call-based `TypedDict`s ([#9567](https://github.com/astral-sh/ruff/pull/9567)) +- \[`pylint`\] Exclude `self` and `cls` when counting method arguments (`PLR0917`) ([#9563](https://github.com/astral-sh/ruff/pull/9563)) + +### CLI + +- `--show-settings` displays active settings in a far more readable format ([#9464](https://github.com/astral-sh/ruff/pull/9464)) +- Add `--extension` support to the formatter ([#9483](https://github.com/astral-sh/ruff/pull/9483)) + +### Configuration + +- Ignore preview status for fixable and unfixable selectors ([#9538](https://github.com/astral-sh/ruff/pull/9538)) +- \[`pycodestyle`\] Use the configured tab size when expanding indents ([#9506](https://github.com/astral-sh/ruff/pull/9506)) + +### Bug fixes + +- Recursively visit deferred AST nodes ([#9541](https://github.com/astral-sh/ruff/pull/9541)) +- Visit deferred lambdas before type definitions ([#9540](https://github.com/astral-sh/ruff/pull/9540)) +- \[`flake8-simplify`\] Avoid some more `enumerate-for-loop` false positives (`SIM113`) ([#9515](https://github.com/astral-sh/ruff/pull/9515)) +- \[`pandas-vet`\] Limit inplace diagnostics to methods that accept inplace ([#9495](https://github.com/astral-sh/ruff/pull/9495)) +- \[`pylint`\] Add the `__prepare__` method to the list of recognized dunder method ([#9529](https://github.com/astral-sh/ruff/pull/9529)) +- \[`pylint`\] Ignore unnecessary dunder calls within dunder definitions ([#9496](https://github.com/astral-sh/ruff/pull/9496)) +- \[`refurb`\] Avoid bailing when `reimplemented-operator` is called on function (`FURB118`) ([#9556](https://github.com/astral-sh/ruff/pull/9556)) +- \[`ruff`\] Avoid treating named expressions as static keys (`RUF011`) ([#9494](https://github.com/astral-sh/ruff/pull/9494)) + +### Documentation + +- Add instructions on using `noqa` with isort rules ([#9555](https://github.com/astral-sh/ruff/pull/9555)) +- Documentation update for URL giving 'page not found' ([#9565](https://github.com/astral-sh/ruff/pull/9565)) +- Fix admonition in dark mode ([#9502](https://github.com/astral-sh/ruff/pull/9502)) +- Update contributing docs to use `cargo bench -p ruff_benchmark` ([#9535](https://github.com/astral-sh/ruff/pull/9535)) +- Update emacs integration section to include `emacs-ruff-format` ([#9403](https://github.com/astral-sh/ruff/pull/9403)) +- \[`flake8-blind-except`\] Document exceptions to `blind-except` rule ([#9580](https://github.com/astral-sh/ruff/pull/9580)) + ## 0.1.13 ### Bug fixes diff --git a/Cargo.lock b/Cargo.lock index 6c7bc8aab33afd..9f2a895c42a8a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2004,7 +2004,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "argfile", @@ -2164,7 +2164,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.1.13" +version = "0.1.14" dependencies = [ "aho-corasick", "annotate-snippets 0.9.2", @@ -2416,7 +2416,7 @@ dependencies = [ [[package]] name = "ruff_shrinking" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "clap", diff --git a/README.md b/README.md index f212a97a466514..4944febea70503 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.13 + rev: v0.1.14 hooks: # Run the linter. - id: ruff diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 8022529d89b484..66b1a9fdbf93f2 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.1.13" +version = "0.1.14" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_linter/Cargo.toml b/crates/ruff_linter/Cargo.toml index 1a1fe1445cc79d..13fe6fbd53747b 100644 --- a/crates/ruff_linter/Cargo.toml +++ b/crates/ruff_linter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_linter" -version = "0.1.13" +version = "0.1.14" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_shrinking/Cargo.toml b/crates/ruff_shrinking/Cargo.toml index 38caa432248c4b..b49893c13d4f26 100644 --- a/crates/ruff_shrinking/Cargo.toml +++ b/crates/ruff_shrinking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_shrinking" -version = "0.1.13" +version = "0.1.14" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/docs/integrations.md b/docs/integrations.md index 6cc40bb1cb585d..470bc0d550c022 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -14,7 +14,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.13 + rev: v0.1.14 hooks: # Run the linter. - id: ruff @@ -27,7 +27,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.13 + rev: v0.1.14 hooks: # Run the linter. - id: ruff @@ -41,7 +41,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.13 + rev: v0.1.14 hooks: # Run the linter. - id: ruff diff --git a/pyproject.toml b/pyproject.toml index 6fb603d938e947..18712b1dfb483b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.1.13" +version = "0.1.14" description = "An extremely fast Python linter and code formatter, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] readme = "README.md" diff --git a/scripts/benchmarks/pyproject.toml b/scripts/benchmarks/pyproject.toml index ff9e7c4f386043..db830a7ba39f06 100644 --- a/scripts/benchmarks/pyproject.toml +++ b/scripts/benchmarks/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scripts" -version = "0.1.13" +version = "0.1.14" description = "" authors = ["Charles Marsh "] From 49a445a23dc13c832b80f7463e787bdeb6b15117 Mon Sep 17 00:00:00 2001 From: Steve C Date: Sat, 20 Jan 2024 22:59:48 -0500 Subject: [PATCH 4/5] [`pylint`] Implement `potential-index-error` (`PLE0643`) (#9545) ## Summary add `potential-index-error` rule (`PLE0643`) See: #970 ## Test Plan `cargo test` --- .../fixtures/pylint/potential_index_error.py | 9 +++ .../src/checkers/ast/analyze/expression.rs | 3 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/rules/pylint/mod.rs | 1 + .../ruff_linter/src/rules/pylint/rules/mod.rs | 2 + .../pylint/rules/potential_index_error.rs | 73 +++++++++++++++++++ ...sts__PLE0643_potential_index_error.py.snap | 40 ++++++++++ ruff.schema.json | 2 + 8 files changed, 131 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pylint/potential_index_error.py create mode 100644 crates/ruff_linter/src/rules/pylint/rules/potential_index_error.rs create mode 100644 crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLE0643_potential_index_error.py.snap 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..7f1a0b47264d51 --- /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 7896a2f4f7c82a..75ee29c6ea5d00 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 d073f0223f0890..40bbdabb659a3a 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..0e4048c20e3fb0 --- /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 00000000000000..2cfa565dc5eb80 --- /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 ef61102357472a..9ea851bd26b54e 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3129,6 +3129,8 @@ "PLE060", "PLE0604", "PLE0605", + "PLE064", + "PLE0643", "PLE07", "PLE070", "PLE0704", From a42600e9a2830a994eff16efbd46b346c75c9e5f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sat, 20 Jan 2024 23:07:11 -0600 Subject: [PATCH 5/5] Always unset the `required-version` option during ecosystem checks (#9593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses our existing configuration overrides to unset the `required-version` option in ecosystem projects during checks. The downside to this approach, is we will now update the config file of _every_ project (with a config file). This roughly normalizes the configuration file, as we don't preserve comments and such. We could instead do a more targeted approach applying this override to projects which we know use this setting 🤷‍♀️ --- .../ruff-ecosystem/ruff_ecosystem/projects.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/python/ruff-ecosystem/ruff_ecosystem/projects.py b/python/ruff-ecosystem/ruff_ecosystem/projects.py index f34c17b08f817a..fc0139c7d1e140 100644 --- a/python/ruff-ecosystem/ruff_ecosystem/projects.py +++ b/python/ruff-ecosystem/ruff_ecosystem/projects.py @@ -50,6 +50,12 @@ def __post_init__(self): ) +ALWAYS_CONFIG_OVERRIDES = { + # Always unset the required version or we'll fail + "required-version": None +} + + @dataclass(frozen=True) class ConfigOverrides(Serializable): """ @@ -89,11 +95,15 @@ def patch_config( """ Temporarily patch the Ruff configuration file in the given directory. """ + dot_ruff_toml = dirpath / ".ruff.toml" ruff_toml = dirpath / "ruff.toml" pyproject_toml = dirpath / "pyproject.toml" # Prefer `ruff.toml` over `pyproject.toml` - if ruff_toml.exists(): + if dot_ruff_toml.exists(): + path = dot_ruff_toml + base = [] + elif ruff_toml.exists(): path = ruff_toml base = [] else: @@ -101,6 +111,7 @@ def patch_config( base = ["tool", "ruff"] overrides = { + **ALWAYS_CONFIG_OVERRIDES, **self.always, **(self.when_preview if preview else self.when_no_preview), } @@ -117,9 +128,17 @@ def patch_config( contents = None toml = {} + # Do not write a toml file if it does not exist and we're just nulling values + if all((value is None for value in overrides.values())): + yield + return + # Update the TOML, using `.` to descend into nested keys for key, value in overrides.items(): - logger.debug(f"Setting {key}={value!r} in {path}") + if value is not None: + logger.debug(f"Setting {key}={value!r} in {path}") + else: + logger.debug(f"Restoring {key} to default in {path}") target = toml names = base + key.split(".") @@ -127,7 +146,12 @@ def patch_config( if name not in target: target[name] = {} target = target[name] - target[names[-1]] = value + + if value is None: + # Remove null values i.e. restore to default + target.pop(names[-1], None) + else: + target[names[-1]] = value tomli_w.dump(toml, path.open("wb"))