mirror of https://github.com/astral-sh/ruff
[`pyflakes`] Handle some common submodule import situations for `unused-import` (`F401`) (#20200)
# Summary The PR under review attempts to make progress towards the age-old problem of submodule imports, specifically with regards to their treatment by the rule [`unused-import` (`F401`)](https://docs.astral.sh/ruff/rules/unused-import/). Some related issues: - https://github.com/astral-sh/ruff/issues/60 - https://github.com/astral-sh/ruff/issues/4656 Prior art: - https://github.com/astral-sh/ruff/pull/13965 - https://github.com/astral-sh/ruff/pull/5010 - https://github.com/astral-sh/ruff/pull/5011 - https://github.com/astral-sh/ruff/pull/666 See the PR summary for a detailed description.
This commit is contained in:
parent
3932f7c849
commit
57e1ff8294
|
|
@ -44,6 +44,43 @@ import some_module
|
|||
__all__ = ["some_module"]
|
||||
```
|
||||
|
||||
## Preview
|
||||
When [preview] is enabled (and certain simplifying assumptions
|
||||
are met), we analyze all import statements for a given module
|
||||
when determining whether an import is used, rather than simply
|
||||
the last of these statements. This can result in both different and
|
||||
more import statements being marked as unused.
|
||||
|
||||
For example, if a module consists of
|
||||
|
||||
```python
|
||||
import a
|
||||
import a.b
|
||||
```
|
||||
|
||||
then both statements are marked as unused under [preview], whereas
|
||||
only the second is marked as unused under stable behavior.
|
||||
|
||||
As another example, if a module consists of
|
||||
|
||||
```python
|
||||
import a.b
|
||||
import a
|
||||
|
||||
a.b.foo()
|
||||
```
|
||||
|
||||
then a diagnostic will only be emitted for the first line under [preview],
|
||||
whereas a diagnostic would only be emitted for the second line under
|
||||
stable behavior.
|
||||
|
||||
Note that this behavior is somewhat subjective and is designed
|
||||
to conform to the developer's intuition rather than Python's actual
|
||||
execution. To wit, the statement `import a.b` automatically executes
|
||||
`import a`, so in some sense `import a` is _always_ redundant
|
||||
in the presence of `import a.b`.
|
||||
|
||||
|
||||
## Fix safety
|
||||
|
||||
Fixes to remove unused imports are safe, except in `__init__.py` files.
|
||||
|
|
@ -96,4 +133,6 @@ else:
|
|||
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
|
||||
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
|
||||
|
||||
[preview]: https://docs.astral.sh/ruff/preview/
|
||||
|
||||
----- stderr -----
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ exit_code: 1
|
|||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ expression: value
|
|||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
|
|
|||
|
|
@ -235,3 +235,8 @@ pub(crate) const fn is_a003_class_scope_shadowing_expansion_enabled(
|
|||
) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/20200
|
||||
pub(crate) const fn is_refined_submodule_import_match_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ mod tests {
|
|||
use crate::settings::{LinterSettings, flags};
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{test_contents, test_path, test_snippet};
|
||||
use crate::{Locator, assert_diagnostics, directives};
|
||||
use crate::{Locator, assert_diagnostics, assert_diagnostics_diff, directives};
|
||||
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_0.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_1.py"))]
|
||||
|
|
@ -392,6 +392,154 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_0.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_1.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_2.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_3.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_4.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_5.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_6.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_7.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_8.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_9.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_10.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_11.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_12.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_13.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_15.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_16.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_17.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_18.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_20.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_21.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_22.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_23.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_32.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_34.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_35.py"))]
|
||||
fn f401_preview_refined_submodule_handling_diffs(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("preview_diff__{}", path.to_string_lossy());
|
||||
assert_diagnostics_diff!(
|
||||
snapshot,
|
||||
Path::new("pyflakes").join(path).as_path(),
|
||||
&LinterSettings::for_rule(rule_code),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(rule_code)
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
r"
|
||||
import a
|
||||
import a.b
|
||||
import a.c",
|
||||
"f401_multiple_unused_submodules"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
import a
|
||||
import a.b
|
||||
a.foo()",
|
||||
"f401_use_top_member"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
import a
|
||||
import a.b
|
||||
a.foo()
|
||||
a.bar()",
|
||||
"f401_use_top_member_twice"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# reverts to stable behavior - used between imports
|
||||
import a
|
||||
a.foo()
|
||||
import a.b",
|
||||
"f401_use_top_member_before_second_import"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# reverts to stable behavior - used between imports
|
||||
import a
|
||||
a.foo()
|
||||
a = 1
|
||||
import a.b",
|
||||
"f401_use_top_member_and_redefine_before_second_import"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# reverts to stable behavior - used between imports
|
||||
import a
|
||||
a.foo()
|
||||
import a.b
|
||||
a = 1",
|
||||
"f401_use_top_member_then_import_then_redefine"
|
||||
)]
|
||||
#[test_case(
|
||||
r#"
|
||||
import a
|
||||
import a.b
|
||||
__all__ = ["a"]"#,
|
||||
"f401_use_in_dunder_all"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
import a.c
|
||||
import a.b
|
||||
a.foo()",
|
||||
"f401_import_submodules_but_use_top_level"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
import a.c
|
||||
import a.b.d
|
||||
a.foo()",
|
||||
"f401_import_submodules_different_lengths_but_use_top_level"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# refined logic only applied _within_ scope
|
||||
import a
|
||||
def foo():
|
||||
import a.b
|
||||
a.foo()",
|
||||
"f401_import_submodules_in_function_scope"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# reverts to stable behavior - used between bindings
|
||||
import a
|
||||
a.b
|
||||
import a.b",
|
||||
"f401_use_in_between_imports"
|
||||
)]
|
||||
#[test_case(
|
||||
r"
|
||||
# reverts to stable behavior - used between bindings
|
||||
import a.b
|
||||
a
|
||||
import a",
|
||||
"f401_use_in_between_imports"
|
||||
)]
|
||||
fn f401_preview_refined_submodule_handling(contents: &str, snapshot: &str) {
|
||||
let diagnostics = test_contents(
|
||||
&SourceKind::Python(dedent(contents).to_string()),
|
||||
Path::new("f401_preview_submodule.py"),
|
||||
&LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..LinterSettings::for_rule(Rule::UnusedImport)
|
||||
},
|
||||
)
|
||||
.0;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f841_dummy_variable_rgx() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
|
|
|||
|
|
@ -5,19 +5,22 @@ use anyhow::{Result, anyhow, bail};
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::name::{QualifiedName, QualifiedNameBuilder};
|
||||
use ruff_python_ast::{self as ast, Stmt};
|
||||
use ruff_python_semantic::{
|
||||
AnyImport, BindingKind, Exceptions, Imported, NodeId, Scope, ScopeId, SemanticModel,
|
||||
SubmoduleImport,
|
||||
AnyImport, Binding, BindingFlags, BindingId, BindingKind, Exceptions, Imported, NodeId, Scope,
|
||||
ScopeId, SemanticModel, SubmoduleImport,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
use crate::preview::is_dunder_init_fix_unused_import_enabled;
|
||||
use crate::preview::{
|
||||
is_dunder_init_fix_unused_import_enabled, is_refined_submodule_import_match_enabled,
|
||||
};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{isort, isort::ImportSection, isort::ImportType};
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Applicability, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
|
|
@ -49,6 +52,43 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
|
|||
/// __all__ = ["some_module"]
|
||||
/// ```
|
||||
///
|
||||
/// ## Preview
|
||||
/// When [preview] is enabled (and certain simplifying assumptions
|
||||
/// are met), we analyze all import statements for a given module
|
||||
/// when determining whether an import is used, rather than simply
|
||||
/// the last of these statements. This can result in both different and
|
||||
/// more import statements being marked as unused.
|
||||
///
|
||||
/// For example, if a module consists of
|
||||
///
|
||||
/// ```python
|
||||
/// import a
|
||||
/// import a.b
|
||||
/// ```
|
||||
///
|
||||
/// then both statements are marked as unused under [preview], whereas
|
||||
/// only the second is marked as unused under stable behavior.
|
||||
///
|
||||
/// As another example, if a module consists of
|
||||
///
|
||||
/// ```python
|
||||
/// import a.b
|
||||
/// import a
|
||||
///
|
||||
/// a.b.foo()
|
||||
/// ```
|
||||
///
|
||||
/// then a diagnostic will only be emitted for the first line under [preview],
|
||||
/// whereas a diagnostic would only be emitted for the second line under
|
||||
/// stable behavior.
|
||||
///
|
||||
/// Note that this behavior is somewhat subjective and is designed
|
||||
/// to conform to the developer's intuition rather than Python's actual
|
||||
/// execution. To wit, the statement `import a.b` automatically executes
|
||||
/// `import a`, so in some sense `import a` is _always_ redundant
|
||||
/// in the presence of `import a.b`.
|
||||
///
|
||||
///
|
||||
/// ## Fix safety
|
||||
///
|
||||
/// Fixes to remove unused imports are safe, except in `__init__.py` files.
|
||||
|
|
@ -100,6 +140,8 @@ use crate::{Applicability, Fix, FixAvailability, Violation};
|
|||
/// - [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
|
||||
/// - [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
|
||||
/// - [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct UnusedImport {
|
||||
/// Qualified name of the import
|
||||
|
|
@ -284,17 +326,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope) {
|
|||
let mut unused: BTreeMap<(NodeId, Exceptions), Vec<ImportBinding>> = BTreeMap::default();
|
||||
let mut ignored: BTreeMap<(NodeId, Exceptions), Vec<ImportBinding>> = BTreeMap::default();
|
||||
|
||||
for binding_id in scope.binding_ids() {
|
||||
let binding = checker.semantic().binding(binding_id);
|
||||
|
||||
if binding.is_used()
|
||||
|| binding.is_explicit_export()
|
||||
|| binding.is_nonlocal()
|
||||
|| binding.is_global()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for binding in unused_imports_in_scope(checker.semantic(), scope, checker.settings()) {
|
||||
let Some(import) = binding.as_any_import() else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -586,3 +618,302 @@ fn fix_by_reexporting<'a>(
|
|||
let isolation = Checker::isolation(checker.semantic().parent_statement_id(node_id));
|
||||
Ok(Fix::safe_edits(head, tail).isolate(isolation))
|
||||
}
|
||||
|
||||
/// Returns an iterator over bindings to import statements that appear unused.
|
||||
///
|
||||
/// The stable behavior is to return those bindings to imports
|
||||
/// satisfying the following properties:
|
||||
///
|
||||
/// - they are not shadowed
|
||||
/// - they are not `global`, not `nonlocal`, and not explicit exports (i.e. `import foo as foo`)
|
||||
/// - they have no references, according to the semantic model
|
||||
///
|
||||
/// Under preview, there is a more refined analysis performed
|
||||
/// in the case where all bindings shadowed by a given import
|
||||
/// binding (including the binding itself) are of a simple form:
|
||||
/// they are required to be un-aliased imports or submodule imports.
|
||||
///
|
||||
/// This alternative analysis is described in the documentation for
|
||||
/// [`unused_imports_from_binding`].
|
||||
fn unused_imports_in_scope<'a, 'b>(
|
||||
semantic: &'a SemanticModel<'b>,
|
||||
scope: &'a Scope,
|
||||
settings: &'a LinterSettings,
|
||||
) -> impl Iterator<Item = &'a Binding<'b>> {
|
||||
scope
|
||||
.binding_ids()
|
||||
.map(|id| (id, semantic.binding(id)))
|
||||
.filter(|(_, bdg)| {
|
||||
matches!(
|
||||
bdg.kind,
|
||||
BindingKind::Import(_)
|
||||
| BindingKind::FromImport(_)
|
||||
| BindingKind::SubmoduleImport(_)
|
||||
)
|
||||
})
|
||||
.filter(|(_, bdg)| !bdg.is_global() && !bdg.is_nonlocal() && !bdg.is_explicit_export())
|
||||
.flat_map(|(id, bdg)| {
|
||||
if is_refined_submodule_import_match_enabled(settings)
|
||||
// No need to apply refined logic if there is only a single binding
|
||||
&& scope.shadowed_bindings(id).nth(1).is_some()
|
||||
// Only apply the new logic in certain situations to avoid
|
||||
// complexity, false positives, and intersection with
|
||||
// `redefined-while-unused` (`F811`).
|
||||
&& has_simple_shadowed_bindings(scope, id, semantic)
|
||||
{
|
||||
unused_imports_from_binding(semantic, id, scope)
|
||||
} else if bdg.is_used() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![bdg]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a `Vec` of bindings to unused import statements that
|
||||
/// are shadowed by a given binding.
|
||||
///
|
||||
/// This is best explained by example. So suppose we have:
|
||||
///
|
||||
/// ```python
|
||||
/// import a
|
||||
/// import a.b
|
||||
/// import a.b.c
|
||||
///
|
||||
/// __all__ = ["a"]
|
||||
///
|
||||
/// a.b.foo()
|
||||
/// ```
|
||||
///
|
||||
/// As of 2025-09-25, Ruff's semantic model, upon visiting
|
||||
/// the whole module, will have a single live binding for
|
||||
/// the symbol `a` that points to the line `import a.b.c`,
|
||||
/// and the remaining two import bindings are considered shadowed
|
||||
/// by the last.
|
||||
///
|
||||
/// This function expects to receive the `id`
|
||||
/// for the live binding and will begin by collecting
|
||||
/// all bindings shadowed by the given one - i.e. all
|
||||
/// the different import statements binding the symbol `a`.
|
||||
/// We iterate over references to this
|
||||
/// module and decide (somewhat subjectively) which
|
||||
/// import statement the user "intends" to reference. To that end,
|
||||
/// to each reference we attempt to build a [`QualifiedName`]
|
||||
/// corresponding to an iterated attribute access (e.g. `a.b.foo`).
|
||||
/// We then determine the closest matching import statement to that
|
||||
/// qualified name, and mark it as used.
|
||||
///
|
||||
/// In the present example, the qualified name associated to the
|
||||
/// reference from the dunder all export is `"a"` and the qualified
|
||||
/// name associated to the reference in the last line is `"a.b.foo"`.
|
||||
/// The closest matches are `import a` and `import a.b`, respectively,
|
||||
/// leaving `import a.b.c` unused.
|
||||
///
|
||||
/// For a precise definition of "closest match" see [`best_match`]
|
||||
/// and [`rank_matches`].
|
||||
///
|
||||
/// Note: if any reference comes from something other than
|
||||
/// a `Name` or a dunder all expression, then we return just
|
||||
/// the original binding, thus reverting the stable behavior.
|
||||
fn unused_imports_from_binding<'a, 'b>(
|
||||
semantic: &'a SemanticModel<'b>,
|
||||
id: BindingId,
|
||||
scope: &'a Scope,
|
||||
) -> Vec<&'a Binding<'b>> {
|
||||
let mut marked = MarkedBindings::from_binding_id(semantic, id, scope);
|
||||
|
||||
let binding = semantic.binding(id);
|
||||
|
||||
// ensure we only do this once
|
||||
let mut marked_dunder_all = false;
|
||||
|
||||
for ref_id in binding.references() {
|
||||
let resolved_reference = semantic.reference(ref_id);
|
||||
if !marked_dunder_all && resolved_reference.in_dunder_all_definition() {
|
||||
let first = *binding
|
||||
.as_any_import()
|
||||
.expect("binding to be import binding since current function called after restricting to these in `unused_imports_in_scope`")
|
||||
.qualified_name()
|
||||
.segments().first().expect("import binding to have nonempty qualified name");
|
||||
mark_uses_of_qualified_name(&mut marked, &QualifiedName::user_defined(first));
|
||||
marked_dunder_all = true;
|
||||
continue;
|
||||
}
|
||||
let Some(expr_id) = resolved_reference.expression_id() else {
|
||||
// If there is some other kind of reference, abandon
|
||||
// the refined approach for the usual one
|
||||
return vec![binding];
|
||||
};
|
||||
let Some(prototype) = expand_to_qualified_name_attribute(semantic, expr_id) else {
|
||||
return vec![binding];
|
||||
};
|
||||
|
||||
mark_uses_of_qualified_name(&mut marked, &prototype);
|
||||
}
|
||||
|
||||
marked.into_unused()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MarkedBindings<'a, 'b> {
|
||||
bindings: Vec<&'a Binding<'b>>,
|
||||
used: Vec<bool>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> MarkedBindings<'a, 'b> {
|
||||
fn from_binding_id(semantic: &'a SemanticModel<'b>, id: BindingId, scope: &'a Scope) -> Self {
|
||||
let bindings: Vec<_> = scope
|
||||
.shadowed_bindings(id)
|
||||
.map(|id| semantic.binding(id))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
used: vec![false; bindings.len()],
|
||||
bindings,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_unused(self) -> Vec<&'a Binding<'b>> {
|
||||
self.bindings
|
||||
.into_iter()
|
||||
.zip(self.used)
|
||||
.filter_map(|(bdg, is_used)| (!is_used).then_some(bdg))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn iter_mut(&mut self) -> impl Iterator<Item = (&'a Binding<'b>, &mut bool)> {
|
||||
self.bindings.iter().copied().zip(self.used.iter_mut())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Some` [`QualifiedName`] delineating the path for the
|
||||
/// maximal [`ExprName`] or [`ExprAttribute`] containing the expression
|
||||
/// associated to the given [`NodeId`], or `None` otherwise.
|
||||
///
|
||||
/// For example, if the `expr_id` points to `a` in `a.b.c.foo()`
|
||||
/// then the qualified name would have segments [`a`, `b`, `c`, `foo`].
|
||||
fn expand_to_qualified_name_attribute<'b>(
|
||||
semantic: &SemanticModel<'b>,
|
||||
expr_id: NodeId,
|
||||
) -> Option<QualifiedName<'b>> {
|
||||
let mut builder = QualifiedNameBuilder::with_capacity(16);
|
||||
|
||||
let mut expr_id = expr_id;
|
||||
|
||||
let expr = semantic.expression(expr_id)?;
|
||||
|
||||
let name = expr.as_name_expr()?;
|
||||
|
||||
builder.push(&name.id);
|
||||
|
||||
while let Some(node_id) = semantic.parent_expression_id(expr_id) {
|
||||
let Some(expr) = semantic.expression(node_id) else {
|
||||
break;
|
||||
};
|
||||
let Some(expr_attr) = expr.as_attribute_expr() else {
|
||||
break;
|
||||
};
|
||||
builder.push(expr_attr.attr.as_str());
|
||||
expr_id = node_id;
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
fn mark_uses_of_qualified_name(marked: &mut MarkedBindings, prototype: &QualifiedName) {
|
||||
let Some(best) = best_match(&marked.bindings, prototype) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(best_import) = best.as_any_import() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let best_name = best_import.qualified_name();
|
||||
|
||||
// We loop through all bindings in case there are repeated instances
|
||||
// of the `best_name`. For example, if we have
|
||||
//
|
||||
// ```python
|
||||
// import a
|
||||
// import a
|
||||
//
|
||||
// a.foo()
|
||||
// ```
|
||||
//
|
||||
// then we want to mark both import statements as used. It
|
||||
// is the job of `redefined-while-unused` (`F811`) to catch
|
||||
// the repeated binding in this case.
|
||||
for (binding, is_used) in marked.iter_mut() {
|
||||
if *is_used {
|
||||
continue;
|
||||
}
|
||||
|
||||
if binding
|
||||
.as_any_import()
|
||||
.is_some_and(|imp| imp.qualified_name() == best_name)
|
||||
{
|
||||
*is_used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a pair with first component the length of the largest
|
||||
/// shared prefix between the qualified name of the import binding
|
||||
/// and the `prototype` and second component the length of the
|
||||
/// qualified name of the import binding (i.e. the number of path
|
||||
/// segments). Moreover, we regard the second component as ordered
|
||||
/// in reverse.
|
||||
///
|
||||
/// For example, if the binding corresponds to `import a.b.c`
|
||||
/// and the prototype to `a.b.foo()`, then the function returns
|
||||
/// `(2,std::cmp::Reverse(3))`.
|
||||
fn rank_matches(binding: &Binding, prototype: &QualifiedName) -> (usize, std::cmp::Reverse<usize>) {
|
||||
let Some(import) = binding.as_any_import() else {
|
||||
unreachable!()
|
||||
};
|
||||
let qname = import.qualified_name();
|
||||
let left = qname
|
||||
.segments()
|
||||
.iter()
|
||||
.zip(prototype.segments())
|
||||
.take_while(|(x, y)| x == y)
|
||||
.count();
|
||||
(left, std::cmp::Reverse(qname.segments().len()))
|
||||
}
|
||||
|
||||
/// Returns the import binding that shares the longest prefix
|
||||
/// with the `prototype` and is of minimal length amongst these.
|
||||
///
|
||||
/// See also [`rank_matches`].
|
||||
fn best_match<'a, 'b>(
|
||||
bindings: &Vec<&'a Binding<'b>>,
|
||||
prototype: &QualifiedName,
|
||||
) -> Option<&'a Binding<'b>> {
|
||||
bindings
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|binding| rank_matches(binding, prototype))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_simple_shadowed_bindings(scope: &Scope, id: BindingId, semantic: &SemanticModel) -> bool {
|
||||
scope.shadowed_bindings(id).enumerate().all(|(i, shadow)| {
|
||||
let shadowed_binding = semantic.binding(shadow);
|
||||
// Bail if one of the shadowed bindings is
|
||||
// used before the last live binding. This is
|
||||
// to avoid situations like this:
|
||||
//
|
||||
// ```
|
||||
// import a
|
||||
// a.b
|
||||
// import a.b
|
||||
// ```
|
||||
if i > 0 && shadowed_binding.is_used() {
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
shadowed_binding.kind,
|
||||
BindingKind::Import(_) | BindingKind::SubmoduleImport(_)
|
||||
) && !shadowed_binding.flags.contains(BindingFlags::ALIAS)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a.c
|
||||
3 | import a.b
|
||||
| ^^^
|
||||
4 | a.foo()
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | import a.c
|
||||
- import a.b
|
||||
3 | a.foo()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b.d` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a.c
|
||||
3 | import a.b.d
|
||||
| ^^^^^
|
||||
4 | a.foo()
|
||||
|
|
||||
help: Remove unused import: `a.b.d`
|
||||
1 |
|
||||
2 | import a.c
|
||||
- import a.b.d
|
||||
3 | a.foo()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | # refined logic only applied _within_ scope
|
||||
3 | import a
|
||||
| ^
|
||||
4 | def foo():
|
||||
5 | import a.b
|
||||
|
|
||||
help: Remove unused import: `a`
|
||||
1 |
|
||||
2 | # refined logic only applied _within_ scope
|
||||
- import a
|
||||
3 | def foo():
|
||||
4 | import a.b
|
||||
5 | a.foo()
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a` imported but unused
|
||||
--> f401_preview_submodule.py:2:8
|
||||
|
|
||||
2 | import a
|
||||
| ^
|
||||
3 | import a.b
|
||||
4 | import a.c
|
||||
|
|
||||
help: Remove unused import: `a`
|
||||
1 |
|
||||
- import a
|
||||
2 | import a.b
|
||||
3 | import a.c
|
||||
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
| ^^^
|
||||
4 | import a.c
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | import a
|
||||
- import a.b
|
||||
3 | import a.c
|
||||
|
||||
F401 [*] `a.c` imported but unused
|
||||
--> f401_preview_submodule.py:4:8
|
||||
|
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
4 | import a.c
|
||||
| ^^^
|
||||
|
|
||||
help: Remove unused import: `a.c`
|
||||
1 |
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
- import a.c
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
| ^^^
|
||||
4 | __all__ = ["a"]
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | import a
|
||||
- import a.b
|
||||
3 | __all__ = ["a"]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
| ^^^
|
||||
4 | a.foo()
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | import a
|
||||
- import a.b
|
||||
3 | a.foo()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F401 [*] `a.b` imported but unused
|
||||
--> f401_preview_submodule.py:3:8
|
||||
|
|
||||
2 | import a
|
||||
3 | import a.b
|
||||
| ^^^
|
||||
4 | a.foo()
|
||||
5 | a.bar()
|
||||
|
|
||||
help: Remove unused import: `a.b`
|
||||
1 |
|
||||
2 | import a
|
||||
- import a.b
|
||||
3 | a.foo()
|
||||
4 | a.bar()
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 2
|
||||
|
||||
--- Added ---
|
||||
F401 [*] `multiprocessing.process` imported but unused
|
||||
--> F401_0.py:10:8
|
||||
|
|
||||
8 | )
|
||||
9 | import multiprocessing.pool
|
||||
10 | import multiprocessing.process
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 | import logging.config
|
||||
12 | import logging.handlers
|
||||
|
|
||||
help: Remove unused import: `multiprocessing.process`
|
||||
7 | namedtuple,
|
||||
8 | )
|
||||
9 | import multiprocessing.pool
|
||||
- import multiprocessing.process
|
||||
10 | import logging.config
|
||||
11 | import logging.handlers
|
||||
12 | from typing import (
|
||||
|
||||
|
||||
F401 [*] `logging.config` imported but unused
|
||||
--> F401_0.py:11:8
|
||||
|
|
||||
9 | import multiprocessing.pool
|
||||
10 | import multiprocessing.process
|
||||
11 | import logging.config
|
||||
| ^^^^^^^^^^^^^^
|
||||
12 | import logging.handlers
|
||||
13 | from typing import (
|
||||
|
|
||||
help: Remove unused import: `logging.config`
|
||||
8 | )
|
||||
9 | import multiprocessing.pool
|
||||
10 | import multiprocessing.process
|
||||
- import logging.config
|
||||
11 | import logging.handlers
|
||||
12 | from typing import (
|
||||
13 | TYPE_CHECKING,
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
--- Linter settings ---
|
||||
-linter.preview = disabled
|
||||
+linter.preview = enabled
|
||||
|
||||
--- Summary ---
|
||||
Removed: 0
|
||||
Added: 0
|
||||
Loading…
Reference in New Issue