Compare commits

..

1 Commits

Author SHA1 Message Date
konstin
0f2979ed66 Formatter: Show preceding, following and enclosing of comments
**Summary** I used to always add `dbg!` for preceding, following and enclosing. With this change `--print-comments` can do this instead.
```python
import re  # import

def to_camel_case(node: str) -> str:
    """Converts PascalCase to camel_case"""
    return re.sub("([A-Z])", r"_\1", node).lower().lstrip("_")

# a
if True:
    pass  # b
else:
    print()
```
Debug output:
```
11..19 Some((StmtImport, 0..9)) Some((StmtFunctionDef, 22..165)) (ModModule, 0..213) "# import"
168..171 Some((StmtFunctionDef, 22..165)) Some((StmtIf, 172..212)) (ModModule, 0..213) "# a"
191..194 Some((StmtPass, 185..189)) Some((ElifElseClause, 195..212)) (StmtIf, 172..212) "# b"
```

**Test Plan** n/a
2023-08-23 12:55:24 +02:00
389 changed files with 5195 additions and 8584 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/*
- scripts/check_ecosystem.py
formatter:
- Cargo.toml
@@ -56,7 +56,6 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@@ -339,5 +338,8 @@ jobs:
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_log.txt | sort > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects
# CPython is not black formatted, so we run only the stability check
- name: "Clone CPython 3.10"
run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
- name: "Check CPython stability"
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython

View File

@@ -1,13 +1,5 @@
# Breaking Changes
## 0.0.283
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))
Previously, when a target Python version was not specified, Ruff would use a default of Python 3.10. However, it is safer to default to an _older_ Python version to avoid assuming the availability of new features. We now default to the oldest supported Python version which is currently Python 3.8.
(We still support Python 3.7 but since [it has reached EOL](https://devguide.python.org/versions/#unsupported-versions) we've decided not to make it the default here.)
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -131,6 +131,7 @@ At time of writing, the repository includes the following crates:
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.

6
Cargo.lock generated
View File

@@ -800,7 +800,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.283"
version = "0.0.282"
dependencies = [
"anyhow",
"clap",
@@ -2042,7 +2042,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.283"
version = "0.0.282"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2141,7 +2141,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.283"
version = "0.0.282"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.283
rev: v0.0.282
hooks:
- id: ruff
```
@@ -211,8 +211,8 @@ line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.8
target-version = "py38"
# Assume Python 3.10.
target-version = "py310"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.283"
version = "0.0.282"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.283"
version = "0.0.282"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -240,16 +240,12 @@ def foo(f=lambda x: print(x)):
from collections import abc
from typing import Annotated, Dict, Optional, Sequence, Union, Set
import typing_extensions
def immutable_annotations(
a: Sequence[int] | None = [],
b: Optional[abc.Mapping[int, int]] = {},
c: Annotated[Union[abc.Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[
Union[abc.Set[str], abc.Sized], "annotation"
] = set(),
):
pass
@@ -258,6 +254,5 @@ def mutable_annotations(
a: list[int] | None = [],
b: Optional[Dict[int, int]] = {},
c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
):
pass

View File

@@ -74,10 +74,3 @@ try:
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass
# https://github.com/astral-sh/ruff/issues/6412
try:
pass
except (ValueError, ValueError, TypeError):
pass

View File

@@ -3,6 +3,3 @@ import logging
name = "world"
logging.info(f"Hello {name}")
logging.log(logging.INFO, f"Hello {name}")
_LOGGER = logging.getLogger()
_LOGGER.info(f"{__name__}")

View File

@@ -1,19 +1,20 @@
import typing
# Shouldn't affect non-union field types.
field1: str
# Should emit for duplicate field types.
field2: str | str # PYI016: Duplicate union member `str`
# Should emit for union types in arguments.
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
print(arg1)
# Should emit for unions in return types.
def func2() -> str | str: # PYI016: Duplicate union member `str`
return "my string"
# Should emit in longer unions, even if not directly adjacent.
field3: str | str | int # PYI016: Duplicate union member `str`
field4: int | int | str # PYI016: Duplicate union member `int`
@@ -32,55 +33,3 @@ field10: (str | int) | str # PYI016: Duplicate union member `str`
# Should emit for nested unions.
field11: dict[int | int, str]
# Should emit for unions with more than two cases
field12: int | int | int # Error
field13: int | int | int | int # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: int | int | str | int # Error
# Should emit for duplicate literal types; also covered by PYI030
field15: typing.Literal[1] | typing.Literal[1] # Error
# Shouldn't emit if in new parent type
field16: int | dict[int, str] # OK
# Shouldn't emit if not in a union parent
field17: dict[int, int] # OK
# Should emit in cases with newlines
field18: typing.Union[
set[
int # foo
],
set[
int # bar
],
] # Error, newline and comment will not be emitted in message
# Should emit in cases with `typing.Union` instead of `|`
field19: typing.Union[int, int] # Error
# Should emit in cases with nested `typing.Union`
field20: typing.Union[int, typing.Union[int, str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field21: typing.Union[int, int | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -74,13 +74,3 @@ field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -14,10 +14,6 @@ class BadClass:
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok

View File

@@ -14,10 +14,6 @@ class BadClass:
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok

View File

@@ -1,8 +0,0 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": ["%%timeit\n", "print('hello world')"]
}

View File

@@ -25,23 +25,6 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16214f6f-bb32-4594-81be-79fb27c6ec92",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import math\n",
"import abc"
]
}
],
"metadata": {

View File

@@ -27,23 +27,6 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d6c55c6-4a34-4662-914b-4ee11c9c24a5",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"from pathlib import Path\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import abc\n",
"import math"
]
}
],
"metadata": {

View File

@@ -1,52 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "eab4754a-d6df-4b41-8ee8-7e23aef440f9",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"import os\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b0e2986-1b87-4bb6-9b1d-c11ca1decd87",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,51 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "cad32845-44f9-4a53-8b8c-a6b1bb3f3378",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d7b8e967-8b4a-493b-b6f7-d5cecfb3a5c3",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,49 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "a0efffbc-85f1-4513-bf49-5387ec3a2a4e",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" foo1 = %matplotlib --list\n",
" foo2: list[str] = %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e0b2b50-43f2-4f59-951d-9404dd560ae4",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" bar1 = !pwd\n",
" bar2: str = !pwd"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,49 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "24426ef2-046c-453e-b809-05b56e7355e0",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" %matplotlib --list\n",
" %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d98fdae-b86b-476e-b4db-9d3ce5562682",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" !pwd\n",
" !pwd"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -634,8 +634,3 @@ def starts_with_this():
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210
class SameLine: """This is a docstring on the same line"""
def same_line(): """This is a docstring on the same line"""

View File

@@ -19,10 +19,6 @@ foo in foo
foo not in foo
id(foo) == id(foo)
len(foo) == len(foo)
# Non-errors.
"foo" == "foo" # This is flagged by `comparison-of-constant` instead.
@@ -47,11 +43,3 @@ foo is not bar
foo in bar
foo not in bar
x(foo) == y(foo)
id(foo) == id(bar)
id(foo, bar) == id(foo, bar)
id(foo, bar=1) == id(foo, bar=1)

View File

@@ -32,30 +32,3 @@ print(
)
'{' '0}'.format(1)
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)

View File

@@ -15,17 +15,3 @@ f"{0}".format(1)
print(f"{0}".format(1))
''.format(1)
'{1} {0}'.format(*args)
"{1}_{0}".format(*args, 1)
"{1}_{0}".format(*args, 1, 2)
"{1}_{0}".format(1, **kwargs)
"{1}_{0}".format(1, foo=2)
"{1}_{0}".format(1, 2, **kwargs)
"{1}_{0}".format(1, 2, foo=3, bar=4)

View File

@@ -0,0 +1,28 @@
# These SHOULD change
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{1}_{0}".format(*args)
"{1}_{0}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)

View File

@@ -106,7 +106,3 @@ print('Hello %(arg)s' % bar['bop'])
"""
% (x,)
)
"%s" % (
x, # comment
)

View File

@@ -198,7 +198,3 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
).format(a=1)
"{}".format(**c)
"{}".format(
1 # comment
)

View File

@@ -46,35 +46,6 @@ from typing import Callable, Match, Pattern, List, OrderedDict, AbstractSet, Con
if True: from collections import (
Mapping, Counter)
# Bad imports from PYI027 that are now handled by PYI022 (UP035)
from typing import ContextManager
from typing import OrderedDict
from typing_extensions import OrderedDict
from typing import Callable
from typing import ByteString
from typing import Container
from typing import Hashable
from typing import ItemsView
from typing import Iterable
from typing import Iterator
from typing import KeysView
from typing import Mapping
from typing import MappingView
from typing import MutableMapping
from typing import MutableSequence
from typing import MutableSet
from typing import Sequence
from typing import Sized
from typing import ValuesView
from typing import Awaitable
from typing import AsyncIterator
from typing import AsyncIterable
from typing import Coroutine
from typing import Collection
from typing import AsyncGenerator
from typing import Reversible
from typing import Generator
# OK
from a import b

View File

@@ -5,42 +5,11 @@ from typing import TypeAlias
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 simple generic
# UP040 with generics (todo)
T = typing.TypeVar["T"]
x: typing.TypeAlias = list[T]
# UP040 call style generic
T = typing.TypeVar("T")
x: typing.TypeAlias = list[T]
# UP040 bounded generic (todo)
T = typing.TypeVar("T", bound=int)
x: typing.TypeAlias = list[T]
T = typing.TypeVar("T", int, str)
x: typing.TypeAlias = list[T]
# UP040 contravariant generic (todo)
T = typing.TypeVar("T", contravariant=True)
x: typing.TypeAlias = list[T]
# UP040 covariant generic (todo)
T = typing.TypeVar("T", covariant=True)
x: typing.TypeAlias = list[T]
# UP040 in class scope
T = typing.TypeVar["T"]
class Foo:
# reference to global variable
x: typing.TypeAlias = list[T]
# reference to class variable
TCLS = typing.TypeVar["TCLS"]
y: typing.TypeAlias = list[TCLS]
# UP040 wont add generics in fix
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# OK
x: TypeAlias

View File

@@ -1,5 +0,0 @@
# ruff: noqa: RUF100
import os # noqa: F401
print(os.sep)

View File

@@ -33,7 +33,7 @@ impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T {
///
/// Returns `Ok(None)` if the statement is empty after removing the imports.
pub(crate) fn remove_imports<'a>(
member_names: impl Iterator<Item = &'a str>,
imports: impl Iterator<Item = &'a str>,
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -45,20 +45,27 @@ pub(crate) fn remove_imports<'a>(
bail!("Expected Statement::Simple");
};
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
names
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
} else if let ImportNames::Star(..) = &import_body.names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
for member in member_names {
if member == "*" {
for import in imports {
let qualified_name = match import_body.module.as_ref() {
Some(module_name) => format!("{}.*", compose_module_path(module_name)),
None => "*".to_string(),
};
if import == qualified_name {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
bail!("Expected \"*\" for unused import (got: \"{}\")", import);
}
}
if !found_star {
@@ -75,10 +82,30 @@ pub(crate) fn remove_imports<'a>(
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
for member in member_names {
let alias_index = aliases
.iter()
.position(|alias| member == compose_module_path(&alias.name));
for import in imports {
let alias_index = aliases.iter().position(|alias| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == import
});
if let Some(index) = alias_index {
aliases.remove(index);
}
@@ -112,7 +139,7 @@ pub(crate) fn remove_imports<'a>(
///
/// Returns the modified import statement.
pub(crate) fn retain_imports(
member_names: &[&str],
imports: &[&str],
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -124,11 +151,14 @@ pub(crate) fn retain_imports(
bail!("Expected Statement::Simple");
};
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
names
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
} else {
bail!("Expected: ImportNames::Aliases");
}
@@ -140,9 +170,28 @@ pub(crate) fn retain_imports(
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
aliases.retain(|alias| {
member_names
.iter()
.any(|member| *member == compose_module_path(&alias.name))
imports.iter().any(|import| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == *import
})
});
// But avoid destroying any trailing comments.

View File

@@ -3,12 +3,10 @@
use anyhow::{bail, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{lexer, AsMode};
use ruff_python_parser::{lexer, Mode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -58,14 +56,14 @@ pub(crate) fn delete_stmt(
/// Generate a `Fix` to remove the specified imports from an `import` statement.
pub(crate) fn remove_unused_imports<'a>(
member_names: impl Iterator<Item = &'a str>,
unused_imports: impl Iterator<Item = &'a str>,
stmt: &Stmt,
parent: Option<&Stmt>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
) -> Result<Edit> {
match codemods::remove_imports(member_names, stmt, locator, stylist)? {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
None => Ok(delete_stmt(stmt, parent, locator, indexer)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
}
@@ -90,7 +88,6 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments,
parentheses: Parentheses,
locator: &Locator,
source_type: PySourceType,
) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 {
@@ -109,7 +106,7 @@ pub(crate) fn remove_argument<T: Ranged>(
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
Mode::Module,
arguments.start(),
)
.flatten()
@@ -138,7 +135,7 @@ pub(crate) fn remove_argument<T: Ranged>(
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
Mode::Module,
arguments.start(),
)
.flatten()
@@ -179,13 +176,16 @@ fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;

View File

@@ -11,18 +11,21 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
let Stmt::For(ast::StmtFor {
if let Stmt::For(ast::StmtFor {
target, iter, body, ..
}) = checker.semantic.current_statement()
else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
} else {
unreachable!("Expected Expr::For | Expr::AsyncFor");
}
}
}

View File

@@ -1,4 +1,5 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
@@ -36,7 +37,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_imports = !checker.source_type.is_stub()
let enforce_typing_imports = !checker.is_stub
&& checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
@@ -111,11 +112,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
branch_detection::different_forks(left, right, &checker.semantic.stmts)
})
}) {
continue;
@@ -171,25 +168,16 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(statement_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(statement_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
&function.decorator_list,
&checker.semantic,
)
})
{
continue;
}
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
continue;
}
} else {
// Only enforce cross-scope shadowing for imports.
@@ -207,11 +195,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
branch_detection::different_forks(left, right, &checker.semantic.stmts)
})
}) {
continue;
@@ -247,7 +231,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
@@ -256,7 +243,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
}
if !checker.source_type.is_stub() {
if !checker.is_stub {
if checker.any_enabled(&[
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
@@ -273,7 +260,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
) {
if enforce_typing_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic

View File

@@ -30,7 +30,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs = checker.is_stub && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection,

View File

@@ -31,7 +31,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.source_type.is_stub()
if !checker.is_stub
&& checker.settings.target_version < PythonVersion::Py310
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -44,7 +44,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP604Annotation) {
if checker.source_type.is_stub()
if checker.is_stub
|| checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -59,7 +59,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.source_type.is_stub()
if !checker.is_stub
&& checker.settings.target_version < PythonVersion::Py39
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -80,9 +80,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// Avoid duplicate checks if the parent is an `Union[...]` since these rules
// traverse nested unions.
if !checker.semantic.in_nested_union() {
let is_unchecked_union = checker
.semantic
.expr_grandparent()
.and_then(Expr::as_subscript_expr)
.map_or(true, |parent| {
!checker.semantic.match_typing_expr(&parent.value, "Union")
});
if is_unchecked_union {
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
@@ -168,7 +176,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
typing::to_pep585_generic(expr, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.source_type.is_stub()
if !checker.is_stub
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -179,7 +187,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub()
if checker.is_stub
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -198,16 +206,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.current_scope().kind.is_function() {
if checker.semantic.scope().kind.is_any_function() {
// Ignore globals.
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
if !checker.semantic.scope().get(id).is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
}) {
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
@@ -216,7 +219,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.current_scope().kind
&checker.semantic.scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker,
@@ -227,7 +230,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
if matches!(checker.semantic.scope().kind, ScopeKind::Module) {
pep8_naming::rules::mixed_case_variable_in_global_scope(
checker, expr, id,
);
@@ -240,7 +243,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
@@ -269,7 +272,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) {
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.source_type.is_stub()
if !checker.is_stub
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -282,7 +285,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub()
if checker.is_stub
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -338,8 +341,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
},
) => {
if checker.any_enabled(&[
// pylint
Rule::BadStringFormatCharacter,
// pyflakes
Rule::StringDotFormatInvalidFormat,
Rule::StringDotFormatExtraNamedArguments,
@@ -402,7 +403,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, call);
pyupgrade::rules::format_literals(checker, &summary, expr);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
@@ -665,7 +666,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_comprehensions::rules::unnecessary_map(
checker,
expr,
checker.semantic.current_expression_parent(),
checker.semantic.expr_parent(),
func,
args,
);
@@ -915,7 +916,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::FString(ast::ExprFString { values, range: _ }) => {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@@ -1063,7 +1064,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}) => {
// Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.source_type.is_stub()
if !checker.is_stub
&& checker.settings.target_version < PythonVersion::Py310
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -1076,23 +1077,29 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
// Avoid duplicate checks if the parent is a union, since these rules already
// Avoid duplicate checks if the parent is an `|` since these rules
// traverse nested unions.
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
let is_unchecked_union = !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
..
}))
);
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
&& is_unchecked_union
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
}
Expr::UnaryOp(ast::ExprUnaryOp {
@@ -1203,7 +1210,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
if checker.is_stub && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
}
}
@@ -1212,7 +1219,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
if checker.is_stub && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
@@ -1240,7 +1247,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}

View File

@@ -15,7 +15,7 @@ pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, parameters);
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters);
}

View File

@@ -53,7 +53,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::BreakOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::break_outside_loop(
stmt,
&mut checker.semantic.current_statements().skip(1),
&mut checker.semantic.parents().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
@@ -63,21 +63,29 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ContinueOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::continue_outside_loop(
stmt,
&mut checker.semantic.current_statements().skip(1),
&mut checker.semantic.parents().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
}
}
Stmt::FunctionDef(ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
parameters,
body,
type_params,
range: _,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
}) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
@@ -105,7 +113,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pep8_naming::rules::invalid_first_argument_name_for_class_method(
checker,
checker.semantic.current_scope(),
checker.semantic.scope(),
name,
decorator_list,
parameters,
@@ -117,7 +125,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidFirstArgumentNameForMethod) {
if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method(
checker,
checker.semantic.current_scope(),
checker.semantic.scope(),
name,
decorator_list,
parameters,
@@ -125,7 +133,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -143,11 +151,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
*is_async,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
stmt.is_async_function_def_stmt(),
);
}
if checker.enabled(Rule::CustomTypeVarReturnType) {
@@ -160,27 +168,30 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
type_params.as_ref(),
);
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
}
}
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py311
{
if checker.is_stub || checker.settings.target_version >= PythonVersion::Py311 {
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters);
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
parameters,
);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
}
if checker.enabled(Rule::DunderFunctionName) {
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
checker.semantic.current_scope(),
checker.semantic.scope(),
stmt,
name,
&checker.settings.pep8_naming.ignore_names,
@@ -335,7 +346,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt);
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
@@ -401,7 +412,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
);
}
if !checker.source_type.is_stub() {
if !checker.is_stub {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
flake8_django::rules::model_without_dunder_str(checker, class_def);
}
@@ -442,7 +453,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() {
if !checker.is_stub {
if checker.any_enabled(&[
Rule::AbstractBaseClassWithoutAbstractMethod,
Rule::EmptyMethodWithoutAbstractDecorator,
@@ -456,7 +467,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -558,7 +569,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
alias,
);
}
if !checker.source_type.is_stub() {
if !checker.is_stub {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -733,7 +744,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.enabled(Rule::FutureAnnotationsInStub) {
flake8_pyi::rules::from_future_import(checker, import_from);
}
@@ -753,7 +764,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
if !matches!(checker.semantic.scope().kind, ScopeKind::Module) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module),
@@ -878,7 +889,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() {
if !checker.is_stub {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -969,7 +980,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::nested_if_statements(
checker,
if_,
checker.semantic.current_statement_parent(),
checker.semantic.stmt_parent(),
);
}
if checker.enabled(Rule::IfWithSameArms) {
@@ -991,7 +1002,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
tryceratops::rules::type_check_without_type_error(
checker,
if_,
checker.semantic.current_statement_parent(),
checker.semantic.stmt_parent(),
);
}
if checker.enabled(Rule::OutdatedVersionBlock) {
@@ -1002,7 +1013,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
@@ -1084,7 +1095,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1094,8 +1106,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
with_,
checker.semantic.current_statement_parent(),
stmt,
body,
checker.semantic.stmt_parent(),
);
}
if checker.enabled(Rule::RedefinedLoopName) {
@@ -1119,6 +1132,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
iter,
orelse,
..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
@@ -1305,7 +1325,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::TypeBivariance) {
pylint::rules::type_bivariance(checker, value);
}
if checker.source_type.is_stub() {
if checker.is_stub {
if checker.any_enabled(&[
Rule::UnprefixedTypeParam,
Rule::AssignmentDefaultInStub,
@@ -1316,8 +1336,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.current_scopes()
.any(|scope| scope.kind.is_function())
.scopes()
.any(|scope| scope.kind.is_any_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
@@ -1375,14 +1395,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
}
if checker.source_type.is_stub() {
if checker.is_stub {
if let Some(value) = value {
if checker.enabled(Rule::AssignmentDefaultInStub) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.current_scopes()
.any(|scope| scope.kind.is_function())
.scopes()
.any(|scope| scope.kind.is_any_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(
checker, target, value, annotation,

View File

@@ -39,13 +39,11 @@ use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
use ruff_python_ast::helpers::{
collect_import_from_member, extract_handled_exceptions, to_module_path,
};
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_ast::{helpers, str, visitor};
use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
@@ -55,6 +53,7 @@ use ruff_python_semantic::{
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::checkers::ast::deferred::Deferred;
@@ -76,8 +75,8 @@ pub(crate) struct Checker<'a> {
package: Option<&'a Path>,
/// The module representation of the current file (e.g., `foo.bar`).
module_path: Option<&'a [String]>,
/// The [`PySourceType`] of the current file.
pub(crate) source_type: PySourceType,
/// Whether the current file is a stub (`.pyi`) file.
is_stub: bool,
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
/// comments).
noqa: flags::Noqa,
@@ -119,7 +118,6 @@ impl<'a> Checker<'a> {
stylist: &'a Stylist,
indexer: &'a Indexer,
importer: Importer<'a>,
source_type: PySourceType,
) -> Checker<'a> {
Checker {
settings,
@@ -128,7 +126,7 @@ impl<'a> Checker<'a> {
path,
package,
module_path: module.path(),
source_type,
is_stub: is_python_stub_file(path),
locator,
stylist,
indexer,
@@ -176,12 +174,13 @@ impl<'a> Checker<'a> {
///
/// If the current expression in the context is not an f-string, returns ``None``.
pub(crate) fn f_string_quote_style(&self) -> Option<Quote> {
if !self.semantic.in_f_string() {
let model = &self.semantic;
if !model.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = self.semantic.current_expression()?;
let expr = model.expr()?;
let string_range = self.indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
@@ -201,7 +200,7 @@ impl<'a> Checker<'a> {
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.statement_id(stmt))
.and_then(|stmt| self.semantic.stmts.node_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
@@ -234,6 +233,11 @@ impl<'a> Checker<'a> {
&self.semantic
}
/// Return `true` if the current file is a stub file (`.pyi`).
pub(crate) const fn is_stub(&self) -> bool {
self.is_stub
}
/// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path {
self.path
@@ -263,7 +267,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_statement(stmt);
self.semantic.push_stmt(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -287,7 +291,7 @@ where
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.current_statements())
&& !helpers::in_nested_block(self.semantic.parents())
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -321,11 +325,11 @@ where
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
// "foo.bar".
let name = alias.name.split('.').next().unwrap();
let call_path: Box<[&str]> = alias.name.split('.').collect();
let qualified_name = &alias.name;
self.add_binding(
name,
alias.identifier(),
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }),
BindingFlags::EXTERNAL,
);
} else {
@@ -342,11 +346,11 @@ where
}
let name = alias.asname.as_ref().unwrap_or(&alias.name);
let call_path: Box<[&str]> = alias.name.split('.').collect();
let qualified_name = &alias.name;
self.add_binding(
name,
alias.identifier(),
BindingKind::Import(Import { call_path }),
BindingKind::Import(Import { qualified_name }),
flags,
);
}
@@ -371,7 +375,7 @@ where
);
} else if &alias.name == "*" {
self.semantic
.current_scope_mut()
.scope_mut()
.add_star_import(StarImport { level, module });
} else {
let mut flags = BindingFlags::EXTERNAL;
@@ -390,16 +394,12 @@ where
// be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz"
// and `qualified_name` would be "foo.bar".
let name = alias.asname.as_ref().unwrap_or(&alias.name);
// Attempt to resolve any relative imports; but if we don't know the current
// module path, or the relative import extends beyond the package root,
// fallback to a literal representation (e.g., `[".", "foo"]`).
let call_path = collect_import_from_member(level, module, &alias.name)
.into_boxed_slice();
let qualified_name =
helpers::format_import_from_member(level, module, &alias.name);
self.add_binding(
name,
alias.identifier(),
BindingKind::FromImport(FromImport { call_path }),
BindingKind::FromImport(FromImport { qualified_name }),
flags,
);
}
@@ -420,7 +420,7 @@ where
BindingKind::Global,
BindingFlags::GLOBAL,
);
let scope = self.semantic.current_scope_mut();
let scope = self.semantic.scope_mut();
scope.add(name, binding_id);
}
}
@@ -443,7 +443,7 @@ where
BindingKind::Nonlocal(scope_id),
BindingFlags::NONLOCAL,
);
let scope = self.semantic.current_scope_mut();
let scope = self.semantic.scope_mut();
scope.add(name, binding_id);
}
}
@@ -454,16 +454,22 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
parameters,
decorator_list,
type_params,
returns,
..
}) => {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@@ -524,7 +530,8 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function(function_def),
ExtractionTarget::Function,
stmt,
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -532,7 +539,8 @@ where
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
_ => unreachable!("Expected Stmt::FunctionDef"),
Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt),
_ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"),
});
self.deferred.functions.push(self.semantic.snapshot());
@@ -566,7 +574,8 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class(class_def),
ExtractionTarget::Class,
stmt,
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -647,7 +656,7 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.current_scope().kind.is_class() {
if self.semantic.scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
@@ -666,7 +675,7 @@ where
}
} else {
matches!(
self.semantic.current_scope().kind,
self.semantic.scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@@ -733,7 +742,8 @@ where
// Step 3: Clean-up
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
let scope_id = self.semantic.scope_id;
self.deferred.scopes.push(scope_id);
self.semantic.pop_scope(); // Function scope
@@ -766,7 +776,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_statement();
self.semantic.pop_stmt();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -802,7 +812,7 @@ where
return;
}
self.semantic.push_expression(expr);
self.semantic.push_expr(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -830,7 +840,7 @@ where
}) => {
if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() {
if id == "locals" && ctx.is_load() {
let scope = self.semantic.current_scope_mut();
let scope = self.semantic.scope_mut();
scope.set_uses_locals();
}
}
@@ -1196,7 +1206,7 @@ where
));
}
}
Expr::FString(_) => {
Expr::JoinedStr(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@@ -1220,7 +1230,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expression();
self.semantic.pop_expr();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1275,7 +1285,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::FString(ast::ExprFString { values, range: _ }) => {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
for value in values {
self.visit_expr(value);
}
@@ -1600,7 +1610,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.current_statement();
let parent = self.semantic.stmt();
if matches!(
parent,
@@ -1615,7 +1625,7 @@ impl<'a> Checker<'a> {
return;
}
if parent.is_for_stmt() {
if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) {
self.add_binding(
id,
expr.range(),
@@ -1635,7 +1645,7 @@ impl<'a> Checker<'a> {
return;
}
let scope = self.semantic.current_scope();
let scope = self.semantic.scope();
if scope.kind.is_module()
&& match parent {
@@ -1687,8 +1697,8 @@ impl<'a> Checker<'a> {
if self
.semantic
.current_expressions()
.any(Expr::is_named_expr_expr)
.expr_ancestors()
.any(|expr| expr.is_named_expr_expr())
{
self.add_binding(
id,
@@ -1714,7 +1724,7 @@ impl<'a> Checker<'a> {
self.semantic.resolve_del(id, expr.range());
if helpers::on_conditional_branch(&mut self.semantic.current_statements()) {
if helpers::on_conditional_branch(&mut self.semantic.parents()) {
return;
}
@@ -1722,7 +1732,7 @@ impl<'a> Checker<'a> {
let binding_id =
self.semantic
.push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty());
let scope = self.semantic.current_scope_mut();
let scope = self.semantic.scope_mut();
scope.add(id, binding_id);
}
@@ -1776,7 +1786,7 @@ impl<'a> Checker<'a> {
pyupgrade::rules::quoted_annotation(self, value, range);
}
}
if self.source_type.is_stub() {
if self.is_stub {
if self.enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(self, value, range);
}
@@ -1814,14 +1824,19 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
if let Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
}) = self.semantic.current_statement()
{
self.visit_parameters(parameters);
self.visit_body(body);
} else {
unreachable!("Expected Stmt::FunctionDef")
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body, parameters, ..
}) => {
self.visit_parameters(parameters);
self.visit_body(body);
}
_ => {
unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef")
}
}
}
}
@@ -1913,7 +1928,6 @@ pub(crate) fn check_ast(
noqa: flags::Noqa,
path: &Path,
package: Option<&Path>,
source_type: PySourceType,
) -> Vec<Diagnostic> {
let module_path = package.and_then(|package| to_module_path(package, path));
let module = Module {
@@ -1941,7 +1955,6 @@ pub(crate) fn check_ast(
stylist,
indexer,
Importer::new(python_ast, locator, stylist),
source_type,
);
checker.bind_builtins();

View File

@@ -2,7 +2,7 @@
use std::borrow::Cow;
use std::path::Path;
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers::to_module_path;
@@ -10,7 +10,7 @@ use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::directives::IsortDirectives;
@@ -87,12 +87,12 @@ pub(crate) fn check_imports(
path: &Path,
package: Option<&Path>,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> (Vec<Diagnostic>, Option<ImportMap>) {
let is_stub = is_python_stub_file(path);
// Extract all import blocks from the AST.
let tracker = {
let mut tracker =
BlockBuilder::new(locator, directives, source_type.is_stub(), source_kind);
let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind);
tracker.visit_body(python_ast);
tracker
};
@@ -104,13 +104,7 @@ pub(crate) fn check_imports(
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
block,
locator,
stylist,
indexer,
settings,
package,
source_type,
block, locator, stylist, indexer, settings, package,
) {
diagnostics.push(diagnostic);
}
@@ -119,11 +113,7 @@ pub(crate) fn check_imports(
}
if settings.rules.enabled(Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports(
python_ast,
locator,
stylist,
settings,
source_type,
python_ast, locator, stylist, settings, is_stub,
));
}

View File

@@ -94,15 +94,8 @@ pub(crate) fn check_noqa(
}
}
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if settings.rules.enabled(Rule::UnusedNOQA)
&& analyze_directives
&& !exemption.is_some_and(|exemption| match exemption {
FileExemption::All => true,
FileExemption::Codes(codes) => codes.contains(&Rule::UnusedNOQA.noqa_code()),
})
{
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(directive) => {

View File

@@ -1,6 +1,7 @@
//! Extract docstrings from an AST.
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind};
/// Extract a docstring from a function or class body.
@@ -27,48 +28,63 @@ pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
match definition {
Definition::Module(module) => docstring_from(module.python_ast),
Definition::Member(member) => docstring_from(member.body()),
Definition::Member(member) => {
if let Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) = &member.stmt
{
docstring_from(body)
} else {
None
}
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum ExtractionTarget<'a> {
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
pub(crate) enum ExtractionTarget {
Class,
Function,
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub(crate) fn extract_definition<'a>(
target: ExtractionTarget<'a>,
target: ExtractionTarget,
stmt: &'a Stmt,
parent: DefinitionId,
definitions: &Definitions<'a>,
) -> Member<'a> {
match target {
ExtractionTarget::Function(function) => match &definitions[parent] {
ExtractionTarget::Function => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Function(function),
kind: MemberKind::Function,
stmt,
},
Definition::Member(Member {
kind: MemberKind::Class(_) | MemberKind::NestedClass(_),
kind: MemberKind::Class | MemberKind::NestedClass,
..
}) => Member {
parent,
kind: MemberKind::Method(function),
kind: MemberKind::Method,
stmt,
},
Definition::Member(_) => Member {
Definition::Member(..) => Member {
parent,
kind: MemberKind::NestedFunction(function),
kind: MemberKind::NestedFunction,
stmt,
},
},
ExtractionTarget::Class(class) => match &definitions[parent] {
Definition::Module(_) => Member {
ExtractionTarget::Class => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Class(class),
kind: MemberKind::Class,
stmt,
},
Definition::Member(_) => Member {
Definition::Member(..) => Member {
parent,
kind: MemberKind::NestedClass(class),
kind: MemberKind::NestedClass,
stmt,
},
},
}

View File

@@ -1,8 +1,8 @@
//! Insert statements into Python code.
use std::ops::Add;
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::{Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
@@ -137,7 +137,6 @@ impl<'a> Insertion<'a> {
mut location: TextSize,
locator: &Locator<'a>,
stylist: &Stylist,
source_type: PySourceType,
) -> Insertion<'a> {
enum Awaiting {
Colon(u32),
@@ -147,7 +146,7 @@ impl<'a> Insertion<'a> {
let mut state = Awaiting::Colon(0);
for (tok, range) in
lexer::lex_starts_at(locator.after(location), source_type.as_mode(), location).flatten()
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten()
{
match state {
// Iterate until we find the colon indicating the start of the block body.
@@ -301,20 +300,20 @@ fn match_leading_semicolon(s: &str) -> Option<TextSize> {
mod tests {
use anyhow::Result;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_suite, Mode};
use ruff_source_file::{LineEnding, Locator};
use ruff_text_size::TextSize;
use ruff_python_codegen::Stylist;
use ruff_python_parser::parse_suite;
use ruff_source_file::{LineEnding, Locator};
use super::Insertion;
#[test]
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = parse_suite(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist))
@@ -425,10 +424,10 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist, PySourceType::default())
Insertion::start_of_block(offset, &locator, &stylist)
}
let contents = "if True: pass";

View File

@@ -7,7 +7,7 @@ use std::error::Error;
use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute};
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
@@ -87,13 +87,13 @@ impl<'a> Importer<'a> {
/// import statement.
pub(crate) fn runtime_import_edit(
&self,
import: &ImportedMembers,
import: &StmtImports,
at: TextSize,
) -> Result<RuntimeImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&import.names,
import.statement,
&import.qualified_names,
import.stmt,
self.locator,
self.stylist,
)?;
@@ -118,15 +118,14 @@ impl<'a> Importer<'a> {
/// `TYPE_CHECKING` block.
pub(crate) fn typing_import_edit(
&self,
import: &ImportedMembers,
import: &StmtImports,
at: TextSize,
semantic: &SemanticModel,
source_type: PySourceType,
) -> Result<TypingImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&import.names,
import.statement,
&import.qualified_names,
import.stmt,
self.locator,
self.stylist,
)?;
@@ -141,7 +140,7 @@ impl<'a> Importer<'a> {
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start(), source_type)
self.add_to_type_checking_block(&content, block.start())
} else {
// Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block(
@@ -354,13 +353,8 @@ impl<'a> Importer<'a> {
}
/// Add an import statement to an existing `TYPE_CHECKING` block.
fn add_to_type_checking_block(
&self,
content: &str,
at: TextSize,
source_type: PySourceType,
) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist, source_type).into_edit(content)
fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist).into_edit(content)
}
/// Return the import statement that precedes the given position, if any.
@@ -452,11 +446,11 @@ impl<'a> ImportRequest<'a> {
}
/// An existing list of module or member imports, located within an import statement.
pub(crate) struct ImportedMembers<'a> {
pub(crate) struct StmtImports<'a> {
/// The import statement.
pub(crate) statement: &'a Stmt,
/// The "names" of the imported members.
pub(crate) names: Vec<&'a str>,
pub(crate) stmt: &'a Stmt,
/// The "qualified names" of the imported modules or members.
pub(crate) qualified_names: Vec<&'a str>,
}
/// The result of an [`Importer::get_or_import_symbol`] call.

View File

@@ -24,6 +24,8 @@ use crate::IOError;
pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"];
/// Run round-trip source code generation on a given Jupyter notebook file path.
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
let mut notebook = Notebook::read(path).map_err(|err| {
@@ -76,21 +78,26 @@ impl Cell {
/// Return `true` if it's a valid code cell.
///
/// A valid code cell is a cell where the cell type is [`Cell::Code`] and the
/// source doesn't contain a cell magic.
/// source doesn't contain a magic, shell or help command.
fn is_valid_code_cell(&self) -> bool {
let source = match self {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
// Ignore a cell if it contains a magic command. There could be valid
// Python code as well, but we'll ignore that for now.
// TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py
!match source {
SourceValue::String(string) => string
.lines()
.any(|line| line.trim_start().starts_with("%%")),
SourceValue::StringArray(string_array) => string_array
.iter()
.any(|line| line.trim_start().starts_with("%%")),
SourceValue::String(string) => string.lines().any(|line| {
MAGIC_PREFIX
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
}),
SourceValue::StringArray(string_array) => string_array.iter().any(|line| {
MAGIC_PREFIX
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
}),
}
}
}
@@ -506,10 +513,9 @@ mod tests {
}
#[test_case(Path::new("markdown.json"), false; "markdown")]
#[test_case(Path::new("only_magic.json"), true; "only_magic")]
#[test_case(Path::new("code_and_magic.json"), true; "code_and_magic")]
#[test_case(Path::new("only_magic.json"), false; "only_magic")]
#[test_case(Path::new("code_and_magic.json"), false; "code_and_magic")]
#[test_case(Path::new("only_code.json"), true; "only_code")]
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
Ok(())
@@ -561,7 +567,7 @@ print("after empty cells")
#[test]
fn test_import_sorting() -> Result<()> {
let path = "isort.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
let (diagnostics, source_kind) = test_notebook_path(
&path,
Path::new("isort_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnsortedImports),
@@ -570,34 +576,10 @@ print("after empty cells")
Ok(())
}
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_unused_variable() -> Result<()> {
let path = "unused_variable.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("unused_variable_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedVariable),
)?;
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let path = "before_fix.ipynb".to_string();
let (_, _, source_kind) = test_notebook_path(
let (_, source_kind) = test_notebook_path(
path,
Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),

View File

@@ -47,43 +47,4 @@ isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted
7 9 | def foo():
8 10 | pass
isort.ipynb:cell 3:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from pathlib import Path
2 | | import sys
3 | |
4 | | %matplotlib \
| |_^ I001
5 | --inline
|
= help: Organize imports
Fix
6 6 | # Newline should be added here
7 7 | def foo():
8 8 | pass
9 |+import sys
9 10 | from pathlib import Path
10 |-import sys
11 11 |
12 12 | %matplotlib \
13 13 | --inline
isort.ipynb:cell 3:7:1: I001 [*] Import block is un-sorted or un-formatted
|
5 | --inline
6 |
7 | / import math
8 | | import abc
|
= help: Organize imports
Fix
12 12 | %matplotlib \
13 13 | --inline
14 14 |
15 |+import abc
15 16 | import math
16 |-import abc

View File

@@ -1,23 +0,0 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |
5 | import os
| ^^ F401
6 |
7 | _ = math.pi
|
= help: Remove unused import: `os`
Fix
2 2 |
3 3 | %matplotlib inline
4 4 |
5 |-import os
6 5 |
7 6 | _ = math.pi

View File

@@ -1,72 +0,0 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
unused_variable.ipynb:cell 1:2:5: F841 [*] Local variable `foo1` is assigned to but never used
|
1 | def f():
2 | foo1 = %matplotlib --list
| ^^^^ F841
3 | foo2: list[str] = %matplotlib --list
|
= help: Remove assignment to unused variable `foo1`
Suggested fix
1 1 | def f():
2 |- foo1 = %matplotlib --list
2 |+ %matplotlib --list
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
unused_variable.ipynb:cell 1:3:5: F841 [*] Local variable `foo2` is assigned to but never used
|
1 | def f():
2 | foo1 = %matplotlib --list
3 | foo2: list[str] = %matplotlib --list
| ^^^^ F841
|
= help: Remove assignment to unused variable `foo2`
Suggested fix
1 1 | def f():
2 2 | foo1 = %matplotlib --list
3 |- foo2: list[str] = %matplotlib --list
3 |+ %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
6 6 | bar2: str = !pwd
unused_variable.ipynb:cell 2:2:5: F841 [*] Local variable `bar1` is assigned to but never used
|
1 | def f():
2 | bar1 = !pwd
| ^^^^ F841
3 | bar2: str = !pwd
|
= help: Remove assignment to unused variable `bar1`
Suggested fix
2 2 | foo1 = %matplotlib --list
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 |- bar1 = !pwd
5 |+ !pwd
6 6 | bar2: str = !pwd
unused_variable.ipynb:cell 2:3:5: F841 [*] Local variable `bar2` is assigned to but never used
|
1 | def f():
2 | bar1 = !pwd
3 | bar2: str = !pwd
| ^^^^ F841
|
= help: Remove assignment to unused variable `bar2`
Suggested fix
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
6 |- bar2: str = !pwd
6 |+ !pwd

View File

@@ -7,15 +7,14 @@ use colored::Colorize;
use itertools::Itertools;
use log::error;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use ruff_python_parser::ParseError;
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::{Locator, SourceFileBuilder};
use crate::autofix::{fix_file, FixResult};
@@ -82,7 +81,6 @@ pub fn check_path(
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
@@ -103,13 +101,9 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_tokens())
{
let is_stub = is_python_stub_file(path);
diagnostics.extend(check_tokens(
&tokens,
path,
locator,
indexer,
settings,
source_type.is_stub(),
&tokens, path, locator, indexer, settings, is_stub,
));
}
@@ -144,11 +138,7 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_jupyter(),
) {
match ruff_python_parser::parse_program_tokens(tokens, &path.to_string_lossy()) {
Ok(python_ast) => {
if use_ast {
diagnostics.extend(check_ast(
@@ -161,7 +151,6 @@ pub fn check_path(
noqa,
path,
package,
source_type,
));
}
if use_imports {
@@ -175,7 +164,6 @@ pub fn check_path(
path,
package,
source_kind,
source_type,
);
imports = module_imports;
diagnostics.extend(import_diagnostics);
@@ -268,13 +256,11 @@ const MAX_ITERATIONS: usize = 100;
/// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
let source_type = PySourceType::from(path);
// Read the file from disk.
let contents = std::fs::read_to_string(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&contents);
@@ -308,7 +294,6 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
settings,
flags::Noqa::Disabled,
None,
source_type,
);
// Log any parse errors.
@@ -341,10 +326,9 @@ pub fn lint_only(
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
@@ -375,7 +359,6 @@ pub fn lint_only(
settings,
noqa,
source_kind,
source_type,
);
result.map(|(diagnostics, imports)| {
@@ -422,7 +405,6 @@ pub fn lint_fix<'a>(
noqa: flags::Noqa,
settings: &Settings,
source_kind: &mut SourceKind,
source_type: PySourceType,
) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(contents);
@@ -438,8 +420,7 @@ pub fn lint_fix<'a>(
// Continuously autofix until the source code stabilizes.
loop {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&transformed);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&transformed);
@@ -470,7 +451,6 @@ pub fn lint_fix<'a>(
settings,
noqa,
Some(source_kind),
source_type,
);
if iterations == 0 {

View File

@@ -231,7 +231,7 @@ impl Renamer {
}
BindingKind::SubmoduleImport(import) => {
// Ex) Rename `import pandas.core` to `import pandas as pd`.
let module_name = import.call_path.first().unwrap();
let module_name = import.qualified_name.split('.').next().unwrap();
Some(Edit::range_replacement(
format!("{module_name} as {target}"),
binding.range,

View File

@@ -262,6 +262,7 @@ pub fn python_files_in_path(
builder.add(path);
}
builder.standard_filters(pyproject_config.settings.lib.respect_gitignore);
builder.require_git(false);
builder.hidden(false);
let walker = builder.build_parallel();

View File

@@ -1,26 +1,23 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::{Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
annotation: &str,
source_type: PySourceType,
pub(crate) fn add_return_annotation(
locator: &Locator,
stmt: &Stmt,
annotation: &str,
) -> Result<Edit> {
let contents = &locator.contents()[statement.range()];
let contents = &locator.contents()[stmt.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count = 0u32;
for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
{
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, stmt.start()).flatten() {
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));

View File

@@ -1,11 +1,53 @@
use ruff_python_ast::{self as ast, Expr, Parameters, Stmt};
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
pub(super) fn match_function_def(
stmt: &Stmt,
) -> (&str, &Parameters, Option<&Expr>, &[Stmt], &[ast::Decorator]) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
}) => (
name,
parameters,
returns.as_ref().map(AsRef::as_ref),
body,
decorator_list,
),
_ => panic!("Found non-FunctionDef in match_function_def"),
}
}
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option<String> {
let function = definition.as_function_def()?;
if visibility::is_overload(&function.decorator_list, semantic) {
Some(function.name.to_string())
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
None
}
} else {
None
}
@@ -18,12 +60,19 @@ pub(crate) fn is_overload_impl(
overloaded_name: &str,
semantic: &SemanticModel,
) -> bool {
let Some(function) = definition.as_function_def() else {
return false;
};
if visibility::is_overload(&function.decorator_list, semantic) {
false
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
false
} else {
let (name, ..) = match_function_def(stmt);
name == overloaded_name
}
} else {
function.name.as_str() == overloaded_name
false
}
}

View File

@@ -1,19 +1,23 @@
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Ranged, Stmt};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::Definition;
use ruff_python_semantic::{Definition, Member, MemberKind};
use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
use super::super::fixes;
use super::super::helpers::match_function_def;
/// ## What it does
/// Checks that function arguments have type annotations.
///
@@ -494,23 +498,20 @@ pub(crate) fn definition(
definition: &Definition,
visibility: visibility::Visibility,
) -> Vec<Diagnostic> {
let Some(function) = definition.as_function_def() else {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member { kind, stmt, .. }) = definition else {
return vec![];
};
let ast::StmtFunctionDef {
range: _,
is_async: _,
decorator_list,
name,
type_params: _,
parameters,
returns,
body,
} = function;
let is_method = definition.is_method();
let is_method = match kind {
MemberKind::Method => true,
MemberKind::Function | MemberKind::NestedFunction => false,
_ => return vec![],
};
let (name, arguments, returns, body, decorator_list) = match_function_def(stmt);
// Keep track of whether we've seen any typed arguments or return values.
let mut has_any_typed_arg = false; // Any argument has been typed?
let mut has_typed_return = false; // Return value has been typed?
@@ -527,19 +528,20 @@ pub(crate) fn definition(
parameter,
default: _,
range: _,
} in parameters
} in arguments
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()),
is_method
&& !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()),
),
)
{
// ANN401 for dynamically typed parameters
// ANN401 for dynamically typed arguments
if let Some(annotation) = &parameter.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) && !is_overridden {
@@ -570,7 +572,7 @@ pub(crate) fn definition(
}
// ANN002, ANN401
if let Some(arg) = &parameters.vararg {
if let Some(arg) = &arguments.vararg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@@ -596,7 +598,7 @@ pub(crate) fn definition(
}
// ANN003, ANN401
if let Some(arg) = &parameters.kwarg {
if let Some(arg) = &arguments.kwarg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
@@ -627,18 +629,18 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) {
if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) {
if let Some(ParameterWithDefault {
parameter,
default: _,
range: _,
}) = parameters
}) = arguments
.posonlyargs
.first()
.or_else(|| parameters.args.first())
.or_else(|| arguments.args.first())
{
if parameter.annotation.is_none() {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
@@ -674,22 +676,24 @@ pub(crate) fn definition(
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
} else if is_method
&& visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic())
{
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
} else if is_method && visibility::is_init(name) {
@@ -701,17 +705,12 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
fixes::add_return_annotation(checker.locator(), stmt, "None")
.map(Fix::suggested)
});
}
diagnostics.push(diagnostic);
@@ -723,18 +722,13 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
fixes::add_return_annotation(checker.locator(), stmt, return_type)
.map(Fix::suggested)
});
}
}
@@ -748,7 +742,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
}
@@ -758,7 +752,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
function.identifier(),
stmt.identifier(),
));
}
}

View File

@@ -112,21 +112,21 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
fn int_value(expr: &Expr, model: &SemanticModel) -> Option<u16> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_u16(),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
}) => {
let left_value = int_value(left, semantic)?;
let right_value = int_value(right, semantic)?;
let left_value = int_value(left, model)?;
let right_value = int_value(right, model)?;
match op {
Operator::BitAnd => Some(left_value & right_value),
Operator::BitOr => Some(left_value | right_value),

View File

@@ -53,7 +53,7 @@ fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bool {
fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool {
match expr {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
@@ -62,8 +62,8 @@ fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bo
..
}) => {
// Only evaluate the full BinOp, not the nested components.
if semantic
.current_expression_parent()
if model
.expr_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
if any_over_expr(expr, &has_string_literal) {
@@ -80,7 +80,7 @@ fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bo
attr == "format" && string_literal(value).is_some()
}
// f"select * from table where val = {val}"
Expr::FString(_) => true,
Expr::JoinedStr(_) => true,
_ => false,
}
}

View File

@@ -10,31 +10,6 @@ use crate::{
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
};
/// ## What it does
/// Check for method calls that initiate a subprocess with a shell.
///
/// ## Why is this bad?
/// Starting a subprocess with a shell can allow attackers to execute arbitrary
/// shell commands. Consider starting the process without a shell call and
/// sanitize the input to mitigate the risk of shell injection.
///
/// ## Example
/// ```python
/// import subprocess
///
/// subprocess.run("ls -l", shell=True)
/// ```
///
/// Use instead:
/// ```python
/// import subprocess
///
/// subprocess.run(["ls", "-l"])
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
#[violation]
pub struct SubprocessPopenWithShellEqualsTrue {
seems_safe: bool,
@@ -53,30 +28,6 @@ impl Violation for SubprocessPopenWithShellEqualsTrue {
}
}
/// ## What it does
/// Check for method calls that initiate a subprocess without a shell.
///
/// ## Why is this bad?
/// Starting a subprocess without a shell can prevent attackers from executing
/// arbitrary shell commands; however, it is still error-prone. Consider
/// validating the input.
///
/// ## Known problems
/// Prone to false positives as it is difficult to determine whether the
/// passed arguments have been validated ([#4045]).
///
/// ## Example
/// ```python
/// import subprocess
///
/// cmd = input("Enter a command: ").split()
/// subprocess.run(cmd)
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
///
/// [#4045]: https://github.com/astral-sh/ruff/issues/4045
#[violation]
pub struct SubprocessWithoutShellEqualsTrue;

View File

@@ -159,12 +159,18 @@ pub(crate) fn abstract_base_class(
continue;
}
let Stmt::FunctionDef(ast::StmtFunctionDef {
let (Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
}) = stmt
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
else {
continue;
};

View File

@@ -77,7 +77,7 @@ fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
/// B019
pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) {
if !checker.semantic().current_scope().kind.is_class() {
if !checker.semantic().scope().kind.is_class() {
return;
}
for decorator in decorator_list {

View File

@@ -150,9 +150,7 @@ fn duplicate_handler_exceptions<'a>(
if unique_elts.len() == 1 {
checker.generator().expr(unique_elts[0])
} else {
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
checker.generator().expr(&type_pattern(unique_elts))
},
expr.range(),
)));

View File

@@ -50,7 +50,7 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return;
};
if !value.is_f_string_expr() {
if !value.is_joined_str_expr() {
return;
}
checker

View File

@@ -67,9 +67,9 @@ struct ArgumentDefaultVisitor<'a> {
}
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
semantic,
semantic: model,
extend_immutable_calls,
diagnostics: Vec::new(),
}

View File

@@ -86,6 +86,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
parameters, body, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
parameters, body, ..
}) => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
@@ -233,7 +236,7 @@ struct AssignedNamesVisitor<'a> {
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if stmt.is_function_def_stmt() {
if matches!(stmt, Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_)) {
// Don't recurse.
return;
}
@@ -248,7 +251,8 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign { target, .. })
| Stmt::For(ast::StmtFor { target, .. }) => {
| Stmt::For(ast::StmtFor { target, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { target, .. }) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
self.names.extend(visitor.names);

View File

@@ -69,13 +69,16 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
));
}
match stmt {
Stmt::While(ast::StmtWhile { body, .. }) | Stmt::For(ast::StmtFor { body, .. }) => {
Stmt::While(ast::StmtWhile { body, .. })
| Stmt::For(ast::StmtFor { body, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, .. }) => {
walk_stmt(checker, body, Stmt::is_return_stmt);
}
Stmt::If(ast::StmtIf { body, .. })
| Stmt::Try(ast::StmtTry { body, .. })
| Stmt::TryStar(ast::StmtTryStar { body, .. })
| Stmt::With(ast::StmtWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
walk_stmt(checker, body, f);
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {

View File

@@ -97,7 +97,7 @@ pub(crate) fn setattr_with_constant(
if let Stmt::Expr(ast::StmtExpr {
value: child,
range: _,
}) = checker.semantic().current_statement()
}) = checker.semantic().stmt()
{
if expr == child.as_ref() {
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());

View File

@@ -159,7 +159,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
if certainty.into() {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().current_scope();
let scope = checker.semantic().scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))

View File

@@ -54,7 +54,7 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::FString(_)
Expr::JoinedStr(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Ellipsis,
..

View File

@@ -81,43 +81,33 @@ B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaul
222 | pass
|
B006_B008.py:258:27: B006 Do not use mutable data structures for argument defaults
B006_B008.py:254:27: B006 Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
253 | def mutable_annotations(
254 | a: list[int] | None = [],
| ^^ B006
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
B006_B008.py:259:35: B006 Do not use mutable data structures for argument defaults
B006_B008.py:255:35: B006 Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
253 | def mutable_annotations(
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
| ^^ B006
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
257 | ):
|
B006_B008.py:260:62: B006 Do not use mutable data structures for argument defaults
B006_B008.py:256:62: B006 Do not use mutable data structures for argument defaults
|
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
262 | ):
|
B006_B008.py:261:80: B006 Do not use mutable data structures for argument defaults
|
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
262 | ):
263 | pass
257 | ):
258 | pass
|

View File

@@ -64,22 +64,4 @@ B014.py:49:8: B014 [*] Exception handler with duplicate exception: `re.error`
51 51 | pass
52 52 |
B014.py:82:8: B014 [*] Exception handler with duplicate exception: `ValueError`
|
80 | try:
81 | pass
82 | except (ValueError, ValueError, TypeError):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B014
83 | pass
|
= help: De-duplicate exceptions
Fix
79 79 | # https://github.com/astral-sh/ruff/issues/6412
80 80 | try:
81 81 | pass
82 |-except (ValueError, ValueError, TypeError):
82 |+except (ValueError, TypeError):
83 83 | pass

View File

@@ -131,7 +131,7 @@ pub(crate) fn builtin_method_shadowing(
fn is_standard_library_override(
name: &str,
class_def: &ast::StmtClassDef,
semantic: &SemanticModel,
model: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
return false;
@@ -139,13 +139,13 @@ fn is_standard_library_override(
match name {
// Ex) `Event#set`
"set" => bases.iter().any(|base| {
semantic
model
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter#filter`
"filter" => bases.iter().any(|base| {
semantic
model
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
}),

View File

@@ -7,7 +7,7 @@ use libcst_native::{
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
use ruff_python_ast::{Expr, Ranged};
use ruff_python_ast::Ranged;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Edit, Fix};
@@ -28,7 +28,7 @@ use crate::{
pub(crate) fn fix_unnecessary_generator_list(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
let module_text = locator.slice(expr.range());
@@ -58,7 +58,10 @@ pub(crate) fn fix_unnecessary_generator_list(
}
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_set(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -93,7 +96,10 @@ pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> R
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
/// range(3)}`.
pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_dict(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -135,7 +141,7 @@ pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) ->
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_list_comprehension_set(
checker: &Checker,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -171,7 +177,7 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
/// range(3)}`.
pub(crate) fn fix_unnecessary_list_comprehension_dict(
checker: &Checker,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -256,7 +262,10 @@ fn drop_trailing_comma<'a>(
}
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_set(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -297,7 +306,10 @@ pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Res
}
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_dict(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -360,7 +372,10 @@ pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Re
}
/// (C408)
pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_collection_call(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
enum Collection {
Tuple,
List,
@@ -520,7 +535,7 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -570,7 +585,7 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call(
pub(crate) fn fix_unnecessary_literal_within_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -622,7 +637,7 @@ pub(crate) fn fix_unnecessary_literal_within_list_call(
pub(crate) fn fix_unnecessary_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
// Expr(Call(List|Tuple)))) -> Expr(List|Tuple)))
let module_text = locator.slice(expr.range());
@@ -644,7 +659,7 @@ pub(crate) fn fix_unnecessary_list_call(
pub(crate) fn fix_unnecessary_call_around_sorted(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -756,7 +771,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
pub(crate) fn fix_unnecessary_double_cast_or_process(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -786,7 +801,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
pub(crate) fn fix_unnecessary_comprehension(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -873,8 +888,8 @@ pub(crate) fn fix_unnecessary_comprehension(
pub(crate) fn fix_unnecessary_map(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
parent: Option<&Expr>,
expr: &ruff_python_ast::Expr,
parent: Option<&ruff_python_ast::Expr>,
object_type: ObjectType,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
@@ -1003,7 +1018,7 @@ pub(crate) fn fix_unnecessary_map(
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
if parent.is_some_and(Expr::is_formatted_value_expr) {
if parent.is_some_and(ruff_python_ast::Expr::is_formatted_value_expr) {
content = format!(" {content} ");
}
}
@@ -1018,7 +1033,7 @@ pub(crate) fn fix_unnecessary_map(
pub(crate) fn fix_unnecessary_literal_within_dict_call(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -1037,7 +1052,7 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call(
pub(crate) fn fix_unnecessary_comprehension_any_all(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
expr: &ruff_python_ast::Expr,
) -> Result<Fix> {
// Expr(ListComp) -> Expr(GeneratorExp)
let module_text = locator.slice(expr.range());

View File

@@ -43,8 +43,8 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
};
let (Some(grandparent), Some(parent)) = (
checker.semantic().current_expression_grandparent(),
checker.semantic().current_expression_parent(),
checker.semantic().expr_grandparent(),
checker.semantic().expr_parent(),
) else {
checker.diagnostics.push(Diagnostic::new(
CallDatetimeStrptimeWithoutZone,

View File

@@ -18,8 +18,9 @@ use super::helpers;
/// `datetime` objects are preferred, as they represent a specific moment in
/// time, unlike "naive" objects.
///
/// `datetime.datetime.today()` creates a "naive" object; instead, use
/// `datetime.datetime.now(tz=)` to create a timezone-aware object.
/// `datetime.datetime.today()` crates a "naive" object; instead, use
/// instead, use `datetime.datetime.now(tz=)` to create a timezone-aware
/// object.
///
/// ## Example
/// ```python

View File

@@ -8,43 +8,6 @@ use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.utcfromtimestamp()`.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.utcfromtimestamp()` returns a naive datetime object;
/// instead, use `datetime.datetime.fromtimestamp(ts, tz=)` to return a
/// timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.utcfromtimestamp()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeUtcfromtimestamp;
@@ -58,6 +21,15 @@ impl Violation for CallDatetimeUtcfromtimestamp {
}
}
/// Checks for `datetime.datetime.utcfromtimestamp()`. (DTZ004)
///
/// ## Why is this bad?
///
/// Because naive `datetime` objects are treated by many `datetime` methods as
/// local times, it is preferred to use aware datetimes to represent times in
/// UTC. As such, the recommended way to create an object representing a
/// specific timestamp in UTC is by calling `datetime.fromtimestamp(timestamp,
/// tz=timezone.utc)`.
pub(crate) fn call_datetime_utcfromtimestamp(
checker: &mut Checker,
func: &Expr,

View File

@@ -8,42 +8,6 @@ use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.utcnow()`.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.utcnow()` returns a naive datetime object; instead, use
/// `datetime.datetime.now(tz=)` to return a timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.utcnow()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeUtcnow;
@@ -57,6 +21,14 @@ impl Violation for CallDatetimeUtcnow {
}
}
/// Checks for `datetime.datetime.today()`. (DTZ003)
///
/// ## Why is this bad?
///
/// Because naive `datetime` objects are treated by many `datetime` methods as
/// local times, it is preferred to use aware datetimes to represent times in
/// UTC. As such, the recommended way to create an object representing the
/// current time in UTC is by calling `datetime.now(timezone.utc)`.
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker
.semantic()

View File

@@ -31,13 +31,6 @@ use super::helpers;
/// ```python
/// import datetime
///
/// datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
/// ```
#[violation]

View File

@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
/// Check if the parent expression is a call to `astimezone`. This assumes that
/// the current expression is a `datetime.datetime` object.
pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
checker.semantic().current_expression_parent().is_some_and( |parent| {
checker.semantic().expr_parent().is_some_and( |parent| {
matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone")
})
}

View File

@@ -210,7 +210,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
}
}
// Check for f-strings.
Expr::FString(_) => {
Expr::JoinedStr(_) => {
if checker.enabled(Rule::FStringInException) {
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -52,7 +52,7 @@ impl Violation for FStringInGetTextFuncCall {
/// INT001
pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) {
if let Some(first) = args.first() {
if first.is_f_string_expr() {
if first.is_joined_str_expr() {
checker
.diagnostics
.push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));

View File

@@ -50,14 +50,14 @@ pub(crate) fn explicit(expr: &Expr, locator: &Locator) -> Option<Diagnostic> {
if matches!(op, Operator::Add) {
if matches!(
left.as_ref(),
Expr::FString(_)
Expr::JoinedStr(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..
})
) && matches!(
right.as_ref(),
Expr::FString(_)
Expr::JoinedStr(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..

View File

@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{Binding, Imported};
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -56,13 +56,11 @@ pub(crate) fn unconventional_import_alias(
binding: &Binding,
conventions: &FxHashMap<String, String>,
) -> Option<Diagnostic> {
let Some(import) = binding.as_any_import() else {
let Some(qualified_name) = binding.qualified_name() else {
return None;
};
let qualified_name = import.qualified_name();
let Some(expected_alias) = conventions.get(qualified_name.as_str()) else {
let Some(expected_alias) = conventions.get(qualified_name) else {
return None;
};
@@ -73,7 +71,7 @@ pub(crate) fn unconventional_import_alias(
let mut diagnostic = Diagnostic::new(
UnconventionalImportAlias {
name: qualified_name,
name: qualified_name.to_string(),
asname: expected_alias.to_string(),
},
binding.range,

View File

@@ -62,7 +62,7 @@ fn check_msg(checker: &mut Checker, msg: &Expr) {
_ => {}
},
// Check for f-strings.
Expr::FString(_) => {
Expr::JoinedStr(_) => {
if checker.enabled(Rule::LoggingFString) {
checker
.diagnostics

View File

@@ -15,15 +15,6 @@ G004.py:5:27: G004 Logging statement uses f-string
4 | logging.info(f"Hello {name}")
5 | logging.log(logging.INFO, f"Hello {name}")
| ^^^^^^^^^^^^^^^ G004
6 |
7 | _LOGGER = logging.getLogger()
|
G004.py:8:14: G004 Logging statement uses f-string
|
7 | _LOGGER = logging.getLogger()
8 | _LOGGER.info(f"{__name__}")
| ^^^^^^^^^^^^^ G004
|

View File

@@ -66,7 +66,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
return;
};
if !checker.semantic().current_scope().kind.is_class() {
if !checker.semantic().scope().kind.is_class() {
return;
}

View File

@@ -2,7 +2,9 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{Decorator, Expr, Parameters, Ranged, TypeParam, TypeParams};
use ruff_python_ast::{
Decorator, Expr, ParameterWithDefault, Parameters, Ranged, TypeParam, TypeParams,
};
use ruff_python_semantic::analyze::visibility::{
is_abstract, is_classmethod, is_new, is_overload, is_staticmethod,
};
@@ -75,23 +77,15 @@ pub(crate) fn custom_type_var_return_type(
args: &Parameters,
type_params: Option<&TypeParams>,
) {
// Given, e.g., `def foo(self: _S, arg: bytes) -> _T`, extract `_T`.
let Some(return_annotation) = returns else {
if args.args.is_empty() && args.posonlyargs.is_empty() {
return;
}
let Some(returns) = returns else {
return;
};
// Given, e.g., `def foo(self: _S, arg: bytes)`, extract `_S`.
let Some(self_or_cls_annotation) = args
.posonlyargs
.iter()
.chain(args.args.iter())
.next()
.and_then(|parameter_with_default| parameter_with_default.parameter.annotation.as_ref())
else {
return;
};
if !checker.semantic().current_scope().kind.is_class() {
if !checker.semantic().scope().kind.is_class() {
return;
};
@@ -103,12 +97,14 @@ pub(crate) fn custom_type_var_return_type(
return;
}
let returns = map_subscript(returns);
let uses_custom_var: bool =
if is_classmethod(decorator_list, checker.semantic()) || is_new(name) {
class_method(self_or_cls_annotation, return_annotation, type_params)
class_method(args, returns, type_params)
} else {
// If not static, or a class method or __new__ we know it is an instance method
instance_method(self_or_cls_annotation, return_annotation, type_params)
instance_method(args, returns, type_params)
};
if uses_custom_var {
@@ -116,7 +112,7 @@ pub(crate) fn custom_type_var_return_type(
CustomTypeVarReturnType {
method_name: name.to_string(),
},
return_annotation.range(),
returns.range(),
));
}
}
@@ -124,11 +120,17 @@ pub(crate) fn custom_type_var_return_type(
/// Returns `true` if the class method is annotated with a custom `TypeVar` that is likely
/// private.
fn class_method(
cls_annotation: &Expr,
args: &Parameters,
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = cls_annotation else {
let ParameterWithDefault { parameter, .. } = &args.args[0];
let Some(annotation) = &parameter.annotation else {
return false;
};
let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = annotation.as_ref() else {
return false;
};
@@ -146,7 +148,7 @@ fn class_method(
return false;
};
let Expr::Name(return_annotation) = map_subscript(return_annotation) else {
let Expr::Name(return_annotation) = return_annotation else {
return false;
};
@@ -160,20 +162,26 @@ fn class_method(
/// Returns `true` if the instance method is annotated with a custom `TypeVar` that is likely
/// private.
fn instance_method(
self_annotation: &Expr,
args: &Parameters,
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let ParameterWithDefault { parameter, .. } = &args.args[0];
let Some(annotation) = &parameter.annotation else {
return false;
};
let Expr::Name(ast::ExprName {
id: first_arg_type, ..
}) = self_annotation
}) = annotation.as_ref()
else {
return false;
};
let Expr::Name(ast::ExprName {
id: return_type, ..
}) = map_subscript(return_annotation)
}) = return_annotation
else {
return false;
};

View File

@@ -242,11 +242,11 @@ fn check_positional_args(
/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation.
fn non_none_annotation_element<'a>(
annotation: &'a Expr,
semantic: &SemanticModel,
model: &SemanticModel,
) -> Option<&'a Expr> {
// E.g., `typing.Union` or `typing.Optional`
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation {
if semantic.match_typing_expr(value, "Optional") {
if model.match_typing_expr(value, "Optional") {
return if is_const_none(slice) {
None
} else {
@@ -254,7 +254,7 @@ fn non_none_annotation_element<'a>(
};
}
if !semantic.match_typing_expr(value, "Union") {
if !model.match_typing_expr(value, "Union") {
return None;
}
@@ -297,8 +297,8 @@ fn non_none_annotation_element<'a>(
}
/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type.
fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| {
@@ -310,34 +310,34 @@ fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool {
}
/// Return `true` if the [`Expr`] is `BaseException`.
fn is_base_exception(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "BaseException"]))
}
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool {
model
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["types", "TracebackType"]))
}
/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`.
fn is_base_exception_type(expr: &Expr, semantic: &SemanticModel) -> bool {
fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool {
let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else {
return false;
};
if semantic.match_typing_expr(value, "Type")
|| semantic
if model.match_typing_expr(value, "Type")
|| model
.resolve_call_path(value)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
{
is_base_exception(slice, semantic)
is_base_exception(slice, model)
} else {
false
}

View File

@@ -1,9 +1,9 @@
use ruff_python_ast::Ranged;
use ruff_python_ast as ast;
use ruff_python_ast::{Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::Expr;
use ruff_python_semantic::{Definition, Member, MemberKind};
use crate::checkers::ast::Checker;
@@ -49,14 +49,14 @@ use crate::checkers::ast::Checker;
/// ```
#[violation]
pub struct IterMethodReturnIterable {
is_async: bool,
async_: bool,
}
impl Violation for IterMethodReturnIterable {
#[derive_message_formats]
fn message(&self) -> String {
let IterMethodReturnIterable { is_async } = self;
if *is_async {
let IterMethodReturnIterable { async_ } = self;
if *async_ {
format!("`__aiter__` methods should return an `AsyncIterator`, not an `AsyncIterable`")
} else {
format!("`__iter__` methods should return an `Iterator`, not an `Iterable`")
@@ -67,31 +67,41 @@ impl Violation for IterMethodReturnIterable {
/// PYI045
pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &Definition) {
let Definition::Member(Member {
kind: MemberKind::Method(function),
kind: MemberKind::Method,
stmt,
..
}) = definition
else {
return;
};
let Some(returns) = function.returns.as_ref() else {
let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else {
return;
};
let is_async = match function.name.as_str() {
let Some(returns) = returns else {
return;
};
let annotation = if let Expr::Subscript(ast::ExprSubscript { value, .. }) = returns.as_ref() {
// Ex) `Iterable[T]`
value
} else {
// Ex) `Iterable`, `typing.Iterable`
returns
};
let async_ = match name.as_str() {
"__iter__" => false,
"__aiter__" => true,
_ => return,
};
// Support both `Iterable` and `Iterable[T]`.
let annotation = map_subscript(returns);
if checker
.semantic()
.resolve_call_path(map_subscript(annotation))
.resolve_call_path(annotation)
.is_some_and(|call_path| {
if is_async {
if async_ {
matches!(
call_path.as_slice(),
["typing", "AsyncIterable"] | ["collections", "abc", "AsyncIterable"]
@@ -105,7 +115,7 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De
})
{
checker.diagnostics.push(Diagnostic::new(
IterMethodReturnIterable { is_async },
IterMethodReturnIterable { async_ },
returns.range(),
));
}

View File

@@ -113,13 +113,13 @@ impl Violation for NonSelfReturnType {
pub(crate) fn non_self_return_type(
checker: &mut Checker,
stmt: &Stmt,
is_async: bool,
name: &str,
decorator_list: &[Decorator],
returns: Option<&Expr>,
parameters: &Parameters,
async_: bool,
) {
let ScopeKind::Class(class_def) = checker.semantic().current_scope().kind else {
let ScopeKind::Class(class_def) = checker.semantic().scope().kind else {
return;
};
@@ -138,7 +138,7 @@ pub(crate) fn non_self_return_type(
return;
}
if is_async {
if async_ {
if name == "__aenter__"
&& is_name(returns, &class_def.name)
&& !is_final(&class_def.decorator_list, checker.semantic())

View File

@@ -121,7 +121,7 @@ impl fmt::Display for ExprType {
/// Return the [`ExprType`] of an [`Expr]` if it is a builtin type (e.g. `int`, `bool`, `float`,
/// `str`, `bytes`, or `complex`).
fn match_builtin_type(expr: &Expr, semantic: &SemanticModel) -> Option<ExprType> {
fn match_builtin_type(expr: &Expr, model: &SemanticModel) -> Option<ExprType> {
let name = expr.as_name_expr()?;
let result = match name.id.as_str() {
"int" => ExprType::Int,
@@ -132,7 +132,7 @@ fn match_builtin_type(expr: &Expr, semantic: &SemanticModel) -> Option<ExprType>
"complex" => ExprType::Complex,
_ => return None,
};
if !semantic.is_builtin(name.id.as_str()) {
if !model.is_builtin(name.id.as_str()) {
return None;
}
Some(result)

View File

@@ -349,8 +349,8 @@ fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool {
fn is_special_assignment(target: &Expr, semantic: &SemanticModel) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = target {
match id.as_str() {
"__all__" => semantic.current_scope().kind.is_module(),
"__match_args__" | "__slots__" => semantic.current_scope().kind.is_class(),
"__all__" => semantic.scope().kind.is_module(),
"__match_args__" | "__slots__" => semantic.scope().kind.is_class(),
_ => false,
}
} else {
@@ -569,9 +569,7 @@ pub(crate) fn unannotated_assignment_in_stub(
return;
}
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
checker.semantic().current_scope().kind
{
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) = checker.semantic().scope().kind {
if is_enum(arguments.as_deref(), checker.semantic()) {
return;
}

View File

@@ -63,7 +63,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
return;
}
if !checker.semantic().current_scope().kind.is_class() {
if !checker.semantic().scope().kind.is_class() {
return;
}
@@ -96,12 +96,11 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let stmt = checker.semantic().stmt();
let parent = checker.semantic().stmt_parent();
let edit = delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(
Fix::automatic(edit)
.isolate(checker.isolation(checker.semantic().current_statement_parent())),
Fix::automatic(edit).isolate(checker.isolation(checker.semantic().stmt_parent())),
);
}
checker.diagnostics.push(diagnostic);

View File

@@ -43,7 +43,7 @@ impl AlwaysAutofixableViolation for StringOrBytesTooLong {
/// PYI053
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) {
// Ignore docstrings.
if is_docstring_stmt(checker.semantic().current_statement()) {
if is_docstring_stmt(checker.semantic().stmt()) {
return;
}

View File

@@ -1,7 +1,6 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Imported;
use ruff_python_semantic::{Binding, BindingKind};
use ruff_python_semantic::{Binding, BindingKind, FromImport};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -51,10 +50,10 @@ pub(crate) fn unaliased_collections_abc_set_import(
checker: &Checker,
binding: &Binding,
) -> Option<Diagnostic> {
let BindingKind::FromImport(import) = &binding.kind else {
let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else {
return None;
};
if !matches!(import.call_path(), ["collections", "abc", "Set"]) {
if qualified_name.as_str() != "collections.abc.Set" {
return None;
}

View File

@@ -173,8 +173,7 @@ pub(crate) fn unused_private_type_var(
let Some(source) = binding.source else {
continue;
};
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) =
checker.semantic().statement(source)
let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = checker.semantic().stmts[source]
else {
continue;
};
@@ -218,7 +217,7 @@ pub(crate) fn unused_private_protocol(
continue;
};
let Stmt::ClassDef(class_def) = checker.semantic().statement(source) else {
let Stmt::ClassDef(class_def) = checker.semantic().stmts[source] else {
continue;
};
@@ -261,7 +260,7 @@ pub(crate) fn unused_private_type_alias(
};
let Stmt::AnnAssign(ast::StmtAnnAssign {
target, annotation, ..
}) = checker.semantic().statement(source)
}) = checker.semantic().stmts[source]
else {
continue;
};
@@ -305,7 +304,7 @@ pub(crate) fn unused_private_typed_dict(
let Some(source) = binding.source else {
continue;
};
let Stmt::ClassDef(class_def) = checker.semantic().statement(source) else {
let Stmt::ClassDef(class_def) = checker.semantic().stmts[source] else {
continue;
};

View File

@@ -1,44 +1,42 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI016.py:7:15: PYI016 [*] Duplicate union member `str`
PYI016.py:5:15: PYI016 [*] Duplicate union member `str`
|
6 | # Should emit for duplicate field types.
7 | field2: str | str # PYI016: Duplicate union member `str`
4 | # Should emit for duplicate field types.
5 | field2: str | str # PYI016: Duplicate union member `str`
| ^^^ PYI016
8 |
9 | # Should emit for union types in arguments.
|
= help: Remove duplicate union member `str`
Fix
4 4 | field1: str
5 5 |
6 6 | # Should emit for duplicate field types.
7 |-field2: str | str # PYI016: Duplicate union member `str`
7 |+field2: str # PYI016: Duplicate union member `str`
8 8 |
9 9 | # Should emit for union types in arguments.
10 10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
2 2 | field1: str
3 3 |
4 4 | # Should emit for duplicate field types.
5 |-field2: str | str # PYI016: Duplicate union member `str`
5 |+field2: str # PYI016: Duplicate union member `str`
6 6 |
7 7 |
8 8 | # Should emit for union types in arguments.
PYI016.py:10:23: PYI016 [*] Duplicate union member `int`
PYI016.py:9:23: PYI016 [*] Duplicate union member `int`
|
9 | # Should emit for union types in arguments.
10 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
8 | # Should emit for union types in arguments.
9 | def func1(arg1: int | int): # PYI016: Duplicate union member `int`
| ^^^ PYI016
11 | print(arg1)
10 | print(arg1)
|
= help: Remove duplicate union member `int`
Fix
7 7 | field2: str | str # PYI016: Duplicate union member `str`
8 8 |
9 9 | # Should emit for union types in arguments.
10 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int`
10 |+def func1(arg1: int): # PYI016: Duplicate union member `int`
11 11 | print(arg1)
6 6 |
7 7 |
8 8 | # Should emit for union types in arguments.
9 |-def func1(arg1: int | int): # PYI016: Duplicate union member `int`
9 |+def func1(arg1: int): # PYI016: Duplicate union member `int`
10 10 | print(arg1)
11 11 |
12 12 |
13 13 | # Should emit for unions in return types.
PYI016.py:14:22: PYI016 [*] Duplicate union member `str`
|
@@ -50,494 +48,170 @@ PYI016.py:14:22: PYI016 [*] Duplicate union member `str`
= help: Remove duplicate union member `str`
Fix
11 11 | print(arg1)
11 11 |
12 12 |
13 13 | # Should emit for unions in return types.
14 |-def func2() -> str | str: # PYI016: Duplicate union member `str`
14 |+def func2() -> str: # PYI016: Duplicate union member `str`
15 15 | return "my string"
16 16 |
17 17 | # Should emit in longer unions, even if not directly adjacent.
17 17 |
PYI016.py:18:15: PYI016 [*] Duplicate union member `str`
PYI016.py:19:15: PYI016 [*] Duplicate union member `str`
|
17 | # Should emit in longer unions, even if not directly adjacent.
18 | field3: str | str | int # PYI016: Duplicate union member `str`
18 | # Should emit in longer unions, even if not directly adjacent.
19 | field3: str | str | int # PYI016: Duplicate union member `str`
| ^^^ PYI016
19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 | field5: str | int | str # PYI016: Duplicate union member `str`
20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 | field5: str | int | str # PYI016: Duplicate union member `str`
|
= help: Remove duplicate union member `str`
Fix
15 15 | return "my string"
16 16 |
17 17 | # Should emit in longer unions, even if not directly adjacent.
18 |-field3: str | str | int # PYI016: Duplicate union member `str`
18 |+field3: str | int # PYI016: Duplicate union member `str`
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
17 17 |
18 18 | # Should emit in longer unions, even if not directly adjacent.
19 |-field3: str | str | int # PYI016: Duplicate union member `str`
19 |+field3: str | int # PYI016: Duplicate union member `str`
20 20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 21 | field5: str | int | str # PYI016: Duplicate union member `str`
22 22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
PYI016.py:19:15: PYI016 [*] Duplicate union member `int`
PYI016.py:20:15: PYI016 [*] Duplicate union member `int`
|
17 | # Should emit in longer unions, even if not directly adjacent.
18 | field3: str | str | int # PYI016: Duplicate union member `str`
19 | field4: int | int | str # PYI016: Duplicate union member `int`
18 | # Should emit in longer unions, even if not directly adjacent.
19 | field3: str | str | int # PYI016: Duplicate union member `str`
20 | field4: int | int | str # PYI016: Duplicate union member `int`
| ^^^ PYI016
20 | field5: str | int | str # PYI016: Duplicate union member `str`
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
21 | field5: str | int | str # PYI016: Duplicate union member `str`
22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
= help: Remove duplicate union member `int`
Fix
16 16 |
17 17 | # Should emit in longer unions, even if not directly adjacent.
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
19 |-field4: int | int | str # PYI016: Duplicate union member `int`
19 |+field4: int | str # PYI016: Duplicate union member `int`
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
22 22 |
17 17 |
18 18 | # Should emit in longer unions, even if not directly adjacent.
19 19 | field3: str | str | int # PYI016: Duplicate union member `str`
20 |-field4: int | int | str # PYI016: Duplicate union member `int`
20 |+field4: int | str # PYI016: Duplicate union member `int`
21 21 | field5: str | int | str # PYI016: Duplicate union member `str`
22 22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
23 23 |
PYI016.py:20:21: PYI016 [*] Duplicate union member `str`
PYI016.py:21:21: PYI016 [*] Duplicate union member `str`
|
18 | field3: str | str | int # PYI016: Duplicate union member `str`
19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 | field5: str | int | str # PYI016: Duplicate union member `str`
19 | field3: str | str | int # PYI016: Duplicate union member `str`
20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 | field5: str | int | str # PYI016: Duplicate union member `str`
| ^^^ PYI016
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
|
= help: Remove duplicate union member `str`
Fix
17 17 | # Should emit in longer unions, even if not directly adjacent.
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 |-field5: str | int | str # PYI016: Duplicate union member `str`
20 |+field5: str | int # PYI016: Duplicate union member `str`
21 21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
22 22 |
23 23 | # Shouldn't emit for non-type unions.
18 18 | # Should emit in longer unions, even if not directly adjacent.
19 19 | field3: str | str | int # PYI016: Duplicate union member `str`
20 20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 |-field5: str | int | str # PYI016: Duplicate union member `str`
21 |+field5: str | int # PYI016: Duplicate union member `str`
22 22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
23 23 |
24 24 | # Shouldn't emit for non-type unions.
PYI016.py:21:28: PYI016 [*] Duplicate union member `int`
PYI016.py:22:28: PYI016 [*] Duplicate union member `int`
|
19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 | field5: str | int | str # PYI016: Duplicate union member `str`
21 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 | field5: str | int | str # PYI016: Duplicate union member `str`
22 | field6: int | bool | str | int # PYI016: Duplicate union member `int`
| ^^^ PYI016
22 |
23 | # Shouldn't emit for non-type unions.
23 |
24 | # Shouldn't emit for non-type unions.
|
= help: Remove duplicate union member `int`
Fix
18 18 | field3: str | str | int # PYI016: Duplicate union member `str`
19 19 | field4: int | int | str # PYI016: Duplicate union member `int`
20 20 | field5: str | int | str # PYI016: Duplicate union member `str`
21 |-field6: int | bool | str | int # PYI016: Duplicate union member `int`
21 |+field6: int | bool | str # PYI016: Duplicate union member `int`
22 22 |
23 23 | # Shouldn't emit for non-type unions.
24 24 | field7 = str | str
19 19 | field3: str | str | int # PYI016: Duplicate union member `str`
20 20 | field4: int | int | str # PYI016: Duplicate union member `int`
21 21 | field5: str | int | str # PYI016: Duplicate union member `str`
22 |-field6: int | bool | str | int # PYI016: Duplicate union member `int`
22 |+field6: int | bool | str # PYI016: Duplicate union member `int`
23 23 |
24 24 | # Shouldn't emit for non-type unions.
25 25 | field7 = str | str
PYI016.py:27:22: PYI016 [*] Duplicate union member `int`
PYI016.py:28:22: PYI016 [*] Duplicate union member `int`
|
26 | # Should emit for strangely-bracketed unions.
27 | field8: int | (str | int) # PYI016: Duplicate union member `int`
27 | # Should emit for strangely-bracketed unions.
28 | field8: int | (str | int) # PYI016: Duplicate union member `int`
| ^^^ PYI016
28 |
29 | # Should handle user brackets when fixing.
29 |
30 | # Should handle user brackets when fixing.
|
= help: Remove duplicate union member `int`
Fix
24 24 | field7 = str | str
25 25 |
26 26 | # Should emit for strangely-bracketed unions.
27 |-field8: int | (str | int) # PYI016: Duplicate union member `int`
27 |+field8: int | (str) # PYI016: Duplicate union member `int`
28 28 |
29 29 | # Should handle user brackets when fixing.
30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
25 25 | field7 = str | str
26 26 |
27 27 | # Should emit for strangely-bracketed unions.
28 |-field8: int | (str | int) # PYI016: Duplicate union member `int`
28 |+field8: int | (str) # PYI016: Duplicate union member `int`
29 29 |
30 30 | # Should handle user brackets when fixing.
31 31 | field9: int | (int | str) # PYI016: Duplicate union member `int`
PYI016.py:30:16: PYI016 [*] Duplicate union member `int`
PYI016.py:31:16: PYI016 [*] Duplicate union member `int`
|
29 | # Should handle user brackets when fixing.
30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
30 | # Should handle user brackets when fixing.
31 | field9: int | (int | str) # PYI016: Duplicate union member `int`
| ^^^ PYI016
31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
32 | field10: (str | int) | str # PYI016: Duplicate union member `str`
|
= help: Remove duplicate union member `int`
Fix
27 27 | field8: int | (str | int) # PYI016: Duplicate union member `int`
28 28 |
29 29 | # Should handle user brackets when fixing.
30 |-field9: int | (int | str) # PYI016: Duplicate union member `int`
30 |+field9: int | (str) # PYI016: Duplicate union member `int`
31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
32 32 |
33 33 | # Should emit for nested unions.
28 28 | field8: int | (str | int) # PYI016: Duplicate union member `int`
29 29 |
30 30 | # Should handle user brackets when fixing.
31 |-field9: int | (int | str) # PYI016: Duplicate union member `int`
31 |+field9: int | (str) # PYI016: Duplicate union member `int`
32 32 | field10: (str | int) | str # PYI016: Duplicate union member `str`
33 33 |
34 34 | # Should emit for nested unions.
PYI016.py:31:24: PYI016 [*] Duplicate union member `str`
PYI016.py:32:24: PYI016 [*] Duplicate union member `str`
|
29 | # Should handle user brackets when fixing.
30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
30 | # Should handle user brackets when fixing.
31 | field9: int | (int | str) # PYI016: Duplicate union member `int`
32 | field10: (str | int) | str # PYI016: Duplicate union member `str`
| ^^^ PYI016
32 |
33 | # Should emit for nested unions.
33 |
34 | # Should emit for nested unions.
|
= help: Remove duplicate union member `str`
Fix
28 28 |
29 29 | # Should handle user brackets when fixing.
30 30 | field9: int | (int | str) # PYI016: Duplicate union member `int`
31 |-field10: (str | int) | str # PYI016: Duplicate union member `str`
31 |+field10: str | int # PYI016: Duplicate union member `str`
32 32 |
33 33 | # Should emit for nested unions.
34 34 | field11: dict[int | int, str]
29 29 |
30 30 | # Should handle user brackets when fixing.
31 31 | field9: int | (int | str) # PYI016: Duplicate union member `int`
32 |-field10: (str | int) | str # PYI016: Duplicate union member `str`
32 |+field10: str | int # PYI016: Duplicate union member `str`
33 33 |
34 34 | # Should emit for nested unions.
35 35 | field11: dict[int | int, str]
PYI016.py:34:21: PYI016 [*] Duplicate union member `int`
PYI016.py:35:21: PYI016 [*] Duplicate union member `int`
|
33 | # Should emit for nested unions.
34 | field11: dict[int | int, str]
34 | # Should emit for nested unions.
35 | field11: dict[int | int, str]
| ^^^ PYI016
35 |
36 | # Should emit for unions with more than two cases
|
= help: Remove duplicate union member `int`
Fix
31 31 | field10: (str | int) | str # PYI016: Duplicate union member `str`
32 32 |
33 33 | # Should emit for nested unions.
34 |-field11: dict[int | int, str]
34 |+field11: dict[int, str]
35 35 |
36 36 | # Should emit for unions with more than two cases
37 37 | field12: int | int | int # Error
PYI016.py:37:16: PYI016 [*] Duplicate union member `int`
|
36 | # Should emit for unions with more than two cases
37 | field12: int | int | int # Error
| ^^^ PYI016
38 | field13: int | int | int | int # Error
|
= help: Remove duplicate union member `int`
Fix
34 34 | field11: dict[int | int, str]
35 35 |
36 36 | # Should emit for unions with more than two cases
37 |-field12: int | int | int # Error
37 |+field12: int | int # Error
38 38 | field13: int | int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
PYI016.py:37:22: PYI016 [*] Duplicate union member `int`
|
36 | # Should emit for unions with more than two cases
37 | field12: int | int | int # Error
| ^^^ PYI016
38 | field13: int | int | int | int # Error
|
= help: Remove duplicate union member `int`
Fix
34 34 | field11: dict[int | int, str]
35 35 |
36 36 | # Should emit for unions with more than two cases
37 |-field12: int | int | int # Error
37 |+field12: int | int # Error
38 38 | field13: int | int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
PYI016.py:38:16: PYI016 [*] Duplicate union member `int`
|
36 | # Should emit for unions with more than two cases
37 | field12: int | int | int # Error
38 | field13: int | int | int | int # Error
| ^^^ PYI016
39 |
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
= help: Remove duplicate union member `int`
Fix
35 35 |
36 36 | # Should emit for unions with more than two cases
37 37 | field12: int | int | int # Error
38 |-field13: int | int | int | int # Error
38 |+field13: int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 41 | field14: int | int | str | int # Error
PYI016.py:38:22: PYI016 [*] Duplicate union member `int`
|
36 | # Should emit for unions with more than two cases
37 | field12: int | int | int # Error
38 | field13: int | int | int | int # Error
| ^^^ PYI016
39 |
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
= help: Remove duplicate union member `int`
Fix
35 35 |
36 36 | # Should emit for unions with more than two cases
37 37 | field12: int | int | int # Error
38 |-field13: int | int | int | int # Error
38 |+field13: int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 41 | field14: int | int | str | int # Error
PYI016.py:38:28: PYI016 [*] Duplicate union member `int`
|
36 | # Should emit for unions with more than two cases
37 | field12: int | int | int # Error
38 | field13: int | int | int | int # Error
| ^^^ PYI016
39 |
40 | # Should emit for unions with more than two cases, even if not directly adjacent
|
= help: Remove duplicate union member `int`
Fix
35 35 |
36 36 | # Should emit for unions with more than two cases
37 37 | field12: int | int | int # Error
38 |-field13: int | int | int | int # Error
38 |+field13: int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 41 | field14: int | int | str | int # Error
PYI016.py:41:16: PYI016 [*] Duplicate union member `int`
|
40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 | field14: int | int | str | int # Error
| ^^^ PYI016
42 |
43 | # Should emit for duplicate literal types; also covered by PYI030
|
= help: Remove duplicate union member `int`
Fix
38 38 | field13: int | int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 |-field14: int | int | str | int # Error
41 |+field14: int | str | int # Error
42 42 |
43 43 | # Should emit for duplicate literal types; also covered by PYI030
44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error
PYI016.py:41:28: PYI016 [*] Duplicate union member `int`
|
40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 | field14: int | int | str | int # Error
| ^^^ PYI016
42 |
43 | # Should emit for duplicate literal types; also covered by PYI030
|
= help: Remove duplicate union member `int`
Fix
38 38 | field13: int | int | int | int # Error
39 39 |
40 40 | # Should emit for unions with more than two cases, even if not directly adjacent
41 |-field14: int | int | str | int # Error
41 |+field14: int | int | str # Error
42 42 |
43 43 | # Should emit for duplicate literal types; also covered by PYI030
44 44 | field15: typing.Literal[1] | typing.Literal[1] # Error
PYI016.py:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]`
|
43 | # Should emit for duplicate literal types; also covered by PYI030
44 | field15: typing.Literal[1] | typing.Literal[1] # Error
| ^^^^^^^^^^^^^^^^^ PYI016
45 |
46 | # Shouldn't emit if in new parent type
|
= help: Remove duplicate union member `typing.Literal[1]`
Fix
41 41 | field14: int | int | str | int # Error
42 42 |
43 43 | # Should emit for duplicate literal types; also covered by PYI030
44 |-field15: typing.Literal[1] | typing.Literal[1] # Error
44 |+field15: typing.Literal[1] # Error
45 45 |
46 46 | # Shouldn't emit if in new parent type
47 47 | field16: int | dict[int, str] # OK
PYI016.py:57:5: PYI016 Duplicate union member `set[int]`
|
55 | int # foo
56 | ],
57 | set[
| _____^
58 | | int # bar
59 | | ],
| |_____^ PYI016
60 | ] # Error, newline and comment will not be emitted in message
|
= help: Remove duplicate union member `set[int]`
PYI016.py:63:28: PYI016 Duplicate union member `int`
|
62 | # Should emit in cases with `typing.Union` instead of `|`
63 | field19: typing.Union[int, int] # Error
| ^^^ PYI016
64 |
65 | # Should emit in cases with nested `typing.Union`
|
= help: Remove duplicate union member `int`
PYI016.py:66:41: PYI016 Duplicate union member `int`
|
65 | # Should emit in cases with nested `typing.Union`
66 | field20: typing.Union[int, typing.Union[int, str]] # Error
| ^^^ PYI016
67 |
68 | # Should emit in cases with mixed `typing.Union` and `|`
|
= help: Remove duplicate union member `int`
PYI016.py:69:28: PYI016 [*] Duplicate union member `int`
|
68 | # Should emit in cases with mixed `typing.Union` and `|`
69 | field21: typing.Union[int, int | str] # Error
| ^^^ PYI016
70 |
71 | # Should emit only once in cases with multiple nested `typing.Union`
|
= help: Remove duplicate union member `int`
Fix
66 66 | field20: typing.Union[int, typing.Union[int, str]] # Error
67 67 |
68 68 | # Should emit in cases with mixed `typing.Union` and `|`
69 |-field21: typing.Union[int, int | str] # Error
69 |+field21: typing.Union[int, str] # Error
70 70 |
71 71 | # Should emit only once in cases with multiple nested `typing.Union`
72 72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
PYI016.py:72:41: PYI016 Duplicate union member `int`
|
71 | # Should emit only once in cases with multiple nested `typing.Union`
72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
| ^^^ PYI016
73 |
74 | # Should emit in cases with newlines
|
= help: Remove duplicate union member `int`
PYI016.py:72:59: PYI016 Duplicate union member `int`
|
71 | # Should emit only once in cases with multiple nested `typing.Union`
72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
| ^^^ PYI016
73 |
74 | # Should emit in cases with newlines
|
= help: Remove duplicate union member `int`
PYI016.py:72:64: PYI016 Duplicate union member `int`
|
71 | # Should emit only once in cases with multiple nested `typing.Union`
72 | field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
| ^^^ PYI016
73 |
74 | # Should emit in cases with newlines
|
= help: Remove duplicate union member `int`
PYI016.py:76:12: PYI016 [*] Duplicate union member `set[int]`
|
74 | # Should emit in cases with newlines
75 | field23: set[ # foo
76 | int] | set[int]
| ^^^^^^^^ PYI016
77 |
78 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `set[int]`
Fix
73 73 |
74 74 | # Should emit in cases with newlines
75 75 | field23: set[ # foo
76 |- int] | set[int]
76 |+ int]
77 77 |
78 78 | # Should emit twice (once for each `int` in the nested union, both of which are
79 79 | # duplicates of the outer `int`), but not three times (which would indicate that
PYI016.py:81:41: PYI016 Duplicate union member `int`
|
79 | # duplicates of the outer `int`), but not three times (which would indicate that
80 | # we incorrectly re-checked the nested union).
81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
| ^^^ PYI016
82 |
83 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `int`
PYI016.py:81:46: PYI016 Duplicate union member `int`
|
79 | # duplicates of the outer `int`), but not three times (which would indicate that
80 | # we incorrectly re-checked the nested union).
81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
| ^^^ PYI016
82 |
83 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `int`
PYI016.py:86:28: PYI016 [*] Duplicate union member `int`
|
84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 | # we incorrectly re-checked the nested union).
86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
| ^^^ PYI016
|
= help: Remove duplicate union member `int`
Fix
83 83 | # Should emit twice (once for each `int` in the nested union, both of which are
84 84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 85 | # we incorrectly re-checked the nested union).
86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
86 |+field25: typing.Union[int, int] # PYI016: Duplicate union member `int`
PYI016.py:86:34: PYI016 [*] Duplicate union member `int`
|
84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 | # we incorrectly re-checked the nested union).
86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
| ^^^ PYI016
|
= help: Remove duplicate union member `int`
Fix
83 83 | # Should emit twice (once for each `int` in the nested union, both of which are
84 84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 85 | # we incorrectly re-checked the nested union).
86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
86 |+field25: typing.Union[int, int] # PYI016: Duplicate union member `int`
32 32 | field10: (str | int) | str # PYI016: Duplicate union member `str`
33 33 |
34 34 | # Should emit for nested unions.
35 |-field11: dict[int | int, str]
35 |+field11: dict[int, str]

View File

@@ -471,8 +471,6 @@ PYI016.pyi:76:12: PYI016 [*] Duplicate union member `set[int]`
75 | field23: set[ # foo
76 | int] | set[int]
| ^^^^^^^^ PYI016
77 |
78 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `set[int]`
@@ -482,62 +480,5 @@ PYI016.pyi:76:12: PYI016 [*] Duplicate union member `set[int]`
75 75 | field23: set[ # foo
76 |- int] | set[int]
76 |+ int]
77 77 |
78 78 | # Should emit twice (once for each `int` in the nested union, both of which are
79 79 | # duplicates of the outer `int`), but not three times (which would indicate that
PYI016.pyi:81:41: PYI016 Duplicate union member `int`
|
79 | # duplicates of the outer `int`), but not three times (which would indicate that
80 | # we incorrectly re-checked the nested union).
81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
| ^^^ PYI016
82 |
83 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `int`
PYI016.pyi:81:46: PYI016 Duplicate union member `int`
|
79 | # duplicates of the outer `int`), but not three times (which would indicate that
80 | # we incorrectly re-checked the nested union).
81 | field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
| ^^^ PYI016
82 |
83 | # Should emit twice (once for each `int` in the nested union, both of which are
|
= help: Remove duplicate union member `int`
PYI016.pyi:86:28: PYI016 [*] Duplicate union member `int`
|
84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 | # we incorrectly re-checked the nested union).
86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
| ^^^ PYI016
|
= help: Remove duplicate union member `int`
Fix
83 83 | # Should emit twice (once for each `int` in the nested union, both of which are
84 84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 85 | # we incorrectly re-checked the nested union).
86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
86 |+field25: typing.Union[int, int] # PYI016: Duplicate union member `int`
PYI016.pyi:86:34: PYI016 [*] Duplicate union member `int`
|
84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 | # we incorrectly re-checked the nested union).
86 | field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
| ^^^ PYI016
|
= help: Remove duplicate union member `int`
Fix
83 83 | # Should emit twice (once for each `int` in the nested union, both of which are
84 84 | # duplicates of the outer `int`), but not three times (which would indicate that
85 85 | # we incorrectly re-checked the nested union).
86 |-field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
86 |+field25: typing.Union[int, int] # PYI016: Duplicate union member `int`

View File

@@ -21,24 +21,17 @@ PYI019.py:14:54: PYI019 Methods like `bad_class_method` should return `typing.Se
| ^^ PYI019
|
PYI019.py:18:55: PYI019 Methods like `bad_posonly_class_method` should return `typing.Self` instead of a custom `TypeVar`
PYI019.py:35:63: PYI019 Methods like `__new__` should return `typing.Self` instead of a custom `TypeVar`
|
17 | @classmethod
18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
| ^^ PYI019
|
PYI019.py:39:63: PYI019 Methods like `__new__` should return `typing.Self` instead of a custom `TypeVar`
|
37 | # Python > 3.12
38 | class PEP695BadDunderNew[T]:
39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
33 | # Python > 3.12
34 | class PEP695BadDunderNew[T]:
35 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
| ^ PYI019
|
PYI019.py:42:46: PYI019 Methods like `generic_instance_method` should return `typing.Self` instead of a custom `TypeVar`
PYI019.py:38:46: PYI019 Methods like `generic_instance_method` should return `typing.Self` instead of a custom `TypeVar`
|
42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
38 | def generic_instance_method[S](self: S) -> S: ... # PYI019
| ^ PYI019
|

View File

@@ -21,24 +21,17 @@ PYI019.pyi:14:54: PYI019 Methods like `bad_class_method` should return `typing.S
| ^^ PYI019
|
PYI019.pyi:18:55: PYI019 Methods like `bad_posonly_class_method` should return `typing.Self` instead of a custom `TypeVar`
PYI019.pyi:35:63: PYI019 Methods like `__new__` should return `typing.Self` instead of a custom `TypeVar`
|
17 | @classmethod
18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
| ^^ PYI019
|
PYI019.pyi:39:63: PYI019 Methods like `__new__` should return `typing.Self` instead of a custom `TypeVar`
|
37 | # Python > 3.12
38 | class PEP695BadDunderNew[T]:
39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
33 | # Python > 3.12
34 | class PEP695BadDunderNew[T]:
35 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
| ^ PYI019
|
PYI019.pyi:42:46: PYI019 Methods like `generic_instance_method` should return `typing.Self` instead of a custom `TypeVar`
PYI019.pyi:38:46: PYI019 Methods like `generic_instance_method` should return `typing.Self` instead of a custom `TypeVar`
|
42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
38 | def generic_instance_method[S](self: S) -> S: ... # PYI019
| ^ PYI019
|

View File

@@ -246,8 +246,9 @@ pub(crate) fn unittest_assertion(
if checker.patch(diagnostic.kind.rule()) {
// We're converting an expression to a statement, so avoid applying the fix if
// the assertion is part of a larger expression.
if checker.semantic().current_statement().is_expr_stmt()
&& checker.semantic().current_expression_parent().is_none()
if checker.semantic().stmt().is_expr_stmt()
&& checker.semantic().expr_parent().is_none()
&& !checker.semantic().scope().kind.is_lambda()
&& !checker.indexer().comment_ranges().intersects(expr.range())
{
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {

Some files were not shown because too many files have changed in this diff Show More