mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:49:50 +00:00
Add pyflakes.extend-generics
setting (#4677)
This commit is contained in:
parent
3180f9978a
commit
edadd7814f
14 changed files with 156 additions and 8 deletions
9
crates/ruff/resources/test/fixtures/pyflakes/F401_15.py
vendored
Normal file
9
crates/ruff/resources/test/fixtures/pyflakes/F401_15.py
vendored
Normal file
|
@ -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"]()
|
|
@ -3884,6 +3884,7 @@ where
|
|||
value,
|
||||
&self.semantic_model,
|
||||
self.settings.typing_modules.iter().map(String::as_str),
|
||||
&self.settings.pyflakes.extend_generics,
|
||||
) {
|
||||
Some(subscript) => {
|
||||
match subscript {
|
||||
|
|
|
@ -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"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_13.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_15.py"))]
|
||||
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
|
||||
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
|
||||
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
|
||||
|
@ -252,6 +254,22 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_generics() -> Result<()> {
|
||||
let snapshot = "extend_immutable_calls".to_string();
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyflakes/F401_15.py"),
|
||||
&Settings {
|
||||
pyflakes: super::settings::Settings {
|
||||
extend_generics: 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]) {
|
||||
|
|
|
@ -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-generics`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import numpy as np # unused import
|
||||
|
|
47
crates/ruff/src/rules/pyflakes/settings.rs
Normal file
47
crates/ruff/src/rules/pyflakes/settings.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
//! Settings for the `Pyflakes` 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 = "extend-generics = [\"django.db.models.ForeignKey\"]"
|
||||
)]
|
||||
/// Additional functions or classes to consider generic, such that any
|
||||
/// subscripts should be treated as type annotation (e.g., `ForeignKey` in
|
||||
/// `django.db.models.ForeignKey["User"]`.
|
||||
pub extend_generics: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, CacheKey)]
|
||||
pub struct Settings {
|
||||
pub extend_generics: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<Options> for Settings {
|
||||
fn from(options: Options) -> Self {
|
||||
Self {
|
||||
extend_generics: options.extend_generics.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Settings> for Options {
|
||||
fn from(settings: Settings) -> Self {
|
||||
Self {
|
||||
extend_generics: Some(settings.extend_generics),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/pyflakes/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::options::Options;
|
||||
use crate::settings::types::{
|
||||
|
@ -89,6 +89,7 @@ pub struct Configuration {
|
|||
pub pep8_naming: Option<pep8_naming::settings::Options>,
|
||||
pub pycodestyle: Option<pycodestyle::settings::Options>,
|
||||
pub pydocstyle: Option<pydocstyle::settings::Options>,
|
||||
pub pyflakes: Option<pyflakes::settings::Options>,
|
||||
pub pylint: Option<pylint::settings::Options>,
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -539,6 +539,9 @@ pub struct Options {
|
|||
/// Options for the `pydocstyle` plugin.
|
||||
pub pydocstyle: Option<pydocstyle::settings::Options>,
|
||||
#[option_group]
|
||||
/// Options for the `pyflakes` plugin.
|
||||
pub pyflakes: Option<pyflakes::settings::Options>,
|
||||
#[option_group]
|
||||
/// Options for the `pylint` plugin.
|
||||
pub pylint: Option<pylint::settings::Options>,
|
||||
// Tables are required to go last.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use rustpython_parser::ast::{self, Constant, Expr, Operator};
|
||||
|
||||
use num_traits::identities::Zero;
|
||||
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,
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ pub fn match_annotated_subscript<'a>(
|
|||
expr: &Expr,
|
||||
semantic_model: &SemanticModel,
|
||||
typing_modules: impl Iterator<Item = &'a str>,
|
||||
extend_generics: &[String],
|
||||
) -> Option<SubscriptKind> {
|
||||
if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) {
|
||||
return None;
|
||||
|
@ -37,7 +38,12 @@ pub fn match_annotated_subscript<'a>(
|
|||
semantic_model
|
||||
.resolve_call_path(expr)
|
||||
.and_then(|call_path| {
|
||||
if SUBSCRIPTS.contains(&call_path.as_slice()) {
|
||||
if SUBSCRIPTS.contains(&call_path.as_slice())
|
||||
|| extend_generics
|
||||
.iter()
|
||||
.map(|target| from_qualified_name(target))
|
||||
.any(|target| call_path == target)
|
||||
{
|
||||
return Some(SubscriptKind::AnnotatedSubscript);
|
||||
}
|
||||
if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) {
|
||||
|
|
|
@ -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<JsValue, JsValue> {
|
|||
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()),
|
||||
})?)
|
||||
}
|
||||
|
|
27
ruff.schema.json
generated
27
ruff.schema.json
generated
|
@ -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-generics": {
|
||||
"description": "Additional functions or classes to consider generic, such that any subscripts should be treated as type annotation (e.g., `ForeignKey` in `django.db.models.ForeignKey[\"User\"]`.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PylintOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue