diff --git a/README.md b/README.md index 986159cc7b..eaf741712b 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,9 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu 8. [flake8-builtins](#flake8-builtins) 9. [flake8-print](#flake8-print) 10. [flake8-quotes](#flake8-quotes) - 11. [Ruff-specific rules](#ruff-specific-rules) - 12. [Meta rules](#meta-rules) + 11. [flake8-annotations](#flake8-annotations) + 12. [Ruff-specific rules](#ruff-specific-rules) + 13. [Meta rules](#meta-rules) 5. [Editor Integrations](#editor-integrations) 6. [FAQ](#faq) 7. [Development](#development) @@ -523,6 +524,23 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on | Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | | | Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | | +### flake8-annotations + +For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2.9.1/) on PyPI. + +| Code | Name | Message | Fix | +| ---- | ---- | ------- | --- | +| ANN001 | MissingTypeFunctionArgument | Missing type annotation for function argument | | +| ANN002 | MissingTypeArgs | Missing type annotation for `*args` | | +| ANN003 | MissingTypeKwargs | Missing type annotation for `**kwargs` | | +| ANN101 | MissingTypeSelf | Missing type annotation for `self` in method | | +| ANN102 | MissingTypeCls | Missing type annotation for `cls` in classmethod | | +| ANN201 | MissingReturnTypePublicFunction | Missing return type annotation for public function | | +| ANN202 | MissingReturnTypePrivateFunction | Missing return type annotation for private function | | +| ANN204 | MissingReturnTypeMagicMethod | Missing return type annotation for magic method | | +| ANN205 | MissingReturnTypeStaticMethod | Missing return type annotation for staticmethod | | +| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod | | + ### Ruff-specific rules | Code | Name | Message | Fix | @@ -605,6 +623,7 @@ including: - [`flake8-super`](https://pypi.org/project/flake8-super/) - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/) +- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/) - [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) - [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (11/34) @@ -627,6 +646,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [`flake8-super`](https://pypi.org/project/flake8-super/) - [`flake8-print`](https://pypi.org/project/flake8-print/) - [`flake8-quotes`](https://pypi.org/project/flake8-quotes/) +- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/) - [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) - [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32) diff --git a/flake8_to_ruff/src/converter.rs b/flake8_to_ruff/src/converter.rs index 61dd6bc2f8..39f424d4b0 100644 --- a/flake8_to_ruff/src/converter.rs +++ b/flake8_to_ruff/src/converter.rs @@ -4,7 +4,7 @@ use anyhow::Result; use ruff::flake8_quotes::settings::Quote; use ruff::settings::options::Options; use ruff::settings::pyproject::Pyproject; -use ruff::{flake8_quotes, pep8_naming}; +use ruff::{flake8_annotations, flake8_quotes, pep8_naming}; use crate::plugin::Plugin; use crate::{parser, plugin}; @@ -39,6 +39,7 @@ pub fn convert( // Parse each supported option. let mut options: Options = Default::default(); + let mut flake8_annotations: flake8_annotations::settings::Options = Default::default(); let mut flake8_quotes: flake8_quotes::settings::Options = Default::default(); let mut pep8_naming: pep8_naming::settings::Options = Default::default(); for (key, value) in flake8 { @@ -78,6 +79,25 @@ pub fn convert( Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), } } + // flake8-annotations + "suppress-none-returning" | "suppress_none_returning" => { + match parser::parse_bool(value.as_ref()) { + Ok(bool) => flake8_annotations.suppress_none_returning = Some(bool), + Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), + } + } + "suppress-dummy-args" | "suppress_dummy_args" => { + match parser::parse_bool(value.as_ref()) { + Ok(bool) => flake8_annotations.suppress_dummy_args = Some(bool), + Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), + } + } + "mypy-init-return" | "mypy_init_return" => { + match parser::parse_bool(value.as_ref()) { + Ok(bool) => flake8_annotations.mypy_init_return = Some(bool), + Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), + } + } // flake8-quotes "quotes" | "inline-quotes" | "inline_quotes" => match value.trim() { "'" | "single" => flake8_quotes.inline_quotes = Some(Quote::Single), @@ -94,10 +114,9 @@ pub fn convert( "\"" | "double" => flake8_quotes.docstring_quotes = Some(Quote::Single), _ => eprintln!("Unexpected '{key}' value: {value}"), }, - "avoid-escape" | "avoid_escape" => match value.trim() { - "true" => flake8_quotes.avoid_escape = Some(true), - "false" => flake8_quotes.avoid_escape = Some(false), - _ => eprintln!("Unexpected '{key}' value: {value}"), + "avoid-escape" | "avoid_escape" => match parser::parse_bool(value.as_ref()) { + Ok(bool) => flake8_quotes.avoid_escape = Some(bool), + Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), }, // pep8-naming "ignore-names" | "ignore_names" => { @@ -166,6 +185,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }); @@ -195,6 +215,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }); @@ -224,6 +245,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }); @@ -253,6 +275,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }); @@ -282,6 +305,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(flake8_quotes::settings::Quote::Single), multiline_quotes: None, @@ -354,6 +378,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }); @@ -384,6 +409,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(flake8_quotes::settings::Quote::Single), multiline_quotes: None, diff --git a/flake8_to_ruff/src/parser.rs b/flake8_to_ruff/src/parser.rs index a592655f18..5392239a70 100644 --- a/flake8_to_ruff/src/parser.rs +++ b/flake8_to_ruff/src/parser.rs @@ -37,6 +37,15 @@ pub fn parse_strings(value: &str) -> Vec { .collect() } +/// Parse a boolean. +pub fn parse_bool(value: &str) -> Result { + match value.trim() { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(anyhow::anyhow!("Unexpected boolean value: {value}")), + } +} + #[derive(Debug)] struct Token { token_name: TokenType, diff --git a/flake8_to_ruff/src/plugin.rs b/flake8_to_ruff/src/plugin.rs index 167bb70151..bcf2713a39 100644 --- a/flake8_to_ruff/src/plugin.rs +++ b/flake8_to_ruff/src/plugin.rs @@ -12,6 +12,7 @@ pub enum Plugin { Flake8Docstrings, Flake8Print, Flake8Quotes, + Flake8Annotations, PEP8Naming, Pyupgrade, } @@ -27,6 +28,7 @@ impl FromStr for Plugin { "flake8-docstrings" => Ok(Plugin::Flake8Docstrings), "flake8-print" => Ok(Plugin::Flake8Print), "flake8-quotes" => Ok(Plugin::Flake8Quotes), + "flake8-annotations" => Ok(Plugin::Flake8Annotations), "pep8-naming" => Ok(Plugin::PEP8Naming), "pyupgrade" => Ok(Plugin::Pyupgrade), _ => Err(anyhow!("Unknown plugin: {}", string)), @@ -58,6 +60,7 @@ impl Plugin { } Plugin::Flake8Print => vec![CheckCodePrefix::T], Plugin::Flake8Quotes => vec![CheckCodePrefix::Q], + Plugin::Flake8Annotations => vec![CheckCodePrefix::ANN], Plugin::PEP8Naming => vec![CheckCodePrefix::N], Plugin::Pyupgrade => vec![CheckCodePrefix::U], } @@ -262,6 +265,31 @@ pub fn infer_plugins(flake8: &HashMap>) -> Vec { "avoid-escape" | "avoid_escape" => { plugins.insert(Plugin::Flake8Quotes); } + // flake8-annotations + "suppress-none-returning" | "suppress_none_returning" => { + plugins.insert(Plugin::Flake8Annotations); + } + "suppress-dummy-args" | "suppress_dummy_args" => { + plugins.insert(Plugin::Flake8Annotations); + } + "allow-untyped-defs" | "allow_untyped_defs" => { + plugins.insert(Plugin::Flake8Annotations); + } + "allow-untyped-nested" | "allow_untyped_nested" => { + plugins.insert(Plugin::Flake8Annotations); + } + "mypy-init-return" | "mypy_init_return" => { + plugins.insert(Plugin::Flake8Annotations); + } + "dispatch-decorators" | "dispatch_decorators" => { + plugins.insert(Plugin::Flake8Annotations); + } + "overload-decorators" | "overload_decorators" => { + plugins.insert(Plugin::Flake8Annotations); + } + "allow-star-arg-any" | "allow_star_arg_any" => { + plugins.insert(Plugin::Flake8Annotations); + } // pep8-naming "ignore-names" | "ignore_names" => { plugins.insert(Plugin::PEP8Naming); diff --git a/foo.py b/foo.py new file mode 100644 index 0000000000..98c1818844 --- /dev/null +++ b/foo.py @@ -0,0 +1,5 @@ +def f(): + class A: + return 1 + +f() diff --git a/resources/test/fixtures/flake8_annotations/annotation_presence.py b/resources/test/fixtures/flake8_annotations/annotation_presence.py new file mode 100644 index 0000000000..2c880f32ff --- /dev/null +++ b/resources/test/fixtures/flake8_annotations/annotation_presence.py @@ -0,0 +1,33 @@ +# Error +def foo(a, b): + pass + + +# Error +def foo(a: int, b): + pass + + +# Error +def foo(a: int, b) -> int: + pass + + +# Error +def foo(a: int, b: int): + pass + + +# Error +def foo(): + pass + + +# OK +def foo(a: int, b: int) -> int: + pass + + +# OK +def foo() -> int: + pass diff --git a/resources/test/fixtures/flake8_annotations/mypy_init_return.py b/resources/test/fixtures/flake8_annotations/mypy_init_return.py new file mode 100644 index 0000000000..18f9d06587 --- /dev/null +++ b/resources/test/fixtures/flake8_annotations/mypy_init_return.py @@ -0,0 +1,41 @@ +"""Test case expected to be run with `mypy_init_return = True`.""" + +# Error +class Foo: + def __init__(self): + ... + + +# Error +class Foo: + def __init__(self, foo): + ... + + +# OK +class Foo: + def __init__(self, foo) -> None: + ... + + +# OK +class Foo: + def __init__(self) -> None: + ... + + +# OK +class Foo: + def __init__(self, foo: int): + ... + + +# OK +class Foo: + def __init__(self, foo: int) -> None: + ... + + +# Error +def __init__(self, foo: int): + ... diff --git a/resources/test/fixtures/flake8_annotations/suppress_dummy_args.py b/resources/test/fixtures/flake8_annotations/suppress_dummy_args.py new file mode 100644 index 0000000000..01358120c2 --- /dev/null +++ b/resources/test/fixtures/flake8_annotations/suppress_dummy_args.py @@ -0,0 +1,26 @@ +"""Test case expected to be run with `suppress_dummy_args = True`.""" + +# OK +def foo(_) -> None: + ... + + +# OK +def foo(*_) -> None: + ... + + +# OK +def foo(**_) -> None: + ... + + +# OK +def foo(a: int, _) -> None: + ... + + +# OK +def foo() -> None: + def bar(_) -> None: + ... diff --git a/resources/test/fixtures/flake8_annotations/suppress_none_returning.py b/resources/test/fixtures/flake8_annotations/suppress_none_returning.py new file mode 100644 index 0000000000..80e016a636 --- /dev/null +++ b/resources/test/fixtures/flake8_annotations/suppress_none_returning.py @@ -0,0 +1,55 @@ +"""Test case expected to be run with `suppress_none_returning = True`.""" + +# OK +def foo(): + a = 2 + 2 + + +# OK +def foo(): + return + + +# OK +def foo(): + return None + + +# OK +def foo(): + a = 2 + 2 + if a == 4: + return + else: + return + + +# OK +def foo(): + a = 2 + 2 + if a == 4: + return None + else: + return + + +# OK +def foo(): + def bar() -> bool: + return True + + bar() + + +# Error +def foo(): + return True + + +# Error +def foo(): + a = 2 + 2 + if a == 4: + return True + else: + return diff --git a/src/check_ast.rs b/src/check_ast.rs index de7a08cf9b..cfa266b251 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -32,8 +32,8 @@ use crate::settings::Settings; use crate::source_code_locator::SourceCodeLocator; use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope}; use crate::{ - docstrings, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pyupgrade, + docstrings, flake8_annotations, flake8_bugbear, flake8_builtins, flake8_comprehensions, + flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade, }; const GLOBAL_SCOPE_INDEX: usize = 0; @@ -57,8 +57,8 @@ pub struct Checker<'a> { pub(crate) locator: &'a SourceCodeLocator<'a>, // Computed checks. checks: Vec, - // Docstring tracking. - docstrings: Vec<(Definition<'a>, Visibility)>, + // Function and class definition tracking (e.g., for docstring enforcement). + definitions: Vec<(Definition<'a>, Visibility)>, // Edit tracking. // TODO(charlie): Instead of exposing deletions, wrap in a public API. pub(crate) deletions: BTreeSet, @@ -100,7 +100,7 @@ impl<'a> Checker<'a> { path, locator, checks: Default::default(), - docstrings: Default::default(), + definitions: Default::default(), deletions: Default::default(), parents: Default::default(), parent_stack: Default::default(), @@ -834,7 +834,8 @@ where &Documentable::Function, ); let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Function); - self.docstrings.push((definition, scope.visibility.clone())); + self.definitions + .push((definition, scope.visibility.clone())); self.visible_scope = scope; self.deferred_functions.push(( @@ -852,7 +853,8 @@ where &Documentable::Class, ); let scope = transition_scope(&self.visible_scope, stmt, &Documentable::Class); - self.docstrings.push((definition, scope.visibility.clone())); + self.definitions + .push((definition, scope.visibility.clone())); self.visible_scope = scope; for stmt in body { @@ -2105,7 +2107,7 @@ impl<'a> Checker<'a> { 'b: 'a, { let docstring = docstrings::extraction::docstring_from(python_ast); - self.docstrings.push(( + self.definitions.push(( Definition { kind: if self.path.ends_with("__init__.py") { DefinitionKind::Package @@ -2354,68 +2356,86 @@ impl<'a> Checker<'a> { } } - fn check_docstrings(&mut self) { - while let Some((docstring, visibility)) = self.docstrings.pop() { - if !pydocstyle::plugins::not_empty(self, &docstring) { + fn check_definitions(&mut self) { + while let Some((definition, visibility)) = self.definitions.pop() { + // flake8-annotations + if self.settings.enabled.contains(&CheckCode::ANN001) + || self.settings.enabled.contains(&CheckCode::ANN002) + || self.settings.enabled.contains(&CheckCode::ANN003) + || self.settings.enabled.contains(&CheckCode::ANN201) + || self.settings.enabled.contains(&CheckCode::ANN202) + || self.settings.enabled.contains(&CheckCode::ANN001) + || self.settings.enabled.contains(&CheckCode::ANN002) + || self.settings.enabled.contains(&CheckCode::ANN003) + || self.settings.enabled.contains(&CheckCode::ANN101) + || self.settings.enabled.contains(&CheckCode::ANN102) + || self.settings.enabled.contains(&CheckCode::ANN201) + || self.settings.enabled.contains(&CheckCode::ANN202) + { + flake8_annotations::plugins::definition(self, &definition, &visibility); + } + + // pydocstyle + if !pydocstyle::plugins::not_empty(self, &definition) { continue; } - if !pydocstyle::plugins::not_missing(self, &docstring, &visibility) { + if !pydocstyle::plugins::not_missing(self, &definition, &visibility) { continue; } if self.settings.enabled.contains(&CheckCode::D200) { - pydocstyle::plugins::one_liner(self, &docstring); + pydocstyle::plugins::one_liner(self, &definition); } if self.settings.enabled.contains(&CheckCode::D201) || self.settings.enabled.contains(&CheckCode::D202) { - pydocstyle::plugins::blank_before_after_function(self, &docstring); + pydocstyle::plugins::blank_before_after_function(self, &definition); } if self.settings.enabled.contains(&CheckCode::D203) || self.settings.enabled.contains(&CheckCode::D204) || self.settings.enabled.contains(&CheckCode::D211) { - pydocstyle::plugins::blank_before_after_class(self, &docstring); + pydocstyle::plugins::blank_before_after_class(self, &definition); } if self.settings.enabled.contains(&CheckCode::D205) { - pydocstyle::plugins::blank_after_summary(self, &docstring); + pydocstyle::plugins::blank_after_summary(self, &definition); } if self.settings.enabled.contains(&CheckCode::D206) || self.settings.enabled.contains(&CheckCode::D207) || self.settings.enabled.contains(&CheckCode::D208) { - pydocstyle::plugins::indent(self, &docstring); + pydocstyle::plugins::indent(self, &definition); } if self.settings.enabled.contains(&CheckCode::D209) { - pydocstyle::plugins::newline_after_last_paragraph(self, &docstring); + pydocstyle::plugins::newline_after_last_paragraph(self, &definition); } if self.settings.enabled.contains(&CheckCode::D210) { - pydocstyle::plugins::no_surrounding_whitespace(self, &docstring); + pydocstyle::plugins::no_surrounding_whitespace(self, &definition); } if self.settings.enabled.contains(&CheckCode::D212) || self.settings.enabled.contains(&CheckCode::D213) { - pydocstyle::plugins::multi_line_summary_start(self, &docstring); + pydocstyle::plugins::multi_line_summary_start(self, &definition); } if self.settings.enabled.contains(&CheckCode::D300) { - pydocstyle::plugins::triple_quotes(self, &docstring); + pydocstyle::plugins::triple_quotes(self, &definition); } if self.settings.enabled.contains(&CheckCode::D400) { - pydocstyle::plugins::ends_with_period(self, &docstring); + pydocstyle::plugins::ends_with_period(self, &definition); } if self.settings.enabled.contains(&CheckCode::D402) { - pydocstyle::plugins::no_signature(self, &docstring); + pydocstyle::plugins::no_signature(self, &definition); } if self.settings.enabled.contains(&CheckCode::D403) { - pydocstyle::plugins::capitalized(self, &docstring); + pydocstyle::plugins::capitalized(self, &definition); } if self.settings.enabled.contains(&CheckCode::D404) { - pydocstyle::plugins::starts_with_this(self, &docstring); + pydocstyle::plugins::starts_with_this(self, &definition); } if self.settings.enabled.contains(&CheckCode::D415) { - pydocstyle::plugins::ends_with_punctuation(self, &docstring); + pydocstyle::plugins::ends_with_punctuation(self, &definition); } if self.settings.enabled.contains(&CheckCode::D418) { - pydocstyle::plugins::if_needed(self, &docstring); + pydocstyle::plugins::if_needed(self, &definition); } if self.settings.enabled.contains(&CheckCode::D212) || self.settings.enabled.contains(&CheckCode::D214) @@ -2433,7 +2453,7 @@ impl<'a> Checker<'a> { || self.settings.enabled.contains(&CheckCode::D416) || self.settings.enabled.contains(&CheckCode::D417) { - pydocstyle::plugins::sections(self, &docstring); + pydocstyle::plugins::sections(self, &definition); } } } @@ -2512,7 +2532,7 @@ pub fn check_ast( checker.check_dead_scopes(); // Check docstrings. - checker.check_docstrings(); + checker.check_definitions(); checker.checks } diff --git a/src/checks.rs b/src/checks.rs index 8ebfcaf577..8a8671a30f 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -115,6 +115,17 @@ pub enum CheckCode { Q001, Q002, Q003, + // flake8-annotations + ANN001, + ANN002, + ANN003, + ANN101, + ANN102, + ANN201, + ANN202, + ANN204, + ANN205, + ANN206, // pyupgrade U001, U002, @@ -205,6 +216,7 @@ pub enum CheckCategory { Flake8Builtins, Flake8Print, Flake8Quotes, + Flake8Annotations, Ruff, Meta, } @@ -219,6 +231,7 @@ impl CheckCategory { CheckCategory::Flake8Comprehensions => "flake8-comprehensions", CheckCategory::Flake8Print => "flake8-print", CheckCategory::Flake8Quotes => "flake8-quotes", + CheckCategory::Flake8Annotations => "flake8-annotations", CheckCategory::Pyupgrade => "pyupgrade", CheckCategory::Pydocstyle => "pydocstyle", CheckCategory::PEP8Naming => "pep8-naming", @@ -242,6 +255,9 @@ impl CheckCategory { } CheckCategory::Flake8Print => Some("https://pypi.org/project/flake8-print/5.0.0/"), CheckCategory::Flake8Quotes => Some("https://pypi.org/project/flake8-quotes/3.3.1/"), + CheckCategory::Flake8Annotations => { + Some("https://pypi.org/project/flake8-annotations/2.9.1/") + } CheckCategory::Pyupgrade => Some("https://pypi.org/project/pyupgrade/3.2.0/"), CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"), CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"), @@ -357,6 +373,17 @@ pub enum CheckKind { BadQuotesMultilineString(Quote), BadQuotesDocstring(Quote), AvoidQuoteEscape, + // flake8-annotations + MissingTypeFunctionArgument, + MissingTypeArgs, + MissingTypeKwargs, + MissingTypeSelf, + MissingTypeCls, + MissingReturnTypePublicFunction, + MissingReturnTypePrivateFunction, + MissingReturnTypeMagicMethod, + MissingReturnTypeStaticMethod, + MissingReturnTypeClassMethod, // pyupgrade TypeOfPrimitive(Primitive), UnnecessaryAbspath, @@ -565,6 +592,17 @@ impl CheckCode { CheckCode::Q001 => CheckKind::BadQuotesMultilineString(Quote::Double), CheckCode::Q002 => CheckKind::BadQuotesDocstring(Quote::Double), CheckCode::Q003 => CheckKind::AvoidQuoteEscape, + // flake8-annotations + CheckCode::ANN001 => CheckKind::MissingTypeFunctionArgument, + CheckCode::ANN002 => CheckKind::MissingTypeArgs, + CheckCode::ANN003 => CheckKind::MissingTypeKwargs, + CheckCode::ANN101 => CheckKind::MissingTypeSelf, + CheckCode::ANN102 => CheckKind::MissingTypeCls, + CheckCode::ANN201 => CheckKind::MissingReturnTypePublicFunction, + CheckCode::ANN202 => CheckKind::MissingReturnTypePrivateFunction, + CheckCode::ANN204 => CheckKind::MissingReturnTypeMagicMethod, + CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod, + CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod, // pyupgrade CheckCode::U001 => CheckKind::UselessMetaclassType, CheckCode::U002 => CheckKind::UnnecessaryAbspath, @@ -747,6 +785,16 @@ impl CheckCode { CheckCode::Q001 => CheckCategory::Flake8Quotes, CheckCode::Q002 => CheckCategory::Flake8Quotes, CheckCode::Q003 => CheckCategory::Flake8Quotes, + CheckCode::ANN001 => CheckCategory::Flake8Annotations, + CheckCode::ANN002 => CheckCategory::Flake8Annotations, + CheckCode::ANN003 => CheckCategory::Flake8Annotations, + CheckCode::ANN101 => CheckCategory::Flake8Annotations, + CheckCode::ANN102 => CheckCategory::Flake8Annotations, + CheckCode::ANN201 => CheckCategory::Flake8Annotations, + CheckCode::ANN202 => CheckCategory::Flake8Annotations, + CheckCode::ANN204 => CheckCategory::Flake8Annotations, + CheckCode::ANN205 => CheckCategory::Flake8Annotations, + CheckCode::ANN206 => CheckCategory::Flake8Annotations, CheckCode::U001 => CheckCategory::Pyupgrade, CheckCode::U002 => CheckCategory::Pyupgrade, CheckCode::U003 => CheckCategory::Pyupgrade, @@ -915,6 +963,17 @@ impl CheckKind { CheckKind::BadQuotesMultilineString(_) => &CheckCode::Q001, CheckKind::BadQuotesDocstring(_) => &CheckCode::Q002, CheckKind::AvoidQuoteEscape => &CheckCode::Q003, + // flake8-annotations + CheckKind::MissingTypeFunctionArgument => &CheckCode::ANN001, + CheckKind::MissingTypeArgs => &CheckCode::ANN002, + CheckKind::MissingTypeKwargs => &CheckCode::ANN003, + CheckKind::MissingTypeSelf => &CheckCode::ANN101, + CheckKind::MissingTypeCls => &CheckCode::ANN102, + CheckKind::MissingReturnTypePublicFunction => &CheckCode::ANN201, + CheckKind::MissingReturnTypePrivateFunction => &CheckCode::ANN202, + CheckKind::MissingReturnTypeMagicMethod => &CheckCode::ANN204, + CheckKind::MissingReturnTypeStaticMethod => &CheckCode::ANN205, + CheckKind::MissingReturnTypeClassMethod => &CheckCode::ANN206, // pyupgrade CheckKind::TypeOfPrimitive(_) => &CheckCode::U003, CheckKind::UnnecessaryAbspath => &CheckCode::U002, @@ -1300,6 +1359,33 @@ impl CheckKind { CheckKind::AvoidQuoteEscape => { "Change outer quotes to avoid escaping inner quotes".to_string() } + // flake8-annotations + CheckKind::MissingTypeFunctionArgument => { + "Missing type annotation for function argument".to_string() + } + CheckKind::MissingTypeArgs => "Missing type annotation for `*args`".to_string(), + CheckKind::MissingTypeKwargs => "Missing type annotation for `**kwargs`".to_string(), + CheckKind::MissingTypeSelf => { + "Missing type annotation for `self` in method".to_string() + } + CheckKind::MissingTypeCls => { + "Missing type annotation for `cls` in classmethod".to_string() + } + CheckKind::MissingReturnTypePublicFunction => { + "Missing return type annotation for public function".to_string() + } + CheckKind::MissingReturnTypePrivateFunction => { + "Missing return type annotation for private function".to_string() + } + CheckKind::MissingReturnTypeMagicMethod => { + "Missing return type annotation for magic method".to_string() + } + CheckKind::MissingReturnTypeStaticMethod => { + "Missing return type annotation for staticmethod".to_string() + } + CheckKind::MissingReturnTypeClassMethod => { + "Missing return type annotation for classmethod".to_string() + } // pyupgrade CheckKind::TypeOfPrimitive(primitive) => { format!("Use `{}` instead of `type(...)`", primitive.builtin()) diff --git a/src/checks_gen.rs b/src/checks_gen.rs index e55bb9e8f0..dc324b70fe 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -13,6 +13,23 @@ pub enum CheckCodePrefix { A001, A002, A003, + ANN, + ANN0, + ANN00, + ANN001, + ANN002, + ANN003, + ANN1, + ANN10, + ANN101, + ANN102, + ANN2, + ANN20, + ANN201, + ANN202, + ANN204, + ANN205, + ANN206, B, B0, B00, @@ -257,6 +274,46 @@ impl CheckCodePrefix { CheckCodePrefix::A001 => vec![CheckCode::A001], CheckCodePrefix::A002 => vec![CheckCode::A002], CheckCodePrefix::A003 => vec![CheckCode::A003], + CheckCodePrefix::ANN => vec![ + CheckCode::ANN001, + CheckCode::ANN002, + CheckCode::ANN003, + CheckCode::ANN101, + CheckCode::ANN102, + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ], + CheckCodePrefix::ANN0 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003], + CheckCodePrefix::ANN00 => vec![CheckCode::ANN001, CheckCode::ANN002, CheckCode::ANN003], + CheckCodePrefix::ANN001 => vec![CheckCode::ANN001], + CheckCodePrefix::ANN002 => vec![CheckCode::ANN002], + CheckCodePrefix::ANN003 => vec![CheckCode::ANN003], + CheckCodePrefix::ANN1 => vec![CheckCode::ANN101, CheckCode::ANN102], + CheckCodePrefix::ANN10 => vec![CheckCode::ANN101, CheckCode::ANN102], + CheckCodePrefix::ANN101 => vec![CheckCode::ANN101], + CheckCodePrefix::ANN102 => vec![CheckCode::ANN102], + CheckCodePrefix::ANN2 => vec![ + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ], + CheckCodePrefix::ANN20 => vec![ + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ], + CheckCodePrefix::ANN201 => vec![CheckCode::ANN201], + CheckCodePrefix::ANN202 => vec![CheckCode::ANN202], + CheckCodePrefix::ANN204 => vec![CheckCode::ANN204], + CheckCodePrefix::ANN205 => vec![CheckCode::ANN205], + CheckCodePrefix::ANN206 => vec![CheckCode::ANN206], CheckCodePrefix::B => vec![ CheckCode::B002, CheckCode::B003, @@ -931,6 +988,23 @@ impl CheckCodePrefix { CheckCodePrefix::A001 => PrefixSpecificity::Explicit, CheckCodePrefix::A002 => PrefixSpecificity::Explicit, CheckCodePrefix::A003 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN => PrefixSpecificity::Category, + CheckCodePrefix::ANN0 => PrefixSpecificity::Hundreds, + CheckCodePrefix::ANN00 => PrefixSpecificity::Tens, + CheckCodePrefix::ANN001 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN002 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN003 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN1 => PrefixSpecificity::Hundreds, + CheckCodePrefix::ANN10 => PrefixSpecificity::Tens, + CheckCodePrefix::ANN101 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN102 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN2 => PrefixSpecificity::Hundreds, + CheckCodePrefix::ANN20 => PrefixSpecificity::Tens, + CheckCodePrefix::ANN201 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN202 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN204 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN205 => PrefixSpecificity::Explicit, + CheckCodePrefix::ANN206 => PrefixSpecificity::Explicit, CheckCodePrefix::B => PrefixSpecificity::Category, CheckCodePrefix::B0 => PrefixSpecificity::Hundreds, CheckCodePrefix::B00 => PrefixSpecificity::Tens, diff --git a/src/flake8_annotations/mod.rs b/src/flake8_annotations/mod.rs new file mode 100644 index 0000000000..038aeb37a9 --- /dev/null +++ b/src/flake8_annotations/mod.rs @@ -0,0 +1,131 @@ +pub mod plugins; +pub mod settings; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use rustpython_parser::lexer::LexResult; + + use crate::autofix::fixer; + use crate::checks::{Check, CheckCode}; + use crate::linter::tokenize; + use crate::{flake8_annotations, fs, linter, noqa, Settings, SourceCodeLocator}; + + fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result> { + let contents = fs::read_file(path)?; + let tokens: Vec = tokenize(&contents); + let locator = SourceCodeLocator::new(&contents); + let noqa_line_for = noqa::extract_noqa_line_for(&tokens); + linter::check_path( + path, + &contents, + tokens, + &locator, + &noqa_line_for, + settings, + autofix, + ) + } + + #[test] + fn defaults() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"), + &Settings { + ..Settings::for_rules(vec![ + CheckCode::ANN001, + CheckCode::ANN002, + CheckCode::ANN003, + CheckCode::ANN101, + CheckCode::ANN102, + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + + #[test] + fn suppress_dummy_args() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"), + &Settings { + flake8_annotations: flake8_annotations::settings::Settings { + mypy_init_return: false, + suppress_dummy_args: true, + suppress_none_returning: false, + }, + ..Settings::for_rules(vec![ + CheckCode::ANN001, + CheckCode::ANN002, + CheckCode::ANN003, + CheckCode::ANN101, + CheckCode::ANN102, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + + #[test] + fn mypy_init_return() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"), + &Settings { + flake8_annotations: flake8_annotations::settings::Settings { + mypy_init_return: true, + suppress_dummy_args: false, + suppress_none_returning: false, + }, + ..Settings::for_rules(vec![ + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } + + #[test] + fn suppress_none_returning() -> Result<()> { + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"), + &Settings { + flake8_annotations: flake8_annotations::settings::Settings { + mypy_init_return: false, + suppress_dummy_args: false, + suppress_none_returning: true, + }, + ..Settings::for_rules(vec![ + CheckCode::ANN201, + CheckCode::ANN202, + CheckCode::ANN204, + CheckCode::ANN205, + CheckCode::ANN206, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(checks); + Ok(()) + } +} diff --git a/src/flake8_annotations/plugins.rs b/src/flake8_annotations/plugins.rs new file mode 100644 index 0000000000..c237397df5 --- /dev/null +++ b/src/flake8_annotations/plugins.rs @@ -0,0 +1,326 @@ +use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind}; + +use crate::ast::types::Range; +use crate::ast::visitor; +use crate::ast::visitor::Visitor; +use crate::check_ast::Checker; +use crate::checks::{CheckCode, CheckKind}; +use crate::docstrings::definition::{Definition, DefinitionKind}; +use crate::visibility::Visibility; +use crate::{visibility, Check}; + +#[derive(Default)] +struct ReturnStatementVisitor<'a> { + returns: Vec<&'a Option>>, +} + +impl<'a, 'b> Visitor<'b> for ReturnStatementVisitor<'a> +where + 'b: 'a, +{ + fn visit_stmt(&mut self, stmt: &'b Stmt) { + match &stmt.node { + StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => { + // No recurse. + } + StmtKind::Return { value } => self.returns.push(value), + _ => visitor::walk_stmt(self, stmt), + } + } +} + +fn is_none_returning(stmt: &Stmt) -> bool { + let mut visitor: ReturnStatementVisitor = Default::default(); + for stmt in match_body(stmt) { + visitor.visit_stmt(stmt); + } + for expr in visitor.returns.into_iter().flatten() { + if !matches!( + expr.node, + ExprKind::Constant { + value: Constant::None, + .. + } + ) { + return false; + } + } + true +} + +fn match_args(stmt: &Stmt) -> &Arguments { + match &stmt.node { + StmtKind::FunctionDef { args, .. } | StmtKind::AsyncFunctionDef { args, .. } => args, + _ => panic!("Found non-FunctionDef in match_args"), + } +} + +fn match_body(stmt: &Stmt) -> &Vec { + match &stmt.node { + StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => body, + _ => panic!("Found non-FunctionDef in match_body"), + } +} + +fn match_returns(stmt: &Stmt) -> &Option> { + match &stmt.node { + StmtKind::FunctionDef { returns, .. } | StmtKind::AsyncFunctionDef { returns, .. } => { + returns + } + _ => panic!("Found non-FunctionDef in match_returns"), + } +} + +/// Generate flake8-annotation checks for a given `Definition`. +pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &Visibility) { + match &definition.kind { + DefinitionKind::Module => {} + DefinitionKind::Package => {} + DefinitionKind::Class(_) => {} + DefinitionKind::NestedClass(_) => {} + DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => { + let args = match_args(stmt); + let returns = match_returns(stmt); + + // ANN001 + for arg in args + .args + .iter() + .chain(args.posonlyargs.iter()) + .chain(args.kwonlyargs.iter()) + { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN001) { + checker.add_check(Check::new( + CheckKind::MissingTypeFunctionArgument, + Range::from_located(arg), + )); + } + } + } + } + + // ANN002 + if let Some(arg) = &args.vararg { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN002) { + checker.add_check(Check::new( + CheckKind::MissingTypeArgs, + Range::from_located(arg), + )); + } + } + } + } + + // ANN003 + if let Some(arg) = &args.kwarg { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN003) { + checker.add_check(Check::new( + CheckKind::MissingTypeKwargs, + Range::from_located(arg), + )); + } + } + } + } + + // ANN201, ANN202 + if returns.is_none() { + // Allow omission of return annotation in `__init__` functions, if the function + // only returns `None` (explicitly or implicitly). + if checker.settings.flake8_annotations.suppress_none_returning + && is_none_returning(stmt) + { + return; + } + + match visibility { + Visibility::Public => { + if checker.settings.enabled.contains(&CheckCode::ANN201) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypePublicFunction, + Range::from_located(stmt), + )); + } + } + Visibility::Private => { + if checker.settings.enabled.contains(&CheckCode::ANN202) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypePrivateFunction, + Range::from_located(stmt), + )); + } + } + } + } + } + DefinitionKind::Method(stmt) => { + let args = match_args(stmt); + let returns = match_returns(stmt); + let mut has_any_typed_arg = false; + + // ANN001 + for arg in args + .args + .iter() + .chain(args.posonlyargs.iter()) + .chain(args.kwonlyargs.iter()) + .skip( + // If this is a non-static method, skip `cls` or `self`. + usize::from(!visibility::is_staticmethod(stmt)), + ) + { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN001) { + checker.add_check(Check::new( + CheckKind::MissingTypeFunctionArgument, + Range::from_located(arg), + )); + } + } + } else { + has_any_typed_arg = true; + } + } + + // ANN002 + if let Some(arg) = &args.vararg { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN002) { + checker.add_check(Check::new( + CheckKind::MissingTypeArgs, + Range::from_located(arg), + )); + } + } + } else { + has_any_typed_arg = true; + } + } + + // ANN003 + if let Some(arg) = &args.kwarg { + if arg.node.annotation.is_none() { + if !(checker.settings.flake8_annotations.suppress_dummy_args + && checker.settings.dummy_variable_rgx.is_match(&arg.node.arg)) + { + if checker.settings.enabled.contains(&CheckCode::ANN003) { + checker.add_check(Check::new( + CheckKind::MissingTypeKwargs, + Range::from_located(arg), + )); + } + } + } else { + has_any_typed_arg = true; + } + } + + // ANN101, ANN102 + if !visibility::is_staticmethod(stmt) { + if let Some(arg) = args.args.first() { + if arg.node.annotation.is_none() { + if visibility::is_classmethod(stmt) { + if checker.settings.enabled.contains(&CheckCode::ANN101) { + checker.add_check(Check::new( + CheckKind::MissingTypeCls, + Range::from_located(arg), + )); + } + } else { + if checker.settings.enabled.contains(&CheckCode::ANN102) { + checker.add_check(Check::new( + CheckKind::MissingTypeSelf, + Range::from_located(arg), + )); + } + } + } + } + } + + // ANN201, ANN202 + if returns.is_none() { + // Allow omission of return annotation in `__init__` functions, if the function + // only returns `None` (explicitly or implicitly). + if checker.settings.flake8_annotations.suppress_none_returning + && is_none_returning(stmt) + { + return; + } + + if visibility::is_classmethod(stmt) { + if checker.settings.enabled.contains(&CheckCode::ANN206) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypeClassMethod, + Range::from_located(stmt), + )); + } + } else if visibility::is_staticmethod(stmt) { + if checker.settings.enabled.contains(&CheckCode::ANN205) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypeStaticMethod, + Range::from_located(stmt), + )); + } + } else if visibility::is_magic(stmt) { + if checker.settings.enabled.contains(&CheckCode::ANN204) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypeMagicMethod, + Range::from_located(stmt), + )); + } + } else if visibility::is_init(stmt) { + // Allow omission of return annotation in `__init__` functions, as long as at + // least one argument is typed. + if checker.settings.enabled.contains(&CheckCode::ANN204) { + if !(checker.settings.flake8_annotations.mypy_init_return + && has_any_typed_arg) + { + checker.add_check(Check::new( + CheckKind::MissingReturnTypeMagicMethod, + Range::from_located(stmt), + )); + } + } + } else { + match visibility { + Visibility::Public => { + if checker.settings.enabled.contains(&CheckCode::ANN201) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypePublicFunction, + Range::from_located(stmt), + )); + } + } + Visibility::Private => { + if checker.settings.enabled.contains(&CheckCode::ANN202) { + checker.add_check(Check::new( + CheckKind::MissingReturnTypePrivateFunction, + Range::from_located(stmt), + )); + } + } + } + } + } + } + } +} diff --git a/src/flake8_annotations/settings.rs b/src/flake8_annotations/settings.rs new file mode 100644 index 0000000000..7930010b47 --- /dev/null +++ b/src/flake8_annotations/settings.rs @@ -0,0 +1,36 @@ +//! Settings for the `flake-annotations` plugin. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Options { + /// Allow omission of a return type hint for `__init__` if at least one + /// argument is annotated. + pub mypy_init_return: Option, + /// Suppress ANN000-level errors for dummy arguments, like `_`. + pub suppress_dummy_args: Option, + /// Suppress ANN200-level errors for functions that meet one of the + /// following criteria: + /// - Contain no `return` statement + /// - Explicit `return` statement(s) all return `None` (explicitly or + /// implicitly). + pub suppress_none_returning: Option, +} + +#[derive(Debug, Hash, Default)] +pub struct Settings { + pub mypy_init_return: bool, + pub suppress_dummy_args: bool, + pub suppress_none_returning: bool, +} + +impl Settings { + pub fn from_options(options: Options) -> Self { + Self { + mypy_init_return: options.mypy_init_return.unwrap_or_default(), + suppress_dummy_args: options.suppress_dummy_args.unwrap_or_default(), + suppress_none_returning: options.suppress_none_returning.unwrap_or_default(), + } + } +} diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap new file mode 100644 index 0000000000..bba684f7d2 --- /dev/null +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__defaults.snap @@ -0,0 +1,69 @@ +--- +source: src/flake8_annotations/mod.rs +expression: checks +--- +- kind: MissingReturnTypePublicFunction + location: + row: 2 + column: 0 + end_location: + row: 7 + column: 0 + fix: ~ +- kind: MissingTypeFunctionArgument + location: + row: 2 + column: 8 + end_location: + row: 2 + column: 9 + fix: ~ +- kind: MissingTypeFunctionArgument + location: + row: 2 + column: 11 + end_location: + row: 2 + column: 12 + fix: ~ +- kind: MissingReturnTypePublicFunction + location: + row: 7 + column: 0 + end_location: + row: 12 + column: 0 + fix: ~ +- kind: MissingTypeFunctionArgument + location: + row: 7 + column: 16 + end_location: + row: 7 + column: 17 + fix: ~ +- kind: MissingTypeFunctionArgument + location: + row: 12 + column: 16 + end_location: + row: 12 + column: 17 + fix: ~ +- kind: MissingReturnTypePublicFunction + location: + row: 17 + column: 0 + end_location: + row: 22 + column: 0 + fix: ~ +- kind: MissingReturnTypePublicFunction + location: + row: 22 + column: 0 + end_location: + row: 27 + column: 0 + fix: ~ + diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__mypy_init_return.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__mypy_init_return.snap new file mode 100644 index 0000000000..d2e7b8c6eb --- /dev/null +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__mypy_init_return.snap @@ -0,0 +1,29 @@ +--- +source: src/flake8_annotations/mod.rs +expression: checks +--- +- kind: MissingReturnTypeMagicMethod + location: + row: 5 + column: 4 + end_location: + row: 10 + column: 0 + fix: ~ +- kind: MissingReturnTypeMagicMethod + location: + row: 11 + column: 4 + end_location: + row: 16 + column: 0 + fix: ~ +- kind: MissingReturnTypePrivateFunction + location: + row: 40 + column: 0 + end_location: + row: 42 + column: 0 + fix: ~ + diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_dummy_args.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_dummy_args.snap new file mode 100644 index 0000000000..dd8109cab0 --- /dev/null +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_dummy_args.snap @@ -0,0 +1,6 @@ +--- +source: src/flake8_annotations/mod.rs +expression: checks +--- +[] + diff --git a/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_none_returning.snap b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_none_returning.snap new file mode 100644 index 0000000000..b8798ea6b7 --- /dev/null +++ b/src/flake8_annotations/snapshots/ruff__flake8_annotations__tests__suppress_none_returning.snap @@ -0,0 +1,21 @@ +--- +source: src/flake8_annotations/mod.rs +expression: checks +--- +- kind: MissingReturnTypePublicFunction + location: + row: 45 + column: 0 + end_location: + row: 50 + column: 0 + fix: ~ +- kind: MissingReturnTypePublicFunction + location: + row: 50 + column: 0 + end_location: + row: 56 + column: 0 + fix: ~ + diff --git a/src/flake8_quotes/checks.rs b/src/flake8_quotes/checks.rs index 86013ac115..8fae48c00d 100644 --- a/src/flake8_quotes/checks.rs +++ b/src/flake8_quotes/checks.rs @@ -135,172 +135,3 @@ pub fn quotes( None } - -#[cfg(test)] -mod tests { - use std::path::Path; - - use anyhow::Result; - use rustpython_parser::lexer::LexResult; - use test_case::test_case; - - use crate::autofix::fixer; - use crate::checks::{Check, CheckCode}; - use crate::flake8_quotes::settings::Quote; - use crate::linter::tokenize; - use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator}; - - fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result> { - let contents = fs::read_file(path)?; - let tokens: Vec = tokenize(&contents); - let locator = SourceCodeLocator::new(&contents); - let noqa_line_for = noqa::extract_noqa_line_for(&tokens); - linter::check_path( - path, - &contents, - tokens, - &locator, - &noqa_line_for, - settings, - autofix, - ) - } - - #[test_case(Path::new("doubles.py"))] - #[test_case(Path::new("doubles_escaped.py"))] - #[test_case(Path::new("doubles_multiline_string.py"))] - #[test_case(Path::new("doubles_noqa.py"))] - #[test_case(Path::new("doubles_wrapped.py"))] - fn doubles(path: &Path) -> Result<()> { - let snapshot = format!("doubles_{}", path.to_string_lossy()); - let mut checks = check_path( - Path::new("./resources/test/fixtures/flake8_quotes") - .join(path) - .as_path(), - &Settings { - flake8_quotes: flake8_quotes::settings::Settings { - inline_quotes: Quote::Single, - multiline_quotes: Quote::Single, - docstring_quotes: Quote::Single, - avoid_escape: true, - }, - ..Settings::for_rules(vec![ - CheckCode::Q000, - CheckCode::Q001, - CheckCode::Q002, - CheckCode::Q003, - ]) - }, - &fixer::Mode::Generate, - )?; - checks.sort_by_key(|check| check.location); - insta::assert_yaml_snapshot!(snapshot, checks); - Ok(()) - } - - #[test_case(Path::new("singles.py"))] - #[test_case(Path::new("singles_escaped.py"))] - #[test_case(Path::new("singles_multiline_string.py"))] - #[test_case(Path::new("singles_noqa.py"))] - #[test_case(Path::new("singles_wrapped.py"))] - fn singles(path: &Path) -> Result<()> { - let snapshot = format!("singles_{}", path.to_string_lossy()); - let mut checks = check_path( - Path::new("./resources/test/fixtures/flake8_quotes") - .join(path) - .as_path(), - &Settings { - flake8_quotes: flake8_quotes::settings::Settings { - inline_quotes: Quote::Double, - multiline_quotes: Quote::Double, - docstring_quotes: Quote::Double, - avoid_escape: true, - }, - ..Settings::for_rules(vec![ - CheckCode::Q000, - CheckCode::Q001, - CheckCode::Q002, - CheckCode::Q003, - ]) - }, - &fixer::Mode::Generate, - )?; - checks.sort_by_key(|check| check.location); - insta::assert_yaml_snapshot!(snapshot, checks); - Ok(()) - } - - #[test_case(Path::new("docstring_doubles.py"))] - #[test_case(Path::new("docstring_doubles_module_multiline.py"))] - #[test_case(Path::new("docstring_doubles_module_singleline.py"))] - #[test_case(Path::new("docstring_doubles_class.py"))] - #[test_case(Path::new("docstring_doubles_function.py"))] - #[test_case(Path::new("docstring_singles.py"))] - #[test_case(Path::new("docstring_singles_module_multiline.py"))] - #[test_case(Path::new("docstring_singles_module_singleline.py"))] - #[test_case(Path::new("docstring_singles_class.py"))] - #[test_case(Path::new("docstring_singles_function.py"))] - fn double_docstring(path: &Path) -> Result<()> { - let snapshot = format!("double_docstring_{}", path.to_string_lossy()); - let mut checks = check_path( - Path::new("./resources/test/fixtures/flake8_quotes") - .join(path) - .as_path(), - &Settings { - flake8_quotes: flake8_quotes::settings::Settings { - inline_quotes: Quote::Single, - multiline_quotes: Quote::Single, - docstring_quotes: Quote::Double, - avoid_escape: true, - }, - ..Settings::for_rules(vec![ - CheckCode::Q000, - CheckCode::Q001, - CheckCode::Q002, - CheckCode::Q003, - ]) - }, - &fixer::Mode::Generate, - )?; - checks.sort_by_key(|check| check.location); - insta::assert_yaml_snapshot!(snapshot, checks); - Ok(()) - } - - #[test_case(Path::new("docstring_doubles.py"))] - #[test_case(Path::new("docstring_doubles_module_multiline.py"))] - #[test_case(Path::new("docstring_doubles_module_singleline.py"))] - #[test_case(Path::new("docstring_doubles_class.py"))] - #[test_case(Path::new("docstring_doubles_function.py"))] - #[test_case(Path::new("docstring_singles.py"))] - #[test_case(Path::new("docstring_singles_module_multiline.py"))] - #[test_case(Path::new("docstring_singles_module_singleline.py"))] - #[test_case(Path::new("docstring_singles_class.py"))] - #[test_case(Path::new("docstring_singles_function.py"))] - fn single_docstring(path: &Path) -> Result<()> { - let snapshot = format!("single_docstring_{}", path.to_string_lossy()); - let mut checks = check_path( - Path::new("./resources/test/fixtures/flake8_quotes") - .join(path) - .as_path(), - &Settings { - flake8_quotes: flake8_quotes::settings::Settings { - inline_quotes: Quote::Single, - multiline_quotes: Quote::Double, - docstring_quotes: Quote::Single, - avoid_escape: true, - }, - ..Settings::for_rules(vec![ - CheckCode::Q000, - CheckCode::Q001, - CheckCode::Q002, - CheckCode::Q003, - ]) - }, - &fixer::Mode::Generate, - )?; - checks.sort_by_key(|check| check.location); - insta::assert_yaml_snapshot!(snapshot, checks); - Ok(()) - } -} diff --git a/src/flake8_quotes/mod.rs b/src/flake8_quotes/mod.rs index bf79386dbb..89229f951c 100644 --- a/src/flake8_quotes/mod.rs +++ b/src/flake8_quotes/mod.rs @@ -1,2 +1,171 @@ pub mod checks; pub mod settings; + +#[cfg(test)] +mod tests { + use std::path::Path; + + use anyhow::Result; + use rustpython_parser::lexer::LexResult; + use test_case::test_case; + + use crate::autofix::fixer; + use crate::checks::{Check, CheckCode}; + use crate::flake8_quotes::settings::Quote; + use crate::linter::tokenize; + use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator}; + + fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result> { + let contents = fs::read_file(path)?; + let tokens: Vec = tokenize(&contents); + let locator = SourceCodeLocator::new(&contents); + let noqa_line_for = noqa::extract_noqa_line_for(&tokens); + linter::check_path( + path, + &contents, + tokens, + &locator, + &noqa_line_for, + settings, + autofix, + ) + } + + #[test_case(Path::new("doubles.py"))] + #[test_case(Path::new("doubles_escaped.py"))] + #[test_case(Path::new("doubles_multiline_string.py"))] + #[test_case(Path::new("doubles_noqa.py"))] + #[test_case(Path::new("doubles_wrapped.py"))] + fn doubles(path: &Path) -> Result<()> { + let snapshot = format!("doubles_{}", path.to_string_lossy()); + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_quotes") + .join(path) + .as_path(), + &Settings { + flake8_quotes: flake8_quotes::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Single, + docstring_quotes: Quote::Single, + avoid_escape: true, + }, + ..Settings::for_rules(vec![ + CheckCode::Q000, + CheckCode::Q001, + CheckCode::Q002, + CheckCode::Q003, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } + + #[test_case(Path::new("singles.py"))] + #[test_case(Path::new("singles_escaped.py"))] + #[test_case(Path::new("singles_multiline_string.py"))] + #[test_case(Path::new("singles_noqa.py"))] + #[test_case(Path::new("singles_wrapped.py"))] + fn singles(path: &Path) -> Result<()> { + let snapshot = format!("singles_{}", path.to_string_lossy()); + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_quotes") + .join(path) + .as_path(), + &Settings { + flake8_quotes: flake8_quotes::settings::Settings { + inline_quotes: Quote::Double, + multiline_quotes: Quote::Double, + docstring_quotes: Quote::Double, + avoid_escape: true, + }, + ..Settings::for_rules(vec![ + CheckCode::Q000, + CheckCode::Q001, + CheckCode::Q002, + CheckCode::Q003, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } + + #[test_case(Path::new("docstring_doubles.py"))] + #[test_case(Path::new("docstring_doubles_module_multiline.py"))] + #[test_case(Path::new("docstring_doubles_module_singleline.py"))] + #[test_case(Path::new("docstring_doubles_class.py"))] + #[test_case(Path::new("docstring_doubles_function.py"))] + #[test_case(Path::new("docstring_singles.py"))] + #[test_case(Path::new("docstring_singles_module_multiline.py"))] + #[test_case(Path::new("docstring_singles_module_singleline.py"))] + #[test_case(Path::new("docstring_singles_class.py"))] + #[test_case(Path::new("docstring_singles_function.py"))] + fn double_docstring(path: &Path) -> Result<()> { + let snapshot = format!("double_docstring_{}", path.to_string_lossy()); + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_quotes") + .join(path) + .as_path(), + &Settings { + flake8_quotes: flake8_quotes::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Single, + docstring_quotes: Quote::Double, + avoid_escape: true, + }, + ..Settings::for_rules(vec![ + CheckCode::Q000, + CheckCode::Q001, + CheckCode::Q002, + CheckCode::Q003, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } + + #[test_case(Path::new("docstring_doubles.py"))] + #[test_case(Path::new("docstring_doubles_module_multiline.py"))] + #[test_case(Path::new("docstring_doubles_module_singleline.py"))] + #[test_case(Path::new("docstring_doubles_class.py"))] + #[test_case(Path::new("docstring_doubles_function.py"))] + #[test_case(Path::new("docstring_singles.py"))] + #[test_case(Path::new("docstring_singles_module_multiline.py"))] + #[test_case(Path::new("docstring_singles_module_singleline.py"))] + #[test_case(Path::new("docstring_singles_class.py"))] + #[test_case(Path::new("docstring_singles_function.py"))] + fn single_docstring(path: &Path) -> Result<()> { + let snapshot = format!("single_docstring_{}", path.to_string_lossy()); + let mut checks = check_path( + Path::new("./resources/test/fixtures/flake8_quotes") + .join(path) + .as_path(), + &Settings { + flake8_quotes: flake8_quotes::settings::Settings { + inline_quotes: Quote::Single, + multiline_quotes: Quote::Double, + docstring_quotes: Quote::Single, + avoid_escape: true, + }, + ..Settings::for_rules(vec![ + CheckCode::Q000, + CheckCode::Q001, + CheckCode::Q002, + CheckCode::Q003, + ]) + }, + &fixer::Mode::Generate, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } +} diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles.py.snap new file mode 100644 index 0000000000..20683a19c5 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles.py.snap @@ -0,0 +1,50 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 5 + column: 0 + end_location: + row: 7 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 16 + column: 4 + end_location: + row: 18 + column: 7 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 21 + column: 20 + end_location: + row: 22 + column: 37 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 30 + column: 8 + end_location: + row: 32 + column: 11 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 35 + column: 12 + end_location: + row: 37 + column: 15 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_class.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_class.py.snap new file mode 100644 index 0000000000..6250fc929c --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_class.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 27 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 5 + column: 22 + end_location: + row: 5 + column: 43 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_function.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_function.py.snap new file mode 100644 index 0000000000..59af08a7bf --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_function.py.snap @@ -0,0 +1,50 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 26 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 11 + column: 4 + end_location: + row: 11 + column: 26 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 15 + column: 38 + end_location: + row: 17 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 17 + column: 4 + end_location: + row: 17 + column: 19 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 21 + column: 4 + end_location: + row: 21 + column: 27 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_multiline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_multiline.py.snap new file mode 100644 index 0000000000..594927fa73 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_multiline.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 4 + column: 0 + end_location: + row: 6 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 9 + column: 0 + end_location: + row: 11 + column: 3 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_singleline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_singleline.py.snap new file mode 100644 index 0000000000..8500dd06d0 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_doubles_module_singleline.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 2 + column: 0 + end_location: + row: 2 + column: 31 + fix: ~ +- kind: + BadQuotesMultilineString: single + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 31 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles.py.snap new file mode 100644 index 0000000000..74d6a48868 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles.py.snap @@ -0,0 +1,32 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: double + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 3 + fix: ~ +- kind: + BadQuotesDocstring: double + location: + row: 14 + column: 4 + end_location: + row: 16 + column: 7 + fix: ~ +- kind: + BadQuotesDocstring: double + location: + row: 26 + column: 8 + end_location: + row: 28 + column: 11 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_class.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_class.py.snap new file mode 100644 index 0000000000..64a9b59ab8 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_class.py.snap @@ -0,0 +1,32 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: double + location: + row: 2 + column: 4 + end_location: + row: 2 + column: 53 + fix: ~ +- kind: + BadQuotesDocstring: double + location: + row: 6 + column: 8 + end_location: + row: 6 + column: 57 + fix: ~ +- kind: + BadQuotesDocstring: double + location: + row: 9 + column: 28 + end_location: + row: 9 + column: 52 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_function.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_function.py.snap new file mode 100644 index 0000000000..5f9399927c --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_function.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: double + location: + row: 2 + column: 4 + end_location: + row: 2 + column: 56 + fix: ~ +- kind: + BadQuotesDocstring: double + location: + row: 8 + column: 4 + end_location: + row: 10 + column: 7 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_multiline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_multiline.py.snap new file mode 100644 index 0000000000..e3c7ae5067 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_multiline.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: double + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 3 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_singleline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_singleline.py.snap new file mode 100644 index 0000000000..0b450cd8c1 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__double_docstring_docstring_singles_module_singleline.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: double + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 49 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles.py.snap new file mode 100644 index 0000000000..cb6e3d8127 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesInlineString: single + location: + row: 1 + column: 24 + end_location: + row: 1 + column: 45 + fix: ~ +- kind: + BadQuotesInlineString: single + location: + row: 2 + column: 24 + end_location: + row: 2 + column: 46 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_escaped.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_escaped.py.snap new file mode 100644 index 0000000000..12867bb8e3 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_escaped.py.snap @@ -0,0 +1,13 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: AvoidQuoteEscape + location: + row: 1 + column: 25 + end_location: + row: 1 + column: 47 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_multiline_string.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_multiline_string.py.snap new file mode 100644 index 0000000000..76901d7342 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_multiline_string.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: single + location: + row: 1 + column: 4 + end_location: + row: 3 + column: 12 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_noqa.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_noqa.py.snap new file mode 100644 index 0000000000..ad8fb2403f --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_noqa.py.snap @@ -0,0 +1,6 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +[] + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_wrapped.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_wrapped.py.snap new file mode 100644 index 0000000000..ad8fb2403f --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__doubles_doubles_wrapped.py.snap @@ -0,0 +1,6 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +[] + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles.py.snap new file mode 100644 index 0000000000..121d2303b7 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles.py.snap @@ -0,0 +1,32 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: single + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 3 + fix: ~ +- kind: + BadQuotesDocstring: single + location: + row: 12 + column: 4 + end_location: + row: 14 + column: 7 + fix: ~ +- kind: + BadQuotesDocstring: single + location: + row: 24 + column: 8 + end_location: + row: 26 + column: 11 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_class.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_class.py.snap new file mode 100644 index 0000000000..f66d3ebb5e --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_class.py.snap @@ -0,0 +1,32 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: single + location: + row: 2 + column: 4 + end_location: + row: 2 + column: 53 + fix: ~ +- kind: + BadQuotesDocstring: single + location: + row: 6 + column: 8 + end_location: + row: 6 + column: 57 + fix: ~ +- kind: + BadQuotesDocstring: single + location: + row: 9 + column: 28 + end_location: + row: 9 + column: 52 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_function.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_function.py.snap new file mode 100644 index 0000000000..874553d24c --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_function.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: single + location: + row: 2 + column: 4 + end_location: + row: 2 + column: 56 + fix: ~ +- kind: + BadQuotesDocstring: single + location: + row: 8 + column: 4 + end_location: + row: 10 + column: 7 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_multiline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_multiline.py.snap new file mode 100644 index 0000000000..1930d69839 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_multiline.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: single + location: + row: 1 + column: 0 + end_location: + row: 3 + column: 3 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_singleline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_singleline.py.snap new file mode 100644 index 0000000000..30a5ea0998 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_doubles_module_singleline.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesDocstring: single + location: + row: 1 + column: 0 + end_location: + row: 1 + column: 49 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles.py.snap new file mode 100644 index 0000000000..26be89d9de --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles.py.snap @@ -0,0 +1,59 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 5 + column: 0 + end_location: + row: 7 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 11 + column: 20 + end_location: + row: 13 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 18 + column: 4 + end_location: + row: 20 + column: 7 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 23 + column: 20 + end_location: + row: 24 + column: 37 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 32 + column: 8 + end_location: + row: 34 + column: 11 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 37 + column: 12 + end_location: + row: 39 + column: 15 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_class.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_class.py.snap new file mode 100644 index 0000000000..06031795b5 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_class.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 27 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 5 + column: 22 + end_location: + row: 5 + column: 43 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_function.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_function.py.snap new file mode 100644 index 0000000000..048cf9db0a --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_function.py.snap @@ -0,0 +1,50 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 3 + column: 4 + end_location: + row: 3 + column: 26 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 11 + column: 4 + end_location: + row: 11 + column: 26 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 15 + column: 38 + end_location: + row: 17 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 17 + column: 4 + end_location: + row: 17 + column: 19 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 21 + column: 4 + end_location: + row: 21 + column: 27 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_multiline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_multiline.py.snap new file mode 100644 index 0000000000..c8adddc847 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_multiline.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 4 + column: 0 + end_location: + row: 6 + column: 3 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 9 + column: 0 + end_location: + row: 11 + column: 3 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_singleline.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_singleline.py.snap new file mode 100644 index 0000000000..a04c70a2e4 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__single_docstring_docstring_singles_module_singleline.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 2 + column: 0 + end_location: + row: 2 + column: 31 + fix: ~ +- kind: + BadQuotesMultilineString: double + location: + row: 6 + column: 0 + end_location: + row: 6 + column: 31 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles.py.snap new file mode 100644 index 0000000000..9d5e860093 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles.py.snap @@ -0,0 +1,23 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesInlineString: double + location: + row: 1 + column: 24 + end_location: + row: 1 + column: 45 + fix: ~ +- kind: + BadQuotesInlineString: double + location: + row: 2 + column: 24 + end_location: + row: 2 + column: 46 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_escaped.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_escaped.py.snap new file mode 100644 index 0000000000..12867bb8e3 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_escaped.py.snap @@ -0,0 +1,13 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: AvoidQuoteEscape + location: + row: 1 + column: 25 + end_location: + row: 1 + column: 47 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_multiline_string.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_multiline_string.py.snap new file mode 100644 index 0000000000..d2e6b1ba07 --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_multiline_string.py.snap @@ -0,0 +1,14 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +- kind: + BadQuotesMultilineString: double + location: + row: 1 + column: 4 + end_location: + row: 3 + column: 12 + fix: ~ + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_noqa.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_noqa.py.snap new file mode 100644 index 0000000000..ad8fb2403f --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_noqa.py.snap @@ -0,0 +1,6 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +[] + diff --git a/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_wrapped.py.snap b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_wrapped.py.snap new file mode 100644 index 0000000000..ad8fb2403f --- /dev/null +++ b/src/flake8_quotes/snapshots/ruff__flake8_quotes__tests__singles_singles_wrapped.py.snap @@ -0,0 +1,6 @@ +--- +source: src/flake8_quotes/mod.rs +expression: checks +--- +[] + diff --git a/src/lib.rs b/src/lib.rs index f1298769d2..67466370bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ pub mod cli; pub mod code_gen; mod cst; mod docstrings; +pub mod flake8_annotations; mod flake8_bugbear; mod flake8_builtins; mod flake8_comprehensions; diff --git a/src/pydocstyle/plugins.rs b/src/pydocstyle/plugins.rs index 1c25a0a17e..ba9e613ec2 100644 --- a/src/pydocstyle/plugins.rs +++ b/src/pydocstyle/plugins.rs @@ -13,7 +13,7 @@ use crate::docstrings::definition::{Definition, DefinitionKind}; use crate::docstrings::helpers; use crate::docstrings::sections::{section_contexts, SectionContext}; use crate::docstrings::styles::SectionStyle; -use crate::visibility::{is_init, is_magic, is_overload, is_static, Visibility}; +use crate::visibility::{is_init, is_magic, is_overload, is_staticmethod, Visibility}; /// D100, D101, D102, D103, D104, D105, D106, D107 pub fn not_missing( @@ -1308,7 +1308,8 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args: .skip( // If this is a non-static method, skip `cls` or `self`. usize::from( - matches!(definition.kind, DefinitionKind::Method(_)) && !is_static(parent), + matches!(definition.kind, DefinitionKind::Method(_)) + && !is_staticmethod(parent), ), ) .collect(); diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 8828a96e0d..5d71d71552 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -12,7 +12,7 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::settings::pyproject::load_options; use crate::settings::types::{FilePattern, PythonVersion}; -use crate::{flake8_quotes, pep8_naming}; +use crate::{flake8_annotations, flake8_quotes, pep8_naming}; #[derive(Debug)] pub struct Configuration { @@ -27,6 +27,7 @@ pub struct Configuration { pub select: Vec, pub target_version: PythonVersion, // Plugins + pub flake8_annotations: flake8_annotations::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, } @@ -95,6 +96,10 @@ impl Configuration { line_length: options.line_length.unwrap_or(88), per_file_ignores: options.per_file_ignores.unwrap_or_default(), // Plugins + flake8_annotations: options + .flake8_annotations + .map(flake8_annotations::settings::Settings::from_options) + .unwrap_or_default(), flake8_quotes: options .flake8_quotes .map(flake8_quotes::settings::Settings::from_options) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index ec9ecaf591..88fd97833f 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -11,7 +11,7 @@ use crate::checks::CheckCode; use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; -use crate::{flake8_quotes, pep8_naming}; +use crate::{flake8_annotations, flake8_quotes, pep8_naming}; pub mod configuration; pub mod options; @@ -29,6 +29,7 @@ pub struct Settings { pub per_file_ignores: Vec, pub target_version: PythonVersion, // Plugins + pub flake8_annotations: flake8_annotations::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, } @@ -45,6 +46,7 @@ impl Settings { ), exclude: config.exclude, extend_exclude: config.extend_exclude, + flake8_annotations: config.flake8_annotations, flake8_quotes: config.flake8_quotes, line_length: config.line_length, pep8_naming: config.pep8_naming, @@ -62,6 +64,7 @@ impl Settings { line_length: 88, per_file_ignores: Default::default(), target_version: PythonVersion::Py310, + flake8_annotations: Default::default(), flake8_quotes: Default::default(), pep8_naming: Default::default(), } @@ -76,6 +79,7 @@ impl Settings { line_length: 88, per_file_ignores: Default::default(), target_version: PythonVersion::Py310, + flake8_annotations: Default::default(), flake8_quotes: Default::default(), pep8_naming: Default::default(), } @@ -95,6 +99,7 @@ impl Hash for Settings { } self.target_version.hash(state); // Add plugin properties in alphabetical order. + self.flake8_annotations.hash(state); self.flake8_quotes.hash(state); self.pep8_naming.hash(state); } diff --git a/src/settings/options.rs b/src/settings/options.rs index 3980632445..53747dc435 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; use crate::settings::types::PythonVersion; -use crate::{flake8_quotes, pep8_naming}; +use crate::{flake8_annotations, flake8_quotes, pep8_naming}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -22,6 +22,7 @@ pub struct Options { pub dummy_variable_rgx: Option, pub target_version: Option, // Plugins + pub flake8_annotations: Option, pub flake8_quotes: Option, pub pep8_naming: Option, } diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index b29b49729b..67de700c65 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -143,6 +143,7 @@ mod tests { per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }) @@ -170,6 +171,7 @@ line-length = 79 per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }) @@ -197,6 +199,7 @@ exclude = ["foo.py"] per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }) @@ -224,6 +227,7 @@ select = ["E501"] per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }) @@ -252,6 +256,7 @@ ignore = ["E501"] per_file_ignores: None, dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: None, pep8_naming: None, }) @@ -326,6 +331,7 @@ other-attribute = 1 ),])), dummy_variable_rgx: None, target_version: None, + flake8_annotations: None, flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(Quote::Single), multiline_quotes: Some(Quote::Double), diff --git a/src/settings/user.rs b/src/settings/user.rs index 84e87baaca..135d43b862 100644 --- a/src/settings/user.rs +++ b/src/settings/user.rs @@ -7,7 +7,7 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::settings::types::{FilePattern, PythonVersion}; -use crate::{flake8_quotes, pep8_naming, Configuration}; +use crate::{flake8_annotations, flake8_quotes, pep8_naming, Configuration}; /// Struct to render user-facing exclusion patterns. #[derive(Debug)] @@ -46,6 +46,7 @@ pub struct UserConfiguration { pub select: Vec, pub target_version: PythonVersion, // Plugins + pub flake8_annotations: flake8_annotations::settings::Settings, pub flake8_quotes: flake8_quotes::settings::Settings, pub pep8_naming: pep8_naming::settings::Settings, // Non-settings exposed to the user @@ -78,6 +79,7 @@ impl UserConfiguration { per_file_ignores: configuration.per_file_ignores, select: configuration.select, target_version: configuration.target_version, + flake8_annotations: configuration.flake8_annotations, flake8_quotes: configuration.flake8_quotes, pep8_naming: configuration.pep8_naming, project_root, diff --git a/src/visibility.rs b/src/visibility.rs index 114d398418..9efd29070a 100644 --- a/src/visibility.rs +++ b/src/visibility.rs @@ -28,13 +28,24 @@ pub struct VisibleScope { } /// Returns `true` if a function is a "static method". -pub fn is_static(stmt: &Stmt) -> bool { +pub fn is_staticmethod(stmt: &Stmt) -> bool { match &stmt.node { StmtKind::FunctionDef { decorator_list, .. } | StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list .iter() .any(|expr| match_name_or_attr(expr, "staticmethod")), - _ => panic!("Found non-FunctionDef in is_overload"), + _ => panic!("Found non-FunctionDef in is_staticmethod"), + } +} + +/// Returns `true` if a function is a "class method". +pub fn is_classmethod(stmt: &Stmt) -> bool { + match &stmt.node { + StmtKind::FunctionDef { decorator_list, .. } + | StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list + .iter() + .any(|expr| match_name_or_attr(expr, "classmethod")), + _ => panic!("Found non-FunctionDef in is_classmethod"), } }