Add pyflakes.extend-generics setting (#4677)

This commit is contained in:
Jonathan Plasse 2023-06-02 00:19:37 +02:00 committed by GitHub
parent 3180f9978a
commit edadd7814f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 156 additions and 8 deletions

View 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"]()

View file

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

View file

@ -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]) {

View file

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

View 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),
}
}
}

View file

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

View file

@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---

View file

@ -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),
}
}

View file

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

View file

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

View file

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

View file

@ -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()) {

View file

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

@ -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": {