From 0f54a62a0a030033c6c682f5c38cac30747611b9 Mon Sep 17 00:00:00 2001 From: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Date: Sat, 27 May 2023 11:43:09 +0200 Subject: [PATCH] Add pyflakes.extend-annotated-subscripts --- .../test/fixtures/pyflakes/F401_15.py | 9 ++++ crates/ruff/src/checkers/ast/mod.rs | 1 + crates/ruff/src/rules/pyflakes/mod.rs | 20 +++++++- .../ruff/src/rules/pyflakes/rules/imports.rs | 4 ++ crates/ruff/src/rules/pyflakes/settings.rs | 47 +++++++++++++++++++ ...les__pyflakes__tests__F401_F401_15.py.snap | 22 +++++++++ ...flakes__tests__extend_immutable_calls.snap | 4 ++ crates/ruff/src/settings/configuration.rs | 5 +- crates/ruff/src/settings/defaults.rs | 3 +- crates/ruff/src/settings/mod.rs | 4 +- crates/ruff/src/settings/options.rs | 5 +- .../src/analyze/typing.rs | 14 +++++- crates/ruff_wasm/src/lib.rs | 3 +- ruff.schema.json | 27 +++++++++++ 14 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyflakes/F401_15.py create mode 100644 crates/ruff/src/rules/pyflakes/settings.rs create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_15.py.snap create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__extend_immutable_calls.snap diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_15.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_15.py new file mode 100644 index 00000000000000..ac4b90b2c4cb42 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_15.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING +from django.db.models import ForeignKey + +if TYPE_CHECKING: + from pathlib import Path + + +class Foo: + var = ForeignKey["Path"]() diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index bcdc2f89f8d271..b655de38495a0e 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3846,6 +3846,7 @@ where value, &self.semantic_model, self.settings.typing_modules.iter().map(String::as_str), + &self.settings.pyflakes.extend_annotated_subscripts, ) { Some(subscript) => { match subscript { diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 052c759047e879..a2390ce4412c5a 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod cformat; pub(crate) mod fixes; pub(crate) mod format; pub(crate) mod rules; +pub mod settings; #[cfg(test)] mod tests { @@ -19,7 +20,7 @@ mod tests { use crate::linter::{check_path, LinterResult}; use crate::registry::{AsRule, Linter, Rule}; - use crate::settings::flags; + use crate::settings::{flags, Settings}; use crate::test::test_path; use crate::{assert_messages, directives, settings}; @@ -38,6 +39,7 @@ mod tests { #[test_case(Rule::UnusedImport, Path::new("F401_12.py"); "F401_12")] #[test_case(Rule::UnusedImport, Path::new("F401_13.py"); "F401_13")] #[test_case(Rule::UnusedImport, Path::new("F401_14.py"); "F401_14")] + #[test_case(Rule::UnusedImport, Path::new("F401_15.py"); "F401_15")] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"); "F402")] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"); "F403")] #[test_case(Rule::LateFutureImport, Path::new("F404.py"); "F404")] @@ -252,6 +254,22 @@ mod tests { Ok(()) } + #[test] + fn extend_annotated_subscripts() -> Result<()> { + let snapshot = "extend_immutable_calls".to_string(); + let diagnostics = test_path( + Path::new("pyflakes/F401_15.py"), + &Settings { + pyflakes: super::settings::Settings { + extend_annotated_subscripts: vec!["django.db.models.ForeignKey".to_string()], + }, + ..Settings::for_rules(vec![Rule::UnusedImport]) + }, + )?; + assert_messages!(snapshot, diagnostics); + Ok(()) + } + /// A re-implementation of the Pyflakes test runner. /// Note that all tests marked with `#[ignore]` should be considered TODOs. fn flakes(contents: &str, expected: &[Rule]) { diff --git a/crates/ruff/src/rules/pyflakes/rules/imports.rs b/crates/ruff/src/rules/pyflakes/rules/imports.rs index 373beefc6bb9da..09784dceba4e11 100644 --- a/crates/ruff/src/rules/pyflakes/rules/imports.rs +++ b/crates/ruff/src/rules/pyflakes/rules/imports.rs @@ -25,6 +25,10 @@ pub(crate) enum UnusedImportContext { /// If an import statement is used to check for the availability or existence /// of a module, consider using `importlib.util.find_spec` instead. /// +/// ## Options +/// +/// - `pyflakes.extend-annotated-subscripts` +/// /// ## Example /// ```python /// import numpy as np # unused import diff --git a/crates/ruff/src/rules/pyflakes/settings.rs b/crates/ruff/src/rules/pyflakes/settings.rs new file mode 100644 index 00000000000000..fde3a91b925401 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/settings.rs @@ -0,0 +1,47 @@ +//! Settings for the `flake8-bugbear` plugin. + +use serde::{Deserialize, Serialize}; + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; + +#[derive( + Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "PyflakesOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"[]"#, + value_type = "list[str]", + example = r#" + extend-annotated-subscripts = ["django.db.models.ForeignKey"] + "# + )] + /// Extend the list of annotated subscripts. + pub extend_annotated_subscripts: Option>, +} + +#[derive(Debug, Default, CacheKey)] +pub struct Settings { + pub extend_annotated_subscripts: Vec, +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + extend_annotated_subscripts: options.extend_annotated_subscripts.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + extend_annotated_subscripts: Some(settings.extend_annotated_subscripts), + } + } +} diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_15.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_15.py.snap new file mode 100644 index 00000000000000..0110ad1ae74cb1 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_15.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +F401_15.py:5:25: F401 [*] `pathlib.Path` imported but unused + | +5 | if TYPE_CHECKING: +6 | from pathlib import Path + | ^^^^ F401 + | + = help: Remove unused import: `pathlib.Path` + +ℹ Suggested fix +2 2 | from django.db.models import ForeignKey +3 3 | +4 4 | if TYPE_CHECKING: +5 |- from pathlib import Path + 5 |+ pass +6 6 | +7 7 | +8 8 | class Foo: + + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__extend_immutable_calls.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__extend_immutable_calls.snap new file mode 100644 index 00000000000000..1976c4331d419a --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__extend_immutable_calls.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 86212fd45e17f3..16acabe30a6c6a 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -19,7 +19,7 @@ use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, + flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -89,6 +89,7 @@ pub struct Configuration { pub pep8_naming: Option, pub pycodestyle: Option, pub pydocstyle: Option, + pub pyflakes: Option, pub pylint: Option, } @@ -241,6 +242,7 @@ impl Configuration { pep8_naming: options.pep8_naming, pycodestyle: options.pycodestyle, pydocstyle: options.pydocstyle, + pyflakes: options.pyflakes, pylint: options.pylint, }) } @@ -326,6 +328,7 @@ impl Configuration { pep8_naming: self.pep8_naming.combine(config.pep8_naming), pycodestyle: self.pycodestyle.combine(config.pycodestyle), pydocstyle: self.pydocstyle.combine(config.pydocstyle), + pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), } } diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 80ae6a09563263..aa5007789d58dd 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -13,7 +13,7 @@ use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, + flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::types::FilePatternSet; @@ -110,6 +110,7 @@ impl Default for Settings { pep8_naming: pep8_naming::settings::Settings::default(), pycodestyle: pycodestyle::settings::Settings::default(), pydocstyle: pydocstyle::settings::Settings::default(), + pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), } } diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 2f31b6d2215215..453a93dac53713 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -19,7 +19,7 @@ use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, + flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -126,6 +126,7 @@ pub struct Settings { pub pep8_naming: pep8_naming::settings::Settings, pub pycodestyle: pycodestyle::settings::Settings, pub pydocstyle: pydocstyle::settings::Settings, + pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, } @@ -231,6 +232,7 @@ impl Settings { pep8_naming: config.pep8_naming.map(Into::into).unwrap_or_default(), pycodestyle: config.pycodestyle.map(Into::into).unwrap_or_default(), pydocstyle: config.pydocstyle.map(Into::into).unwrap_or_default(), + pyflakes: config.pyflakes.map(Into::into).unwrap_or_default(), pylint: config.pylint.map(Into::into).unwrap_or_default(), }) } diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index 5904adcc39f349..9c349ea561a76f 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -11,7 +11,7 @@ use crate::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, + flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -535,6 +535,9 @@ pub struct Options { /// Options for the `pydocstyle` plugin. pub pydocstyle: Option, #[option_group] + /// Options for the `pyflakes` plugin. + pub pyflakes: Option, + #[option_group] /// Options for the `pylint` plugin. pub pylint: Option, // Tables are required to go last. diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 8bbde2990b1018..15016a1c74ce49 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1,6 +1,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator}; -use ruff_python_ast::call_path::{from_unqualified_name, CallPath}; +use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; use ruff_python_stdlib::typing::{ IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS, }; @@ -28,13 +28,23 @@ pub fn match_annotated_subscript<'a>( expr: &Expr, model: &SemanticModel, typing_modules: impl Iterator, + extend_annotated_subscripts: &[String], ) -> Option { if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) { return None; } model.resolve_call_path(expr).and_then(|call_path| { - if SUBSCRIPTS.contains(&call_path.as_slice()) { + let extend_annotated_subscripts: Vec = extend_annotated_subscripts + .iter() + .map(|target| from_qualified_name(target)) + .collect(); + + if SUBSCRIPTS.contains(&call_path.as_slice()) + || extend_annotated_subscripts + .iter() + .any(|target| call_path == *target) + { return Some(SubscriptKind::AnnotatedSubscript); } if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) { diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 9aeb656786726c..84566771dd9cb1 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -12,7 +12,7 @@ use ruff::rules::{ flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, - flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, + flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; @@ -158,6 +158,7 @@ pub fn defaultSettings() -> Result { pep8_naming: Some(pep8_naming::settings::Settings::default().into()), pycodestyle: Some(pycodestyle::settings::Settings::default().into()), pydocstyle: Some(pydocstyle::settings::Settings::default().into()), + pyflakes: Some(pyflakes::settings::Settings::default().into()), pylint: Some(pylint::settings::Settings::default().into()), })?) } diff --git a/ruff.schema.json b/ruff.schema.json index d3547d8c391fb4..3000fbfd218566 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -442,6 +442,17 @@ } ] }, + "pyflakes": { + "description": "Options for the `pyflakes` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyflakesOptions" + }, + { + "type": "null" + } + ] + }, "pylint": { "description": "Options for the `pylint` plugin.", "anyOf": [ @@ -1429,6 +1440,22 @@ }, "additionalProperties": false }, + "PyflakesOptions": { + "type": "object", + "properties": { + "extend-annotated-subscripts": { + "description": "Extend the list of annotated subscripts.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "PylintOptions": { "type": "object", "properties": {