diff --git a/README.md b/README.md index fa3a70b66572a9..657b8fcfaae660 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ An extremely fast Python linter, written in Rust. <i>Linting the CPython codebase from scratch.</i> </p> -- ⚡️ 10-100x faster than existing linters -- 🐍 Installable via `pip` -- 🤝 Python 3.10 compatibility -- 🛠️ `pyproject.toml` support -- 📦 Built-in caching, to avoid re-analyzing unchanged files -- 🔧 `--fix` support, for automatic error correction (e.g., automatically remove unused imports) -- 👀 `--watch` support, for continuous file monitoring -- ⚖️ [Near-parity](#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set -- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) ([`pydocstyle`](https://pypi.org/project/pydocstyle/)) +- ⚡️ 10-100x faster than existing linters +- 🐍 Installable via `pip` +- 🤝 Python 3.10 compatibility +- 🛠️ `pyproject.toml` support +- 📦 Built-in caching, to avoid re-analyzing unchanged files +- 🔧 `--fix` support, for automatic error correction (e.g., automatically remove unused imports) +- 👀 `--watch` support, for continuous file monitoring +- ⚖️ [Near-parity](#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set +- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) ([`pydocstyle`](https://pypi.org/project/pydocstyle/)) Ruff aims to be orders of magnitude faster than alternative tools while integrating more functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety @@ -60,13 +60,13 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu 12. [flake8-2020](#flake8-2020) 13. [Ruff-specific rules](#ruff-specific-rules) 14. [Meta rules](#meta-rules) -5. [Editor Integrations](#editor-integrations) -6. [FAQ](#faq) -7. [Development](#development) -8. [Releases](#releases) -9. [Benchmarks](#benchmarks) -10. [License](#license) -11. [Contributing](#contributing) +4. [Editor Integrations](#editor-integrations) +5. [FAQ](#faq) +6. [Development](#development) +7. [Releases](#releases) +8. [Benchmarks](#benchmarks) +9. [License](#license) +10. [Contributing](#contributing) ## Installation and Usage @@ -105,6 +105,7 @@ repos: ``` <!-- TODO(charlie): Remove this message a few versions after v0.0.86. --> + _Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as the hook ID._ ## Configuration @@ -257,7 +258,7 @@ Exclusions are based on globs, and can be either: ### Ignoring errors To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`, -either on the command-line or in your `project.toml` file. +either on the command-line or in your `project.toml` file. To ignore an error in-line, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html). To ignore an individual error, add `# noqa: {code}` to the end of the line, like so: @@ -313,285 +314,286 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| F401 | UnusedImport | `...` imported but unused | 🛠 | -| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | | -| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | | -| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | | -| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | | -| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | | -| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | | -| F541 | FStringMissingPlaceholders | f-string without any placeholders | | -| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | | -| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | | -| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | | -| F622 | TwoStarredExpressions | Two starred expressions in assignment | | -| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | | -| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | 🛠 | -| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | | -| F634 | IfTuple | If test is a tuple, which is always `True` | | -| F701 | BreakOutsideLoop | `break` outside loop | | -| F702 | ContinueOutsideLoop | `continue` not properly in loop | | -| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | | -| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | | -| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | | -| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | | -| F821 | UndefinedName | Undefined name `...` | | -| F822 | UndefinedExport | Undefined name `...` in `__all__` | | -| F823 | UndefinedLocal | Local variable `...` referenced before assignment | | -| F831 | DuplicateArgumentName | Duplicate argument name in function definition | | -| F841 | UnusedVariable | Local variable `...` is assigned to but never used | | -| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | 🛠 | +| Code | Name | Message | Fix | +| ---- | ----------------------------- | ----------------------------------------------------------------- | --- | +| F401 | UnusedImport | `...` imported but unused | 🛠 | +| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | | +| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | | +| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | | +| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | | +| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | | +| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | | +| F541 | FStringMissingPlaceholders | f-string without any placeholders | | +| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | | +| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | | +| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | | +| F622 | TwoStarredExpressions | Two starred expressions in assignment | | +| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | | +| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | 🛠 | +| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | | +| F634 | IfTuple | If test is a tuple, which is always `True` | | +| F701 | BreakOutsideLoop | `break` outside loop | | +| F702 | ContinueOutsideLoop | `continue` not properly in loop | | +| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function | | +| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | | +| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | | +| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | | +| F821 | UndefinedName | Undefined name `...` | | +| F822 | UndefinedExport | Undefined name `...` in `__all__` | | +| F823 | UndefinedLocal | Local variable `...` referenced before assignment | | +| F831 | DuplicateArgumentName | Duplicate argument name in function definition | | +| F841 | UnusedVariable | Local variable `...` is assigned to but never used | | +| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | 🛠 | ### pycodestyle For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | | -| E501 | LineTooLong | Line too long (89 > 88 characters) | | -| E711 | NoneComparison | Comparison to `None` should be `cond is None` | | -| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | | -| E713 | NotInTest | Test for membership should be `not in` | | -| E714 | NotIsTest | Test for object identity should be `is not` | | -| E721 | TypeComparison | Do not compare types, use `isinstance()` | | -| E722 | DoNotUseBareExcept | Do not use bare `except` | | -| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | | -| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | | -| E742 | AmbiguousClassName | Ambiguous class name: `...` | | -| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | | -| E902 | IOError | IOError: `...` | | -| E999 | SyntaxError | SyntaxError: `...` | | -| W292 | NoNewLineAtEndOfFile | No newline at end of file | | -| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | | +| Code | Name | Message | Fix | +| ---- | -------------------------- | --------------------------------------------- | --- | +| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | | +| E501 | LineTooLong | Line too long (89 > 88 characters) | | +| E711 | NoneComparison | Comparison to `None` should be `cond is None` | | +| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | | +| E713 | NotInTest | Test for membership should be `not in` | | +| E714 | NotIsTest | Test for object identity should be `is not` | | +| E721 | TypeComparison | Do not compare types, use `isinstance()` | | +| E722 | DoNotUseBareExcept | Do not use bare `except` | | +| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | | +| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | | +| E742 | AmbiguousClassName | Ambiguous class name: `...` | | +| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | | +| E902 | IOError | IOError: `...` | | +| E999 | SyntaxError | SyntaxError: `...` | | +| W292 | NoNewLineAtEndOfFile | No newline at end of file | | +| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | | ### isort For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 | +| Code | Name | Message | Fix | +| ---- | --------------- | ----------------------------------------- | --- | +| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 | ### pydocstyle For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| D100 | PublicModule | Missing docstring in public module | | -| D101 | PublicClass | Missing docstring in public class | | -| D102 | PublicMethod | Missing docstring in public method | | -| D103 | PublicFunction | Missing docstring in public function | | -| D104 | PublicPackage | Missing docstring in public package | | -| D105 | MagicMethod | Missing docstring in magic method | | -| D106 | PublicNestedClass | Missing docstring in public nested class | | -| D107 | PublicInit | Missing docstring in `__init__` | | -| D200 | FitsOnOneLine | One-line docstring should fit on one line | | -| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | 🛠 | -| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | 🛠 | -| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | 🛠 | -| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 | -| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 | -| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | | -| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 | -| D208 | NoOverIndentation | Docstring is over-indented | 🛠 | -| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 | -| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 | -| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 | -| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | -| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | -| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 | -| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 | -| D300 | UsesTripleQuotes | Use """triple double quotes""" | | -| D400 | EndsInPeriod | First line should end with a period | | -| D402 | NoSignature | First line should not be the function's signature | | -| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | -| D404 | NoThisPrefix | First word of the docstring should not be 'This' | | -| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 | -| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 | -| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 | -| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | 🛠 | -| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 | -| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 | -| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 | -| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | 🛠 | -| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 | -| D414 | NonEmptySection | Section has no content ("Returns") | | -| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | -| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 | -| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | | -| D418 | SkipDocstring | Function decorated with `@overload` shouldn't contain a docstring | | -| D419 | NonEmpty | Docstring is empty | | +| Code | Name | Message | Fix | +| ---- | ------------------------------------ | -------------------------------------------------------------------------------- | --- | +| D100 | PublicModule | Missing docstring in public module | | +| D101 | PublicClass | Missing docstring in public class | | +| D102 | PublicMethod | Missing docstring in public method | | +| D103 | PublicFunction | Missing docstring in public function | | +| D104 | PublicPackage | Missing docstring in public package | | +| D105 | MagicMethod | Missing docstring in magic method | | +| D106 | PublicNestedClass | Missing docstring in public nested class | | +| D107 | PublicInit | Missing docstring in `__init__` | | +| D200 | FitsOnOneLine | One-line docstring should fit on one line | | +| D201 | NoBlankLineBeforeFunction | No blank lines allowed before function docstring (found 1) | 🛠 | +| D202 | NoBlankLineAfterFunction | No blank lines allowed after function docstring (found 1) | 🛠 | +| D203 | OneBlankLineBeforeClass | 1 blank line required before class docstring | 🛠 | +| D204 | OneBlankLineAfterClass | 1 blank line required after class docstring | 🛠 | +| D205 | BlankLineAfterSummary | 1 blank line required between summary line and description | 🛠 | +| D206 | IndentWithSpaces | Docstring should be indented with spaces, not tabs | | +| D207 | NoUnderIndentation | Docstring is under-indented | 🛠 | +| D208 | NoOverIndentation | Docstring is over-indented | 🛠 | +| D209 | NewLineAfterLastParagraph | Multi-line docstring closing quotes should be on a separate line | 🛠 | +| D210 | NoSurroundingWhitespace | No whitespaces allowed surrounding docstring text | 🛠 | +| D211 | NoBlankLineBeforeClass | No blank lines allowed before class docstring | 🛠 | +| D212 | MultiLineSummaryFirstLine | Multi-line docstring summary should start at the first line | | +| D213 | MultiLineSummarySecondLine | Multi-line docstring summary should start at the second line | | +| D214 | SectionNotOverIndented | Section is over-indented ("Returns") | 🛠 | +| D215 | SectionUnderlineNotOverIndented | Section underline is over-indented ("Returns") | 🛠 | +| D300 | UsesTripleQuotes | Use """triple double quotes""" | | +| D400 | EndsInPeriod | First line should end with a period | | +| D402 | NoSignature | First line should not be the function's signature | | +| D403 | FirstLineCapitalized | First word of the first line should be properly capitalized | | +| D404 | NoThisPrefix | First word of the docstring should not be 'This' | | +| D405 | CapitalizeSectionName | Section name should be properly capitalized ("returns") | 🛠 | +| D406 | NewLineAfterSectionName | Section name should end with a newline ("Returns") | 🛠 | +| D407 | DashedUnderlineAfterSection | Missing dashed underline after section ("Returns") | 🛠 | +| D408 | SectionUnderlineAfterName | Section underline should be in the line following the section's name ("Returns") | 🛠 | +| D409 | SectionUnderlineMatchesSectionLength | Section underline should match the length of its name ("Returns") | 🛠 | +| D410 | BlankLineAfterSection | Missing blank line after section ("Returns") | 🛠 | +| D411 | BlankLineBeforeSection | Missing blank line before section ("Returns") | 🛠 | +| D412 | NoBlankLinesBetweenHeaderAndContent | No blank lines allowed between a section header and its content ("Returns") | 🛠 | +| D413 | BlankLineAfterLastSection | Missing blank line after last section ("Returns") | 🛠 | +| D414 | NonEmptySection | Section has no content ("Returns") | | +| D415 | EndsInPunctuation | First line should end with a period, question mark, or exclamation point | | +| D416 | SectionNameEndsInColon | Section name should end with a colon ("Returns") | 🛠 | +| D417 | DocumentAllArguments | Missing argument descriptions in the docstring: `x`, `y` | | +| D418 | SkipDocstring | Function decorated with `@overload` shouldn't contain a docstring | | +| D419 | NonEmpty | Docstring is empty | | ### pyupgrade For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 | -| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 | -| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 | -| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 | -| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 | -| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 | -| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 | -| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 | -| U009 | PEP3120UnnecessaryCodingComment | utf-8 encoding declaration is unnecessary | 🛠 | -| U010 | UnnecessaryFutureImport | Unnessary __future__ import `...` for target Python version | | -| U011 | UnnecessaryLRUCacheParams | Unnessary parameters to functools.lru_cache | 🛠 | +| Code | Name | Message | Fix | +| ---- | ------------------------------- | ----------------------------------------------------------- | --- | +| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 | +| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | 🛠 | +| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 | +| U004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 | +| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | 🛠 | +| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 | +| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 | +| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 | +| U009 | PEP3120UnnecessaryCodingComment | utf-8 encoding declaration is unnecessary | 🛠 | +| U010 | UnnecessaryFutureImport | Unnessary **future** import `...` for target Python version | | +| U011 | UnnecessaryLRUCacheParams | Unnessary parameters to functools.lru_cache | 🛠 | ### pep8-naming For more, see [pep8-naming](https://pypi.org/project/pep8-naming/0.13.2/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| N801 | InvalidClassName | Class name `...` should use CapWords convention | | -| N802 | InvalidFunctionName | Function name `...` should be lowercase | | -| N803 | InvalidArgumentName | Argument name `...` should be lowercase | | -| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | | -| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | | -| N806 | NonLowercaseVariableInFunction | Variable `...` in function should be lowercase | | -| N807 | DunderFunctionName | Function name should not start and end with `__` | | -| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | | -| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | | -| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | | -| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | | -| N815 | MixedCaseVariableInClassScope | Variable `mixedCase` in class scope should not be mixedCase | | -| N816 | MixedCaseVariableInGlobalScope | Variable `mixedCase` in global scope should not be mixedCase | | -| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | | -| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | | +| Code | Name | Message | Fix | +| ---- | -------------------------------------- | ------------------------------------------------------------ | --- | +| N801 | InvalidClassName | Class name `...` should use CapWords convention | | +| N802 | InvalidFunctionName | Function name `...` should be lowercase | | +| N803 | InvalidArgumentName | Argument name `...` should be lowercase | | +| N804 | InvalidFirstArgumentNameForClassMethod | First argument of a class method should be named `cls` | | +| N805 | InvalidFirstArgumentNameForMethod | First argument of a method should be named `self` | | +| N806 | NonLowercaseVariableInFunction | Variable `...` in function should be lowercase | | +| N807 | DunderFunctionName | Function name should not start and end with `__` | | +| N811 | ConstantImportedAsNonConstant | Constant `...` imported as non-constant `...` | | +| N812 | LowercaseImportedAsNonLowercase | Lowercase `...` imported as non-lowercase `...` | | +| N813 | CamelcaseImportedAsLowercase | Camelcase `...` imported as lowercase `...` | | +| N814 | CamelcaseImportedAsConstant | Camelcase `...` imported as constant `...` | | +| N815 | MixedCaseVariableInClassScope | Variable `mixedCase` in class scope should not be mixedCase | | +| N816 | MixedCaseVariableInGlobalScope | Variable `mixedCase` in global scope should not be mixedCase | | +| N817 | CamelcaseImportedAsAcronym | Camelcase `...` imported as acronym `...` | | +| N818 | ErrorSuffixOnExceptionName | Exception name `...` should be named with an Error suffix | | ### flake8-comprehensions For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/3.10.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| C400 | UnnecessaryGeneratorList | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 | -| C401 | UnnecessaryGeneratorSet | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 | -| C402 | UnnecessaryGeneratorDict | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 | -| C403 | UnnecessaryListComprehensionSet | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | 🛠 | -| C404 | UnnecessaryListComprehensionDict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | 🛠 | -| C405 | UnnecessaryLiteralSet | Unnecessary `(list\|tuple)` literal (rewrite as a `set` literal) | 🛠 | -| C406 | UnnecessaryLiteralDict | Unnecessary `(list\|tuple)` literal (rewrite as a `dict` literal) | 🛠 | -| C408 | UnnecessaryCollectionCall | Unnecessary `(dict\|list\|tuple)` call (rewrite as a literal) | 🛠 | -| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | 🛠 | -| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | 🛠 | -| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | 🛠 | -| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | | -| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary `(list\|reversed\|set\|sorted\|tuple)` call within `(list\|set\|sorted\|tuple)()` | | -| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within `(reversed\|set\|sorted)()` | | -| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 | -| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | | +| Code | Name | Message | Fix | +| ---- | --------------------------------- | --------------------------------------------------------------------------------------------- | --- | +| C400 | UnnecessaryGeneratorList | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 | +| C401 | UnnecessaryGeneratorSet | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 | +| C402 | UnnecessaryGeneratorDict | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 | +| C403 | UnnecessaryListComprehensionSet | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | 🛠 | +| C404 | UnnecessaryListComprehensionDict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | 🛠 | +| C405 | UnnecessaryLiteralSet | Unnecessary `(list\|tuple)` literal (rewrite as a `set` literal) | 🛠 | +| C406 | UnnecessaryLiteralDict | Unnecessary `(list\|tuple)` literal (rewrite as a `dict` literal) | 🛠 | +| C408 | UnnecessaryCollectionCall | Unnecessary `(dict\|list\|tuple)` call (rewrite as a literal) | 🛠 | +| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | 🛠 | +| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | 🛠 | +| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | 🛠 | +| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | | +| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary `(list\|reversed\|set\|sorted\|tuple)` call within `(list\|set\|sorted\|tuple)()` | | +| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within `(reversed\|set\|sorted)()` | | +| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 | +| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | | ### flake8-bugbear For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | | -| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment. | | -| B004 | UnreliableCallableCheck | Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results. | | -| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader. | | -| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | | -| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 | -| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | | -| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 | -| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | | -| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 | -| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | | -| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 | -| B015 | UselessComparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | | -| B016 | CannotRaiseLiteral | Cannot raise a literal. Did you intend to return it or raise an Exception? | | -| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | | -| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | | -| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | | -| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged. | | +| Code | Name | Message | Fix | +| ---- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --- | +| B002 | UnaryPrefixIncrement | Python does not support the unary prefix increment. | | +| B003 | AssignmentToOsEnviron | Assigning to `os.environ` doesn't clear the environment. | | +| B004 | UnreliableCallableCheck | Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results. | | +| B005 | StripWithMultiCharacters | Using `.strip()` with multi-character strings is misleading the reader. | | +| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | | +| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 | +| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | | +| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 | +| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | | +| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 | +| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | | +| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 | +| B015 | UselessComparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | | +| B016 | CannotRaiseLiteral | Cannot raise a literal. Did you intend to return it or raise an Exception? | | +| B017 | NoAssertRaisesException | `assertRaises(Exception):` should be considered evil. | | +| B018 | UselessExpression | Found useless expression. Either assign it to a variable or remove it. | | +| B019 | CachedInstanceMethod | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks. | | +| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | | +| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged. | | ### flake8-builtins For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/2.0.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | -| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | -| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | +| Code | Name | Message | Fix | +| ---- | ------------------------- | --------------------------------------------------- | --- | +| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | +| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | +| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | ### flake8-print For more, see [flake8-print](https://pypi.org/project/flake8-print/5.0.0/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| T201 | PrintFound | `print` found | 🛠 | -| T203 | PPrintFound | `pprint` found | 🛠 | +| Code | Name | Message | Fix | +| ---- | ----------- | -------------- | --- | +| T201 | PrintFound | `print` found | 🛠 | +| T203 | PPrintFound | `pprint` found | 🛠 | ### flake8-quotes For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | | -| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | | -| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | | -| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | | +| Code | Name | Message | Fix | +| ---- | ------------------------ | -------------------------------------------------------- | --- | +| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | | +| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | | +| 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 `*...` | | -| ANN003 | MissingTypeKwargs | Missing type annotation for `**...` | | -| ANN101 | MissingTypeSelf | Missing type annotation for `...` in method | | -| ANN102 | MissingTypeCls | Missing type annotation for `...` 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 `...` | | -| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | | +| Code | Name | Message | Fix | +| ------ | -------------------------------- | ------------------------------------------------------------------ | --- | +| ANN001 | MissingTypeFunctionArgument | Missing type annotation for function argument `...` | | +| ANN002 | MissingTypeArgs | Missing type annotation for `*...` | | +| ANN003 | MissingTypeKwargs | Missing type annotation for `**...` | | +| ANN101 | MissingTypeSelf | Missing type annotation for `...` in method | | +| ANN102 | MissingTypeCls | Missing type annotation for `...` 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 `...` | | +| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | | ### flake8-2020 For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI. -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| YTT101 | SysVersionSlice3Referenced | `sys.version[:3]` referenced (python3.10), use `sys.version_info` | | -| YTT102 | SysVersion2Referenced | `sys.version[2]` referenced (python3.10), use `sys.version_info` | | -| YTT103 | SysVersionCmpStr3 | `sys.version` compared to string (python3.10), use `sys.version_info` | | -| YTT201 | SysVersionInfo0Eq3Referenced | `sys.version_info[0] == 3` referenced (python4), use `>=` | | -| YTT202 | SixPY3Referenced | `six.PY3` referenced (python4), use `not six.PY2` | | -| YTT203 | SysVersionInfo1CmpInt | `sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to tuple | | -| YTT204 | SysVersionInfoMinorCmpInt | `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple | | -| YTT301 | SysVersion0Referenced | `sys.version[0]` referenced (python10), use `sys.version_info` | | -| YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | | -| YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | | +| Code | Name | Message | Fix | +| ------ | ---------------------------- | ------------------------------------------------------------------------------------------- | --- | +| YTT101 | SysVersionSlice3Referenced | `sys.version[:3]` referenced (python3.10), use `sys.version_info` | | +| YTT102 | SysVersion2Referenced | `sys.version[2]` referenced (python3.10), use `sys.version_info` | | +| YTT103 | SysVersionCmpStr3 | `sys.version` compared to string (python3.10), use `sys.version_info` | | +| YTT201 | SysVersionInfo0Eq3Referenced | `sys.version_info[0] == 3` referenced (python4), use `>=` | | +| YTT202 | SixPY3Referenced | `six.PY3` referenced (python4), use `not six.PY2` | | +| YTT203 | SysVersionInfo1CmpInt | `sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to tuple | | +| YTT204 | SysVersionInfoMinorCmpInt | `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple | | +| YTT301 | SysVersion0Referenced | `sys.version[0]` referenced (python10), use `sys.version_info` | | +| YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | | +| YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | | ### Ruff-specific rules -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| RUF001 | AmbiguousUnicodeCharacterString | String contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 | -| RUF002 | AmbiguousUnicodeCharacterDocstring | Docstring contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 | -| RUF003 | AmbiguousUnicodeCharacterComment | Comment contains ambiguous unicode character '𝐁' (did you mean 'B'?) | | +| Code | Name | Message | Fix | +| ------ | ---------------------------------- | ---------------------------------------------------------------------- | --- | +| RUF001 | AmbiguousUnicodeCharacterString | String contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 | +| RUF002 | AmbiguousUnicodeCharacterDocstring | Docstring contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 | +| RUF003 | AmbiguousUnicodeCharacterComment | Comment contains ambiguous unicode character '𝐁' (did you mean 'B'?) | | ### Meta rules -| Code | Name | Message | Fix | -| ---- | ---- | ------- | --- | -| M001 | UnusedNOQA | Unused `noqa` directive | 🛠 | +| Code | Name | Message | Fix | +| ---- | ---------- | ----------------------- | --- | +| M001 | UnusedNOQA | Unused `noqa` directive | 🛠 | <!-- End auto-generated sections. --> @@ -684,7 +686,7 @@ including: - [`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/) (20/32) +- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32) - [`flake8-2020`](https://pypi.org/project/flake8-2020/) - [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34) - [`autoflake`](https://pypi.org/project/autoflake/) (1/7) @@ -708,7 +710,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl - [`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/) (20/32) +- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (21/32) - [`flake8-2020`](https://pypi.org/project/flake8-2020/) Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa), diff --git a/resources/test/fixtures/B019.py b/resources/test/fixtures/B019.py new file mode 100644 index 00000000000000..55d32aa1bad6eb --- /dev/null +++ b/resources/test/fixtures/B019.py @@ -0,0 +1,108 @@ +""" +Should emit: +B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101 +""" +import functools +from functools import cache, cached_property, lru_cache + + +def some_other_cache(): + ... + + +@functools.cache +def compute_func(self, y): + ... + + +class Foo: + def __init__(self, x): + self.x = x + + def compute_method(self, y): + ... + + @some_other_cache + def user_cached_instance_method(self, y): + ... + + @classmethod + @functools.cache + def cached_classmethod(cls, y): + ... + + @classmethod + @cache + def other_cached_classmethod(cls, y): + ... + + @classmethod + @functools.lru_cache + def lru_cached_classmethod(cls, y): + ... + + @classmethod + @lru_cache + def other_lru_cached_classmethod(cls, y): + ... + + @staticmethod + @functools.cache + def cached_staticmethod(y): + ... + + @staticmethod + @cache + def other_cached_staticmethod(y): + ... + + @staticmethod + @functools.lru_cache + def lru_cached_staticmethod(y): + ... + + @staticmethod + @lru_cache + def other_lru_cached_staticmethod(y): + ... + + @functools.cached_property + def some_cached_property(self): + ... + + @cached_property + def some_other_cached_property(self): + ... + + # Remaining methods should emit B019 + @functools.cache + def cached_instance_method(self, y): + ... + + @cache + def another_cached_instance_method(self, y): + ... + + @functools.cache() + def called_cached_instance_method(self, y): + ... + + @cache() + def another_called_cached_instance_method(self, y): + ... + + @functools.lru_cache + def lru_cached_instance_method(self, y): + ... + + @lru_cache + def another_lru_cached_instance_method(self, y): + ... + + @functools.lru_cache() + def called_lru_cached_instance_method(self, y): + ... + + @lru_cache() + def another_called_lru_cached_instance_method(self, y): + ... diff --git a/src/check_ast.rs b/src/check_ast.rs index ede664ac3f9ac8..0caf1f9b6a1cad 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -346,6 +346,9 @@ where if self.settings.enabled.contains(&CheckCode::B018) { flake8_bugbear::plugins::useless_expression(self, body); } + if self.settings.enabled.contains(&CheckCode::B019) { + flake8_bugbear::plugins::cached_instance_method(self, decorator_list); + } self.check_builtin_shadowing(name, Range::from_located(stmt), true); diff --git a/src/checks.rs b/src/checks.rs index f8e9a8e6019b21..325d9830691429 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -93,6 +93,7 @@ pub enum CheckCode { B016, B017, B018, + B019, B025, B026, // flake8-comprehensions @@ -380,6 +381,7 @@ pub enum CheckKind { CannotRaiseLiteral, NoAssertRaisesException, UselessExpression, + CachedInstanceMethod, DuplicateTryBlockException(String), StarArgUnpackingAfterKeywordArg, // flake8-comprehensions @@ -610,6 +612,7 @@ impl CheckCode { CheckCode::B016 => CheckKind::CannotRaiseLiteral, CheckCode::B017 => CheckKind::NoAssertRaisesException, CheckCode::B018 => CheckKind::UselessExpression, + CheckCode::B019 => CheckKind::CachedInstanceMethod, CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()), CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg, // flake8-comprehensions @@ -841,6 +844,7 @@ impl CheckCode { CheckCode::B016 => CheckCategory::Flake8Bugbear, CheckCode::B017 => CheckCategory::Flake8Bugbear, CheckCode::B018 => CheckCategory::Flake8Bugbear, + CheckCode::B019 => CheckCategory::Flake8Bugbear, CheckCode::B025 => CheckCategory::Flake8Bugbear, CheckCode::B026 => CheckCategory::Flake8Bugbear, CheckCode::C400 => CheckCategory::Flake8Comprehensions, @@ -1036,6 +1040,7 @@ impl CheckKind { CheckKind::CannotRaiseLiteral => &CheckCode::B016, CheckKind::NoAssertRaisesException => &CheckCode::B017, CheckKind::UselessExpression => &CheckCode::B018, + CheckKind::CachedInstanceMethod => &CheckCode::B019, CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025, CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026, // flake8-comprehensions @@ -1388,6 +1393,11 @@ impl CheckKind { CheckKind::UselessExpression => { "Found useless expression. Either assign it to a variable or remove it.".to_string() } + CheckKind::CachedInstanceMethod => { + "Use of `functools.lru_cache` or `functools.cache` on \ + methods can lead to memory leaks." + .to_string() + } CheckKind::DuplicateTryBlockException(name) => { format!("try-except block with duplicate exception `{name}`") } diff --git a/src/checks_gen.rs b/src/checks_gen.rs index a45d6ed14c1023..daf0409ce075fa 100644 --- a/src/checks_gen.rs +++ b/src/checks_gen.rs @@ -53,6 +53,7 @@ pub enum CheckCodePrefix { B016, B017, B018, + B019, B02, B025, B026, @@ -368,6 +369,7 @@ impl CheckCodePrefix { CheckCode::B016, CheckCode::B017, CheckCode::B018, + CheckCode::B019, CheckCode::B025, CheckCode::B026, ], @@ -388,6 +390,7 @@ impl CheckCodePrefix { CheckCode::B016, CheckCode::B017, CheckCode::B018, + CheckCode::B019, CheckCode::B025, CheckCode::B026, ], @@ -418,6 +421,7 @@ impl CheckCodePrefix { CheckCode::B016, CheckCode::B017, CheckCode::B018, + CheckCode::B019, ], CheckCodePrefix::B010 => vec![CheckCode::B010], CheckCodePrefix::B011 => vec![CheckCode::B011], @@ -427,6 +431,7 @@ impl CheckCodePrefix { CheckCodePrefix::B016 => vec![CheckCode::B016], CheckCodePrefix::B017 => vec![CheckCode::B017], CheckCodePrefix::B018 => vec![CheckCode::B018], + CheckCodePrefix::B019 => vec![CheckCode::B019], CheckCodePrefix::B02 => vec![CheckCode::B025, CheckCode::B026], CheckCodePrefix::B025 => vec![CheckCode::B025], CheckCodePrefix::B026 => vec![CheckCode::B026], @@ -1134,6 +1139,7 @@ impl CheckCodePrefix { CheckCodePrefix::B016 => PrefixSpecificity::Explicit, CheckCodePrefix::B017 => PrefixSpecificity::Explicit, CheckCodePrefix::B018 => PrefixSpecificity::Explicit, + CheckCodePrefix::B019 => PrefixSpecificity::Explicit, CheckCodePrefix::B02 => PrefixSpecificity::Tens, CheckCodePrefix::B025 => PrefixSpecificity::Explicit, CheckCodePrefix::B026 => PrefixSpecificity::Explicit, diff --git a/src/flake8_bugbear/plugins/cached_instance_method.rs b/src/flake8_bugbear/plugins/cached_instance_method.rs new file mode 100644 index 00000000000000..df2c99b92dbaf5 --- /dev/null +++ b/src/flake8_bugbear/plugins/cached_instance_method.rs @@ -0,0 +1,32 @@ +use rustpython_ast::Expr; + +use crate::ast::helpers::compose_call_path; +use crate::ast::types::Range; +use crate::ast::types::ScopeKind; +use crate::check_ast::Checker; +use crate::checks::{Check, CheckKind}; + +const CACHE_FUNCTIONS: [&str; 4] = [ + "functools.lru_cache", + "functools.cache", + "lru_cache", + "cache", +]; + +pub fn cached_instance_method(checker: &mut Checker, decorator_list: &[Expr]) { + if matches!(checker.current_scope().kind, ScopeKind::Class(_)) { + for decorator in decorator_list { + if let Some(decorator_path) = compose_call_path(decorator) { + if decorator_path == "classmethod" || decorator_path == "staticmethod" { + return; + } + if CACHE_FUNCTIONS.contains(&decorator_path.as_str()) { + checker.add_check(Check::new( + CheckKind::CachedInstanceMethod, + Range::from_located(decorator), + )); + } + } + } + } +} diff --git a/src/flake8_bugbear/plugins/mod.rs b/src/flake8_bugbear/plugins/mod.rs index 2b2781690b8cda..186076725021e8 100644 --- a/src/flake8_bugbear/plugins/mod.rs +++ b/src/flake8_bugbear/plugins/mod.rs @@ -1,6 +1,7 @@ pub use assert_false::assert_false; pub use assert_raises_exception::assert_raises_exception; pub use assignment_to_os_environ::assignment_to_os_environ; +pub use cached_instance_method::cached_instance_method; pub use cannot_raise_literal::cannot_raise_literal; pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions}; pub use function_call_argument_default::function_call_argument_default; @@ -19,6 +20,7 @@ pub use useless_expression::useless_expression; mod assert_false; mod assert_raises_exception; mod assignment_to_os_environ; +mod cached_instance_method; mod cannot_raise_literal; mod duplicate_exceptions; mod function_call_argument_default; diff --git a/src/linter.rs b/src/linter.rs index 4350b1c47c8024..2351893f428911 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -342,6 +342,7 @@ mod tests { #[test_case(CheckCode::B016, Path::new("B016.py"); "B016")] #[test_case(CheckCode::B017, Path::new("B017.py"); "B017")] #[test_case(CheckCode::B018, Path::new("B018.py"); "B018")] + #[test_case(CheckCode::B019, Path::new("B019.py"); "B019")] #[test_case(CheckCode::B025, Path::new("B025.py"); "B025")] #[test_case(CheckCode::B026, Path::new("B026.py"); "B026")] #[test_case(CheckCode::C400, Path::new("C400.py"); "C400")] diff --git a/src/snapshots/ruff__linter__tests__B019_B019.py.snap b/src/snapshots/ruff__linter__tests__B019_B019.py.snap new file mode 100644 index 00000000000000..7980a548efb9b1 --- /dev/null +++ b/src/snapshots/ruff__linter__tests__B019_B019.py.snap @@ -0,0 +1,69 @@ +--- +source: src/linter.rs +expression: checks +--- +- kind: CachedInstanceMethod + location: + row: 78 + column: 5 + end_location: + row: 78 + column: 20 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 82 + column: 5 + end_location: + row: 82 + column: 10 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 86 + column: 5 + end_location: + row: 86 + column: 22 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 90 + column: 5 + end_location: + row: 90 + column: 12 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 94 + column: 5 + end_location: + row: 94 + column: 24 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 98 + column: 5 + end_location: + row: 98 + column: 14 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 102 + column: 5 + end_location: + row: 102 + column: 26 + fix: ~ +- kind: CachedInstanceMethod + location: + row: 106 + column: 5 + end_location: + row: 106 + column: 16 + fix: ~ +