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
203 changed files with 1987 additions and 4642 deletions

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

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

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

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

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

View File

@@ -16,7 +16,7 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.current_statement()
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);

View File

@@ -37,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,
@@ -112,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;
@@ -172,14 +168,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(source) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(checker.semantic.statement(source)),
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
@@ -201,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;
@@ -253,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,

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()
@@ -84,7 +84,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// traverse nested unions.
let is_unchecked_union = checker
.semantic
.current_expression_grandparent()
.expr_grandparent()
.and_then(Expr::as_subscript_expr)
.map_or(true, |parent| {
!checker.semantic.match_typing_expr(&parent.value, "Union")
@@ -176,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()
@@ -187,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()
@@ -206,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_any_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,
);
@@ -224,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,
@@ -235,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,
);
@@ -248,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,
@@ -277,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()
@@ -290,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()
@@ -346,8 +341,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
},
) => {
if checker.any_enabled(&[
// pylint
Rule::BadStringFormatCharacter,
// pyflakes
Rule::StringDotFormatInvalidFormat,
Rule::StringDotFormatExtraNamedArguments,
@@ -673,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,
);
@@ -1071,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()
@@ -1087,7 +1080,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Avoid duplicate checks if the parent is an `|` since these rules
// traverse nested unions.
let is_unchecked_union = !matches!(
checker.semantic.current_expression_parent(),
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
..
@@ -1217,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);
}
}
@@ -1226,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);
}
}
@@ -1254,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,7 +63,7 @@ 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);
}
@@ -113,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,
@@ -125,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,
@@ -133,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);
}
@@ -168,14 +168,12 @@ 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);
}
@@ -193,7 +191,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
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,
@@ -348,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,
@@ -414,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);
}
@@ -455,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,
@@ -469,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);
}
@@ -571,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);
}
@@ -746,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);
}
@@ -766,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),
@@ -891,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);
}
@@ -982,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) {
@@ -1004,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) {
@@ -1015,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,
@@ -1110,7 +1108,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker,
stmt,
body,
checker.semantic.current_statement_parent(),
checker.semantic.stmt_parent(),
);
}
if checker.enabled(Rule::RedefinedLoopName) {
@@ -1327,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,
@@ -1338,7 +1336,7 @@ 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()
.scopes()
.any(|scope| scope.kind.is_any_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
@@ -1397,13 +1395,13 @@ 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()
.scopes()
.any(|scope| scope.kind.is_any_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(

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,
@@ -182,7 +180,7 @@ impl<'a> Checker<'a> {
}
// Find the quote character used to start the containing f-string.
let expr = model.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))?;
@@ -202,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())
})
@@ -235,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
@@ -264,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 {
@@ -288,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;
}
@@ -322,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 {
@@ -343,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,
);
}
@@ -372,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;
@@ -391,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,
);
}
@@ -421,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);
}
}
@@ -444,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);
}
}
@@ -657,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
@@ -676,7 +675,7 @@ where
}
} else {
matches!(
self.semantic.current_scope().kind,
self.semantic.scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@@ -777,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) {
@@ -813,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.
@@ -841,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();
}
}
@@ -1231,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) {
@@ -1611,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,
@@ -1646,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 {
@@ -1698,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,
@@ -1725,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;
}
@@ -1733,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);
}
@@ -1787,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);
}
@@ -1825,7 +1824,7 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
match &self.semantic.current_statement() {
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
})
@@ -1929,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 {
@@ -1957,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

@@ -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,6 +1,6 @@
use anyhow::{bail, Result};
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_diagnostics::Edit;
use ruff_source_file::Locator;
@@ -10,7 +10,6 @@ pub(crate) fn add_return_annotation(
locator: &Locator,
stmt: &Stmt,
annotation: &str,
source_type: PySourceType,
) -> Result<Edit> {
let contents = &locator.contents()[stmt.range()];
@@ -18,9 +17,7 @@ pub(crate) fn add_return_annotation(
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(), stmt.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

@@ -709,13 +709,8 @@ pub(crate) fn definition(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
checker.locator(),
stmt,
"None",
checker.source_type,
)
.map(Fix::suggested)
fixes::add_return_annotation(checker.locator(), stmt, "None")
.map(Fix::suggested)
});
}
diagnostics.push(diagnostic);
@@ -732,13 +727,8 @@ pub(crate) fn definition(
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
checker.locator(),
stmt,
return_type,
checker.source_type,
)
.map(Fix::suggested)
fixes::add_return_annotation(checker.locator(), stmt, return_type)
.map(Fix::suggested)
});
}
}

View File

@@ -63,7 +63,7 @@ fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool
}) => {
// Only evaluate the full BinOp, not the nested components.
if model
.current_expression_parent()
.expr_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
if any_over_expr(expr, &has_string_literal) {

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

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

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

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

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

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

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

@@ -119,7 +119,7 @@ pub(crate) fn non_self_return_type(
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;
};

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

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

View File

@@ -550,7 +550,6 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D
arguments,
edits::Parentheses::Preserve,
checker.locator(),
checker.source_type,
)
.map(Fix::suggested)
});

View File

@@ -1,7 +1,5 @@
use ruff_python_ast::{
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext, Ranged};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -97,12 +95,7 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
/// ```
///
/// This method assumes that the first argument is a string.
fn get_parametrize_name_range(
decorator: &Decorator,
expr: &Expr,
locator: &Locator,
source_type: PySourceType,
) -> TextRange {
fn get_parametrize_name_range(decorator: &Decorator, expr: &Expr, locator: &Locator) -> TextRange {
let mut locations = Vec::new();
let mut implicit_concat = None;
@@ -110,7 +103,7 @@ fn get_parametrize_name_range(
// decorator to find them.
for (tok, range) in lexer::lex_starts_at(
locator.slice(decorator.range()),
source_type.as_mode(),
Mode::Module,
decorator.start(),
)
.flatten()
@@ -148,12 +141,8 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
if names.len() > 1 {
match names_type {
types::ParametrizeNameType::Tuple => {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator(),
checker.source_type,
);
let name_range =
get_parametrize_name_range(decorator, expr, checker.locator());
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,
@@ -183,12 +172,8 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
checker.diagnostics.push(diagnostic);
}
types::ParametrizeNameType::List => {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator(),
checker.source_type,
);
let name_range =
get_parametrize_name_range(decorator, expr, checker.locator());
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,

View File

@@ -11,35 +11,6 @@ use crate::registry::Rule;
use super::helpers::is_empty_or_null_string;
/// ## What it does
/// Checks for `pytest.raises` context managers with multiple statements.
///
/// ## Why is this bad?
/// When a `pytest.raises` is used as a context manager and contains multiple
/// statements, it can lead to the test passing when it actually should fail.
/// To avoid this, a `pytest.raises` context manager should only contain
/// a single simple statement that raises the expected exception.
///
/// ## Example
/// ```python
/// def test_foo():
/// with pytest.raises(MyError):
/// setup() # may raise `MyError`
/// func_to_test()
/// assert foo() # not executed
/// ```
///
/// Use instead:
/// ```python
/// def test_foo():
/// setup()
/// with pytest.raises(MyException):
/// func_to_test()
/// assert foo()
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
#[violation]
pub struct PytestRaisesWithMultipleStatements;
@@ -50,41 +21,6 @@ impl Violation for PytestRaisesWithMultipleStatements {
}
}
/// ## What it does
/// Checks for `pytest.raises` calls without a `match` parameter.
///
/// ## Why is this bad?
/// `pytest.raises(Error)` will catch any `Error` and may catch errors that are
/// unrelated to the code under test. To avoid this, `pytest.raises` should be
/// called with a `match` parameter. The exception names that require a `match`
/// parameter can be configured via the
/// `flake8-pytest-style.raises-require-match-for` and
/// `flake8-pytest-style.raises-extend-require-match-for` settings.
///
/// ## Example
/// ```python
/// def test_foo():
/// with pytest.raises(ValueError):
/// ...
///
/// # empty string is also an error
/// with pytest.raises(ValueError, match=""):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def test_foo():
/// with pytest.raises(ValueError, match="expected message"):
/// ...
/// ```
///
/// ## Options
/// - `flake8-pytest-style.raises-require-match-for`
/// - `flake8-pytest-style.raises-extend-require-match-for`
///
/// ## References
/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
#[violation]
pub struct PytestRaisesTooBroad {
exception: String,

View File

@@ -1,5 +1,5 @@
use ruff_python_ast::{self as ast, Arguments, Expr, PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::{self as ast, Arguments, Expr, Ranged};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
@@ -71,7 +71,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
return;
}
let range = match_parens(func.end(), checker.locator(), checker.source_type)
let range = match_parens(func.end(), checker.locator())
.expect("Expected call to include parentheses");
let mut diagnostic = Diagnostic::new(UnnecessaryParenOnRaiseException, range);
if checker.patch(diagnostic.kind.rule()) {
@@ -82,18 +82,14 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
}
/// Return the range of the first parenthesis pair after a given [`TextSize`].
fn match_parens(
start: TextSize,
locator: &Locator,
source_type: PySourceType,
) -> Option<TextRange> {
fn match_parens(start: TextSize, locator: &Locator) -> Option<TextRange> {
let contents = &locator.contents()[usize::from(start)..];
let mut fix_start = None;
let mut fix_end = None;
let mut count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, source_type.as_mode(), start).flatten() {
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, start).flatten() {
match tok {
Tok::Lpar => {
if count == 0 {

View File

@@ -77,7 +77,7 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) {
// Ignore accesses on instances within special methods (e.g., `__eq__`).
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. }) =
checker.semantic().current_scope().kind
checker.semantic().scope().kind
{
if matches!(
name.as_str(),

View File

@@ -378,7 +378,6 @@ pub(crate) fn nested_if_statements(
let colon = first_colon_range(
TextRange::new(test.end(), first_stmt.start()),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
// Check if the parent is already emitting a larger diagnostic including this if statement

View File

@@ -155,14 +155,14 @@ pub(crate) fn negation_with_equal_op(
if !matches!(&ops[..], [CmpOp::Eq]) {
return;
}
if is_exception_check(checker.semantic().current_statement()) {
if is_exception_check(checker.semantic().stmt()) {
return;
}
// Avoid flagging issues in dunder implementations.
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. })
| ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) =
&checker.semantic().current_scope().kind
&checker.semantic().scope().kind
{
if is_dunder_method(name) {
return;
@@ -213,14 +213,14 @@ pub(crate) fn negation_with_not_equal_op(
if !matches!(&ops[..], [CmpOp::NotEq]) {
return;
}
if is_exception_check(checker.semantic().current_statement()) {
if is_exception_check(checker.semantic().stmt()) {
return;
}
// Avoid flagging issues in dunder implementations.
if let ScopeKind::Function(ast::StmtFunctionDef { name, .. })
| ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { name, .. }) =
&checker.semantic().current_scope().kind
&checker.semantic().scope().kind
{
if is_dunder_method(name) {
return;

View File

@@ -119,7 +119,6 @@ pub(crate) fn multiple_with_statements(
body.first().expect("Expected body to be non-empty").start(),
),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
let mut diagnostic = Diagnostic::new(

View File

@@ -43,7 +43,7 @@ impl Violation for OpenFileWithContextHandler {
/// Return `true` if the current expression is nested in an `await
/// exit_stack.enter_async_context` call.
fn match_async_exit_stack(semantic: &SemanticModel) -> bool {
let Some(expr) = semantic.current_expression_grandparent() else {
let Some(expr) = semantic.expr_grandparent() else {
return false;
};
let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else {
@@ -58,7 +58,7 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool {
if attr != "enter_async_context" {
return false;
}
for parent in semantic.current_statements() {
for parent in semantic.parents() {
if let Stmt::With(ast::StmtWith { items, .. }) = parent {
for item in items {
if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr {
@@ -77,7 +77,7 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool {
/// Return `true` if the current expression is nested in an
/// `exit_stack.enter_context` call.
fn match_exit_stack(semantic: &SemanticModel) -> bool {
let Some(expr) = semantic.current_expression_parent() else {
let Some(expr) = semantic.expr_parent() else {
return false;
};
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
@@ -89,7 +89,7 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool {
if attr != "enter_context" {
return false;
}
for parent in semantic.current_statements() {
for parent in semantic.parents() {
if let Stmt::With(ast::StmtWith { items, .. }) = parent {
for item in items {
if let Expr::Call(ast::ExprCall { func, .. }) = &item.context_expr {
@@ -133,7 +133,7 @@ pub(crate) fn open_file_with_context_handler(checker: &mut Checker, func: &Expr)
}
// Ex) `with open("foo.txt") as f: ...`
if checker.semantic().current_statement().is_with_stmt() {
if checker.semantic().stmt().is_with_stmt() {
return;
}

View File

@@ -60,7 +60,7 @@ impl Violation for ReimplementedBuiltin {
/// SIM110, SIM111
pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
if !checker.semantic().current_scope().kind.is_any_function() {
if !checker.semantic().scope().kind.is_any_function() {
return;
}
@@ -73,7 +73,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
// - `for` loop with an `else: return True` or `else: return False`.
// - `for` loop followed by `return True` or `return False`.
let Some(terminal) = match_else_return(stmt).or_else(|| {
let parent = checker.semantic().current_statement_parent()?;
let parent = checker.semantic().stmt_parent()?;
let suite = traversal::suite(stmt, parent)?;
let sibling = traversal::next_sibling(stmt, suite)?;
match_sibling_return(stmt, sibling)

View File

@@ -35,7 +35,7 @@ pub(crate) fn runtime_evaluated(
}
fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticModel) -> bool {
let ScopeKind::Class(class_def) = &semantic.current_scope().kind else {
let ScopeKind::Class(class_def) = &semantic.scope().kind else {
return false;
};
@@ -49,7 +49,7 @@ fn runtime_evaluated_base_class(base_classes: &[String], semantic: &SemanticMode
}
fn runtime_evaluated_decorators(decorators: &[String], semantic: &SemanticModel) -> bool {
let ScopeKind::Class(class_def) = &semantic.current_scope().kind else {
let ScopeKind::Class(class_def) = &semantic.scope().kind else {
return false;
};

View File

@@ -58,8 +58,8 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI
let mut diagnostic = Diagnostic::new(EmptyTypeCheckingBlock, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
// Delete the entire type-checking block.
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 = autofix::edits::delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(parent)));
}

View File

@@ -1,17 +1,15 @@
use std::borrow::Cow;
use anyhow::Result;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{AnyImport, Imported, ResolvedReferenceId, Scope, StatementId};
use ruff_text_size::TextRange;
use ruff_python_semantic::{NodeId, ResolvedReferenceId, Scope};
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::importer::ImportedMembers;
use crate::importer::StmtImports;
/// ## What it does
/// Checks for runtime imports defined in a type-checking block.
@@ -71,13 +69,13 @@ pub(crate) fn runtime_import_in_type_checking_block(
diagnostics: &mut Vec<Diagnostic>,
) {
// Collect all runtime imports by statement.
let mut errors_by_statement: FxHashMap<StatementId, Vec<ImportBinding>> = FxHashMap::default();
let mut ignores_by_statement: FxHashMap<StatementId, Vec<ImportBinding>> = FxHashMap::default();
let mut errors_by_statement: FxHashMap<NodeId, Vec<Import>> = FxHashMap::default();
let mut ignores_by_statement: FxHashMap<NodeId, Vec<Import>> = FxHashMap::default();
for binding_id in scope.binding_ids() {
let binding = checker.semantic().binding(binding_id);
let Some(import) = binding.as_any_import() else {
let Some(qualified_name) = binding.qualified_name() else {
continue;
};
@@ -94,12 +92,12 @@ pub(crate) fn runtime_import_in_type_checking_block(
.is_runtime()
})
{
let Some(statement_id) = binding.source else {
let Some(stmt_id) = binding.source else {
continue;
};
let import = ImportBinding {
import,
let import = Import {
qualified_name,
reference_id,
range: binding.range,
parent_range: binding.parent_range(checker.semantic()),
@@ -114,29 +112,26 @@ pub(crate) fn runtime_import_in_type_checking_block(
})
{
ignores_by_statement
.entry(statement_id)
.entry(stmt_id)
.or_default()
.push(import);
} else {
errors_by_statement
.entry(statement_id)
.or_default()
.push(import);
errors_by_statement.entry(stmt_id).or_default().push(import);
}
}
}
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for (statement_id, imports) in errors_by_statement {
for (stmt_id, imports) in errors_by_statement {
let fix = if checker.patch(Rule::RuntimeImportInTypeCheckingBlock) {
fix_imports(checker, statement_id, &imports).ok()
fix_imports(checker, stmt_id, &imports).ok()
} else {
None
};
for ImportBinding {
import,
for Import {
qualified_name,
range,
parent_range,
..
@@ -144,7 +139,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
{
let mut diagnostic = Diagnostic::new(
RuntimeImportInTypeCheckingBlock {
qualified_name: import.qualified_name(),
qualified_name: qualified_name.to_string(),
},
range,
);
@@ -160,8 +155,8 @@ pub(crate) fn runtime_import_in_type_checking_block(
// Separately, generate a diagnostic for every _ignored_ import, to ensure that the
// suppression comments aren't marked as unused.
for ImportBinding {
import,
for Import {
qualified_name,
range,
parent_range,
..
@@ -169,7 +164,7 @@ pub(crate) fn runtime_import_in_type_checking_block(
{
let mut diagnostic = Diagnostic::new(
RuntimeImportInTypeCheckingBlock {
qualified_name: import.qualified_name(),
qualified_name: qualified_name.to_string(),
},
range,
);
@@ -181,9 +176,9 @@ pub(crate) fn runtime_import_in_type_checking_block(
}
/// A runtime-required import with its surrounding context.
struct ImportBinding<'a> {
struct Import<'a> {
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
import: AnyImport<'a>,
qualified_name: &'a str,
/// The first reference to the imported symbol.
reference_id: ResolvedReferenceId,
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
@@ -193,24 +188,18 @@ struct ImportBinding<'a> {
}
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.
fn fix_imports(
checker: &Checker,
statement_id: StatementId,
imports: &[ImportBinding],
) -> Result<Fix> {
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
let member_names: Vec<Cow<'_, str>> = imports
fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result<Fix> {
let stmt = checker.semantic().stmts[stmt_id];
let parent = checker.semantic().stmts.parent(stmt);
let qualified_names: Vec<&str> = imports
.iter()
.map(|ImportBinding { import, .. }| import)
.map(Imported::member_name)
.map(|Import { qualified_name, .. }| *qualified_name)
.collect();
// Find the first reference across all imports.
let at = imports
.iter()
.map(|ImportBinding { reference_id, .. }| {
.map(|Import { reference_id, .. }| {
checker.semantic().reference(*reference_id).range().start()
})
.min()
@@ -218,8 +207,8 @@ fn fix_imports(
// Step 1) Remove the import.
let remove_import_edit = autofix::edits::remove_unused_imports(
member_names.iter().map(AsRef::as_ref),
statement,
qualified_names.iter().copied(),
stmt,
parent,
checker.locator(),
checker.stylist(),
@@ -228,9 +217,9 @@ fn fix_imports(
// Step 2) Add the import to the top-level.
let add_import_edit = checker.importer().runtime_import_edit(
&ImportedMembers {
statement,
names: member_names.iter().map(AsRef::as_ref).collect(),
&StmtImports {
stmt,
qualified_names,
},
at,
)?;

View File

@@ -1,17 +1,15 @@
use std::borrow::Cow;
use anyhow::Result;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{AnyImport, Binding, Imported, ResolvedReferenceId, Scope, StatementId};
use ruff_text_size::TextRange;
use ruff_python_semantic::{Binding, NodeId, ResolvedReferenceId, Scope};
use crate::autofix;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::importer::ImportedMembers;
use crate::importer::StmtImports;
use crate::rules::isort::{categorize, ImportSection, ImportType};
/// ## What it does
@@ -190,9 +188,9 @@ pub(crate) fn typing_only_runtime_import(
diagnostics: &mut Vec<Diagnostic>,
) {
// Collect all typing-only imports by statement and import type.
let mut errors_by_statement: FxHashMap<(StatementId, ImportType), Vec<ImportBinding>> =
let mut errors_by_statement: FxHashMap<(NodeId, ImportType), Vec<Import>> =
FxHashMap::default();
let mut ignores_by_statement: FxHashMap<(StatementId, ImportType), Vec<ImportBinding>> =
let mut ignores_by_statement: FxHashMap<(NodeId, ImportType), Vec<Import>> =
FxHashMap::default();
for binding_id in scope.binding_ids() {
@@ -208,10 +206,23 @@ pub(crate) fn typing_only_runtime_import(
continue;
}
let Some(import) = binding.as_any_import() else {
let Some(qualified_name) = binding.qualified_name() else {
continue;
};
if is_exempt(
qualified_name,
&checker
.settings
.flake8_type_checking
.exempt_modules
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
) {
continue;
}
let Some(reference_id) = binding.references.first().copied() else {
continue;
};
@@ -225,25 +236,20 @@ pub(crate) fn typing_only_runtime_import(
.is_typing()
})
{
let qualified_name = import.qualified_name();
if is_exempt(
qualified_name.as_str(),
&checker
.settings
.flake8_type_checking
.exempt_modules
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
) {
continue;
}
// Extract the module base and level from the full name.
// Ex) `foo.bar.baz` -> `foo`, `0`
// Ex) `.foo.bar.baz` -> `foo`, `1`
let level = qualified_name
.chars()
.take_while(|c| *c == '.')
.count()
.try_into()
.unwrap();
// Categorize the import, using coarse-grained categorization.
let import_type = match categorize(
qualified_name.as_str(),
None,
qualified_name,
Some(level),
&checker.settings.src,
checker.package(),
&checker.settings.isort.known_modules,
@@ -265,12 +271,12 @@ pub(crate) fn typing_only_runtime_import(
continue;
}
let Some(statement_id) = binding.source else {
let Some(stmt_id) = binding.source else {
continue;
};
let import = ImportBinding {
import,
let import = Import {
qualified_name,
reference_id,
range: binding.range,
parent_range: binding.parent_range(checker.semantic()),
@@ -282,12 +288,12 @@ pub(crate) fn typing_only_runtime_import(
})
{
ignores_by_statement
.entry((statement_id, import_type))
.entry((stmt_id, import_type))
.or_default()
.push(import);
} else {
errors_by_statement
.entry((statement_id, import_type))
.entry((stmt_id, import_type))
.or_default()
.push(import);
}
@@ -296,22 +302,24 @@ pub(crate) fn typing_only_runtime_import(
// Generate a diagnostic for every import, but share a fix across all imports within the same
// statement (excluding those that are ignored).
for ((statement_id, import_type), imports) in errors_by_statement {
for ((stmt_id, import_type), imports) in errors_by_statement {
let fix = if checker.patch(rule_for(import_type)) {
fix_imports(checker, statement_id, &imports).ok()
fix_imports(checker, stmt_id, &imports).ok()
} else {
None
};
for ImportBinding {
import,
for Import {
qualified_name,
range,
parent_range,
..
} in imports
{
let mut diagnostic =
Diagnostic::new(diagnostic_for(import_type, import.qualified_name()), range);
let mut diagnostic = Diagnostic::new(
diagnostic_for(import_type, qualified_name.to_string()),
range,
);
if let Some(range) = parent_range {
diagnostic.set_parent(range.start());
}
@@ -325,15 +333,17 @@ pub(crate) fn typing_only_runtime_import(
// Separately, generate a diagnostic for every _ignored_ import, to ensure that the
// suppression comments aren't marked as unused.
for ((_, import_type), imports) in ignores_by_statement {
for ImportBinding {
import,
for Import {
qualified_name,
range,
parent_range,
..
} in imports
{
let mut diagnostic =
Diagnostic::new(diagnostic_for(import_type, import.qualified_name()), range);
let mut diagnostic = Diagnostic::new(
diagnostic_for(import_type, qualified_name.to_string()),
range,
);
if let Some(range) = parent_range {
diagnostic.set_parent(range.start());
}
@@ -343,9 +353,9 @@ pub(crate) fn typing_only_runtime_import(
}
/// A runtime-required import with its surrounding context.
struct ImportBinding<'a> {
struct Import<'a> {
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
import: AnyImport<'a>,
qualified_name: &'a str,
/// The first reference to the imported symbol.
reference_id: ResolvedReferenceId,
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
@@ -376,13 +386,13 @@ fn diagnostic_for(import_type: ImportType, qualified_name: String) -> Diagnostic
/// Return `true` if `this` is implicitly loaded via importing `that`.
fn is_implicit_import(this: &Binding, that: &Binding) -> bool {
let Some(this_import) = this.as_any_import() else {
let Some(this_module) = this.module_name() else {
return false;
};
let Some(that_import) = that.as_any_import() else {
let Some(that_module) = that.module_name() else {
return false;
};
this_import.module_name() == that_import.module_name()
this_module == that_module
}
/// Return `true` if `name` is exempt from typing-only enforcement.
@@ -402,24 +412,18 @@ fn is_exempt(name: &str, exempt_modules: &[&str]) -> bool {
}
/// Generate a [`Fix`] to remove typing-only imports from a runtime context.
fn fix_imports(
checker: &Checker,
statement_id: StatementId,
imports: &[ImportBinding],
) -> Result<Fix> {
let statement = checker.semantic().statement(statement_id);
let parent = checker.semantic().parent_statement(statement_id);
let member_names: Vec<Cow<'_, str>> = imports
fn fix_imports(checker: &Checker, stmt_id: NodeId, imports: &[Import]) -> Result<Fix> {
let stmt = checker.semantic().stmts[stmt_id];
let parent = checker.semantic().stmts.parent(stmt);
let qualified_names: Vec<&str> = imports
.iter()
.map(|ImportBinding { import, .. }| import)
.map(Imported::member_name)
.map(|Import { qualified_name, .. }| *qualified_name)
.collect();
// Find the first reference across all imports.
let at = imports
.iter()
.map(|ImportBinding { reference_id, .. }| {
.map(|Import { reference_id, .. }| {
checker.semantic().reference(*reference_id).range().start()
})
.min()
@@ -427,8 +431,8 @@ fn fix_imports(
// Step 1) Remove the import.
let remove_import_edit = autofix::edits::remove_unused_imports(
member_names.iter().map(AsRef::as_ref),
statement,
qualified_names.iter().copied(),
stmt,
parent,
checker.locator(),
checker.stylist(),
@@ -437,13 +441,12 @@ fn fix_imports(
// Step 2) Add the import to a `TYPE_CHECKING` block.
let add_import_edit = checker.importer().typing_import_edit(
&ImportedMembers {
statement,
names: member_names.iter().map(AsRef::as_ref).collect(),
&StmtImports {
stmt,
qualified_names,
},
at,
checker.semantic(),
checker.source_type,
)?;
Ok(

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt};
use ruff_python_ast::{self as ast, Ranged, Stmt};
use ruff_text_size::TextRange;
use ruff_source_file::Locator;
@@ -13,7 +13,6 @@ pub(crate) fn annotate_imports<'a>(
comments: Vec<Comment<'a>>,
locator: &Locator,
split_on_trailing_comma: bool,
source_type: PySourceType,
) -> Vec<AnnotatedImport<'a>> {
let mut comments_iter = comments.into_iter().peekable();
@@ -120,7 +119,7 @@ pub(crate) fn annotate_imports<'a>(
names: aliases,
level: level.map(|level| level.to_u32()),
trailing_comma: if split_on_trailing_comma {
trailing_comma(import, locator, source_type)
trailing_comma(import, locator)
} else {
TrailingComma::default()
},

View File

@@ -1,7 +1,6 @@
use std::borrow::Cow;
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_text_size::{TextRange, TextSize};
use ruff_source_file::Locator;
@@ -23,13 +22,9 @@ impl Comment<'_> {
}
/// Collect all comments in an import block.
pub(crate) fn collect_comments<'a>(
range: TextRange,
locator: &'a Locator,
source_type: PySourceType,
) -> Vec<Comment<'a>> {
pub(crate) fn collect_comments<'a>(range: TextRange, locator: &'a Locator) -> Vec<Comment<'a>> {
let contents = locator.slice(range);
lexer::lex_starts_at(contents, source_type.as_mode(), range.start())
lexer::lex_starts_at(contents, Mode::Module, range.start())
.flatten()
.filter_map(|(tok, range)| {
if let Tok::Comment(value) = tok {

View File

@@ -1,5 +1,5 @@
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_python_trivia::PythonWhitespace;
use ruff_source_file::{Locator, UniversalNewlines};
@@ -8,15 +8,11 @@ use crate::rules::isort::types::TrailingComma;
/// Return `true` if a `Stmt::ImportFrom` statement ends with a magic
/// trailing comma.
pub(super) fn trailing_comma(
stmt: &Stmt,
locator: &Locator,
source_type: PySourceType,
) -> TrailingComma {
pub(super) fn trailing_comma(stmt: &Stmt, locator: &Locator) -> TrailingComma {
let contents = locator.slice(stmt.range());
let mut count = 0u32;
let mut trailing_comma = TrailingComma::Absent;
for (tok, _) in lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten() {
for (tok, _) in lexer::lex_starts_at(contents, Mode::Module, stmt.start()).flatten() {
if matches!(tok, Tok::Lpar) {
count = count.saturating_add(1);
}

View File

@@ -11,7 +11,6 @@ pub use categorize::{ImportSection, ImportType};
use comments::Comment;
use normalize::normalize_imports;
use order::order_imports;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use settings::RelativeImportsOrder;
@@ -73,7 +72,6 @@ pub(crate) fn format_imports(
stylist: &Stylist,
src: &[PathBuf],
package: Option<&Path>,
source_type: PySourceType,
combine_as_imports: bool,
force_single_line: bool,
force_sort_within_sections: bool,
@@ -96,13 +94,7 @@ pub(crate) fn format_imports(
section_order: &[ImportSection],
) -> String {
let trailer = &block.trailer;
let block = annotate_imports(
&block.imports,
comments,
locator,
split_on_trailing_comma,
source_type,
);
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(

View File

@@ -1,5 +1,5 @@
use log::error;
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
use ruff_python_ast::{self as ast, Stmt, Suite};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
@@ -91,7 +91,7 @@ fn add_required_import(
locator: &Locator,
stylist: &Stylist,
settings: &Settings,
source_type: PySourceType,
is_stub: bool,
) -> Option<Diagnostic> {
// Don't add imports to semantically-empty files.
if python_ast.iter().all(is_docstring_stmt) {
@@ -99,7 +99,7 @@ fn add_required_import(
}
// We don't need to add `__future__` imports to stubs.
if source_type.is_stub() && required_import.is_future_import() {
if is_stub && required_import.is_future_import() {
return None;
}
@@ -131,7 +131,7 @@ pub(crate) fn add_required_imports(
locator: &Locator,
stylist: &Stylist,
settings: &Settings,
source_type: PySourceType,
is_stub: bool,
) -> Vec<Diagnostic> {
settings
.isort
@@ -172,7 +172,7 @@ pub(crate) fn add_required_imports(
locator,
stylist,
settings,
source_type,
is_stub,
)
})
.collect(),
@@ -190,7 +190,7 @@ pub(crate) fn add_required_imports(
locator,
stylist,
settings,
source_type,
is_stub,
)
})
.collect(),

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use itertools::{EitherOrBoth, Itertools};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_ast::{Ranged, Stmt};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
@@ -87,7 +87,6 @@ pub(crate) fn organize_imports(
indexer: &Indexer,
settings: &Settings,
package: Option<&Path>,
source_type: PySourceType,
) -> Option<Diagnostic> {
let indentation = locator.slice(extract_indentation_range(&block.imports, locator));
let indentation = leading_indentation(indentation);
@@ -106,7 +105,6 @@ pub(crate) fn organize_imports(
let comments = comments::collect_comments(
TextRange::new(range.start(), locator.full_line_end(range.end())),
locator,
source_type,
);
let trailing_line_end = if block.trailer.is_none() {
@@ -125,7 +123,6 @@ pub(crate) fn organize_imports(
stylist,
&settings.src,
package,
source_type,
settings.isort.combine_as_imports,
settings.isort.force_single_line,
settings.isort.force_sort_within_sections,

View File

@@ -1,8 +1,8 @@
use ruff_python_ast as ast;
use ruff_python_ast::Expr;
use ruff_python_semantic::{BindingKind, Imported, SemanticModel};
#[derive(Debug)]
use ruff_python_semantic::{BindingKind, Import, SemanticModel};
pub(super) enum Resolution {
/// The expression resolves to an irrelevant expression type (e.g., a constant).
IrrelevantExpression,
@@ -26,23 +26,26 @@ pub(super) fn test_expression(expr: &Expr, semantic: &SemanticModel) -> Resoluti
| Expr::ListComp(_)
| Expr::DictComp(_)
| Expr::GeneratorExp(_) => Resolution::IrrelevantExpression,
Expr::Name(ast::ExprName { id, .. }) => semantic.find_binding(id).map_or(
Resolution::IrrelevantBinding,
|binding| match &binding.kind {
BindingKind::Annotation
| BindingKind::Argument
| BindingKind::Assignment
| BindingKind::NamedExprAssignment
| BindingKind::UnpackedAssignment
| BindingKind::LoopVar
| BindingKind::Global
| BindingKind::Nonlocal(_) => Resolution::RelevantLocal,
BindingKind::Import(import) if matches!(import.call_path(), ["pandas"]) => {
Resolution::PandasModule
}
_ => Resolution::IrrelevantBinding,
},
),
Expr::Name(ast::ExprName { id, .. }) => {
semantic
.find_binding(id)
.map_or(Resolution::IrrelevantBinding, |binding| {
match binding.kind {
BindingKind::Annotation
| BindingKind::Argument
| BindingKind::Assignment
| BindingKind::NamedExprAssignment
| BindingKind::UnpackedAssignment
| BindingKind::LoopVar
| BindingKind::Global
| BindingKind::Nonlocal(_) => Resolution::RelevantLocal,
BindingKind::Import(Import {
qualified_name: module,
}) if module == "pandas" => Resolution::PandasModule,
_ => Resolution::IrrelevantBinding,
}
})
}
_ => Resolution::RelevantLocal,
}
}

View File

@@ -53,7 +53,7 @@ pub(crate) fn attr(checker: &mut Checker, attr: &str, value: &Expr, attr_expr: &
};
// Avoid flagging on function calls (e.g., `df.values()`).
if let Some(parent) = checker.semantic().current_expression_parent() {
if let Some(parent) = checker.semantic().expr_parent() {
if matches!(parent, Expr::Call(_)) {
return;
}

View File

@@ -1,9 +1,8 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Keyword, PySourceType, Ranged};
use ruff_python_semantic::BindingKind;
use ruff_python_semantic::Imported;
use ruff_python_ast::{self as ast, Keyword, Ranged};
use ruff_python_semantic::{BindingKind, Import};
use ruff_source_file::Locator;
use crate::autofix::edits::{remove_argument, Parentheses};
@@ -58,11 +57,12 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
.first()
.and_then(|module| checker.semantic().find_binding(module))
.is_some_and(|binding| {
if let BindingKind::Import(import) = &binding.kind {
matches!(import.call_path(), ["pandas"])
} else {
false
}
matches!(
binding.kind,
BindingKind::Import(Import {
qualified_name: "pandas"
})
)
})
{
return;
@@ -84,16 +84,18 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
// the star argument _doesn't_ contain an override).
// 2. The call is part of a larger expression (we're converting an expression to a
// statement, and expressions can't contain statements).
// 3. The call is in a lambda (we can't assign to a variable in a lambda). This
// should be unnecessary, as lambdas are expressions, and so (2) should apply,
// but we don't currently restore expression stacks when parsing deferred nodes,
// and so the parent is lost.
if !seen_star
&& checker.semantic().current_statement().is_expr_stmt()
&& checker.semantic().current_expression_parent().is_none()
&& checker.semantic().stmt().is_expr_stmt()
&& checker.semantic().expr_parent().is_none()
&& !checker.semantic().scope().kind.is_lambda()
{
if let Some(fix) = convert_inplace_argument_to_assignment(
call,
keyword,
checker.source_type,
checker.locator(),
) {
if let Some(fix) =
convert_inplace_argument_to_assignment(checker.locator(), call, keyword)
{
diagnostic.set_fix(fix);
}
}
@@ -111,10 +113,9 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
/// Remove the `inplace` argument from a function call and replace it with an
/// assignment.
fn convert_inplace_argument_to_assignment(
locator: &Locator,
call: &ast::ExprCall,
keyword: &Keyword,
source_type: PySourceType,
locator: &Locator,
) -> Option<Fix> {
// Add the assignment.
let attr = call.func.as_attribute_expr()?;
@@ -124,14 +125,8 @@ fn convert_inplace_argument_to_assignment(
);
// Remove the `inplace` argument.
let remove_argument = remove_argument(
keyword,
&call.arguments,
Parentheses::Preserve,
locator,
source_type,
)
.ok()?;
let remove_argument =
remove_argument(keyword, &call.arguments, Parentheses::Preserve, locator).ok()?;
Some(Fix::suggested_edits(insert_assignment, [remove_argument]))
}

View File

@@ -69,7 +69,7 @@ pub(crate) fn mixed_case_variable_in_class_scope(
return;
}
let parent = checker.semantic().current_statement();
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_class(arguments, checker.semantic())

View File

@@ -75,7 +75,7 @@ pub(crate) fn mixed_case_variable_in_global_scope(checker: &mut Checker, expr: &
return;
}
let parent = checker.semantic().current_statement();
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic()) {
return;
}

View File

@@ -65,7 +65,7 @@ pub(crate) fn non_lowercase_variable_in_function(checker: &mut Checker, expr: &E
return;
}
let parent = checker.semantic().current_statement();
let parent = checker.semantic().stmt();
if helpers::is_named_tuple_assignment(parent, checker.semantic())
|| helpers::is_typed_dict_assignment(parent, checker.semantic())
|| helpers::is_type_var_assignment(parent, checker.semantic())

View File

@@ -167,7 +167,7 @@ fn is_unused(expr: &Expr, model: &SemanticModel) -> bool {
//
// print(bar)
// ```
let scope = model.current_scope();
let scope = model.scope();
scope
.get_all(id)
.map(|binding_id| model.binding(binding_id))

View File

@@ -102,12 +102,12 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) {
range: iterable_range,
..
}) => {
let scope = checker.semantic().current_scope();
let scope = checker.semantic().scope();
if let Some(binding_id) = scope.get(id) {
let binding = checker.semantic().binding(binding_id);
if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() {
if let Some(parent_id) = binding.source {
let parent = checker.semantic().statement(parent_id);
let parent = checker.semantic().stmts[parent_id];
if let Stmt::Assign(ast::StmtAssign { value, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..

View File

@@ -116,10 +116,10 @@ pub(crate) fn lambda_assignment(
// rewriting it as a function declaration may break type-checking.
// See: https://github.com/astral-sh/ruff/issues/3046
// See: https://github.com/astral-sh/ruff/issues/5421
if (annotation.is_some() && checker.semantic().current_scope().kind.is_class())
if (annotation.is_some() && checker.semantic().scope().kind.is_class())
|| checker
.semantic()
.current_scope()
.scope()
.get_all(id)
.any(|binding_id| checker.semantic().binding(binding_id).kind.is_annotation())
{

View File

@@ -52,13 +52,15 @@ pub(crate) fn type_comparison(checker: &mut Checker, compare: &ast::ExprCompare)
}
// Left-hand side must be, e.g., `type(obj)`.
let Expr::Call(ast::ExprCall { func, .. }) = left else {
let Expr::Call(ast::ExprCall {
func, ..
}) = left else {
continue;
};
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
continue;
};
continue;
};
if !(id == "type" && checker.semantic().is_builtin("type")) {
continue;

View File

@@ -43,7 +43,9 @@ use crate::registry::{AsRule, Rule};
///
/// [D211]: https://beta.ruff.rs/docs/rules/blank-line-before-class
#[violation]
pub struct OneBlankLineBeforeClass;
pub struct OneBlankLineBeforeClass {
lines: usize,
}
impl AlwaysAutofixableViolation for OneBlankLineBeforeClass {
#[derive_message_formats]
@@ -95,7 +97,9 @@ impl AlwaysAutofixableViolation for OneBlankLineBeforeClass {
///
/// [PEP 257]: https://peps.python.org/pep-0257/
#[violation]
pub struct OneBlankLineAfterClass;
pub struct OneBlankLineAfterClass {
lines: usize,
}
impl AlwaysAutofixableViolation for OneBlankLineAfterClass {
#[derive_message_formats]
@@ -140,7 +144,9 @@ impl AlwaysAutofixableViolation for OneBlankLineAfterClass {
///
/// [D203]: https://beta.ruff.rs/docs/rules/one-blank-line-before-class
#[violation]
pub struct BlankLineBeforeClass;
pub struct BlankLineBeforeClass {
lines: usize,
}
impl AlwaysAutofixableViolation for BlankLineBeforeClass {
#[derive_message_formats]
@@ -164,24 +170,14 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
return;
};
// Special-case: the docstring is on the same line as the class. For example:
// ```python
// class PhotoMetadata: """Metadata about a photo."""
// ```
let between_range = TextRange::new(stmt.start(), docstring.start());
if !checker.locator().contains_line_break(between_range) {
return;
}
if checker.enabled(Rule::OneBlankLineBeforeClass) || checker.enabled(Rule::BlankLineBeforeClass)
{
let mut lines = UniversalNewlineIterator::with_offset(
checker.locator().slice(between_range),
between_range.start(),
)
.rev();
let before = checker
.locator()
.slice(TextRange::new(stmt.start(), docstring.start()));
let mut blank_lines_before = 0usize;
let mut lines = UniversalNewlineIterator::with_offset(before, stmt.start()).rev();
let mut blank_lines_start = lines.next().map(|line| line.start()).unwrap_or_default();
for line in lines {
@@ -195,7 +191,12 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
if checker.enabled(Rule::BlankLineBeforeClass) {
if blank_lines_before != 0 {
let mut diagnostic = Diagnostic::new(BlankLineBeforeClass, docstring.range());
let mut diagnostic = Diagnostic::new(
BlankLineBeforeClass {
lines: blank_lines_before,
},
docstring.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// Delete the blank line before the class.
diagnostic.set_fix(Fix::automatic(Edit::deletion(
@@ -208,7 +209,12 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
}
if checker.enabled(Rule::OneBlankLineBeforeClass) {
if blank_lines_before != 1 {
let mut diagnostic = Diagnostic::new(OneBlankLineBeforeClass, docstring.range());
let mut diagnostic = Diagnostic::new(
OneBlankLineBeforeClass {
lines: blank_lines_before,
},
docstring.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// Insert one blank line before the class.
diagnostic.set_fix(Fix::automatic(Edit::replacement(
@@ -223,9 +229,9 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
}
if checker.enabled(Rule::OneBlankLineAfterClass) {
let after_range = TextRange::new(docstring.end(), stmt.end());
let after = checker.locator().slice(after_range);
let after = checker
.locator()
.slice(TextRange::new(docstring.end(), stmt.end()));
let all_blank_after = after.universal_newlines().skip(1).all(|line| {
line.trim_whitespace().is_empty() || line.trim_whitespace_start().starts_with('#')
@@ -234,10 +240,9 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
return;
}
let mut lines = UniversalNewlineIterator::with_offset(after, after_range.start());
let first_line_start = lines.next().map(|l| l.start()).unwrap_or_default();
let mut blank_lines_after = 0usize;
let mut lines = UniversalNewlineIterator::with_offset(after, docstring.end());
let first_line_start = lines.next().map(|l| l.start()).unwrap_or_default();
let mut blank_lines_end = docstring.end();
for line in lines {
@@ -250,7 +255,12 @@ pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstr
}
if blank_lines_after != 1 {
let mut diagnostic = Diagnostic::new(OneBlankLineAfterClass, docstring.range());
let mut diagnostic = Diagnostic::new(
OneBlankLineAfterClass {
lines: blank_lines_after,
},
docstring.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// Insert a blank line before the class (replacing any existing lines).
diagnostic.set_fix(Fix::automatic(Edit::replacement(

View File

@@ -72,14 +72,11 @@ pub(crate) fn no_signature(checker: &mut Checker, docstring: &Docstring) {
return;
};
// Search for occurrences of the function name followed by an open parenthesis (e.g., `foo(` for
// a function named `foo`).
if first_line
.match_indices(name.as_str())
.any(|(index, _)| first_line[index + name.len()..].starts_with('('))
{
checker
.diagnostics
.push(Diagnostic::new(NoSignature, docstring.range()));
}
if !first_line.contains(&format!("{name}(")) {
return;
};
checker
.diagnostics
.push(Diagnostic::new(NoSignature, docstring.range()));
}

View File

@@ -523,7 +523,7 @@ pub(crate) fn not_missing(
definition: &Definition,
visibility: Visibility,
) -> bool {
if checker.source_type.is_stub() {
if checker.is_stub() {
return true;
}

View File

@@ -272,38 +272,4 @@ D.py:615:5: D400 [*] First line should end with a period
617 617 | """
618 618 |
D.py:639:17: D400 [*] First line should end with a period
|
639 | class SameLine: """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
640 |
641 | def same_line(): """This is a docstring on the same line"""
|
= help: Add period
Suggested fix
636 636 | """ This is a docstring that starts with a space.""" # noqa: D210
637 637 |
638 638 |
639 |-class SameLine: """This is a docstring on the same line"""
639 |+class SameLine: """This is a docstring on the same line."""
640 640 |
641 641 | def same_line(): """This is a docstring on the same line"""
D.py:641:18: D400 [*] First line should end with a period
|
639 | class SameLine: """This is a docstring on the same line"""
640 |
641 | def same_line(): """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D400
|
= help: Add period
Suggested fix
638 638 |
639 639 | class SameLine: """This is a docstring on the same line"""
640 640 |
641 |-def same_line(): """This is a docstring on the same line"""
641 |+def same_line(): """This is a docstring on the same line."""

View File

@@ -17,20 +17,4 @@ D.py:636:5: D404 First word of the docstring should not be "This"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D404
|
D.py:639:17: D404 First word of the docstring should not be "This"
|
639 | class SameLine: """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D404
640 |
641 | def same_line(): """This is a docstring on the same line"""
|
D.py:641:18: D404 First word of the docstring should not be "This"
|
639 | class SameLine: """This is a docstring on the same line"""
640 |
641 | def same_line(): """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D404
|

View File

@@ -254,38 +254,4 @@ D.py:615:5: D415 [*] First line should end with a period, question mark, or excl
617 617 | """
618 618 |
D.py:639:17: D415 [*] First line should end with a period, question mark, or exclamation point
|
639 | class SameLine: """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
640 |
641 | def same_line(): """This is a docstring on the same line"""
|
= help: Add closing punctuation
Suggested fix
636 636 | """ This is a docstring that starts with a space.""" # noqa: D210
637 637 |
638 638 |
639 |-class SameLine: """This is a docstring on the same line"""
639 |+class SameLine: """This is a docstring on the same line."""
640 640 |
641 641 | def same_line(): """This is a docstring on the same line"""
D.py:641:18: D415 [*] First line should end with a period, question mark, or exclamation point
|
639 | class SameLine: """This is a docstring on the same line"""
640 |
641 | def same_line(): """This is a docstring on the same line"""
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ D415
|
= help: Add closing punctuation
Suggested fix
638 638 |
639 639 | class SameLine: """This is a docstring on the same line"""
640 640 |
641 |-def same_line(): """This is a docstring on the same line"""
641 |+def same_line(): """This is a docstring on the same line."""

View File

@@ -12,14 +12,11 @@ mod tests {
use anyhow::Result;
use regex::Regex;
use ruff_python_parser::lexer::LexResult;
use test_case::test_case;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::AsMode;
use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::Locator;
@@ -507,9 +504,8 @@ mod tests {
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
fn flakes(contents: &str, expected: &[Rule]) {
let contents = dedent(contents);
let source_type = PySourceType::default();
let settings = Settings::for_rules(Linter::Pyflakes.rules());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents);
let locator = Locator::new(&contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
let indexer = Indexer::from_tokens(&tokens, &locator);
@@ -533,7 +529,6 @@ mod tests {
&settings,
flags::Noqa::Enabled,
None,
source_type,
);
diagnostics.sort_by_key(Diagnostic::start);
let actual = diagnostics

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