Compare commits

..

5 Commits

Author SHA1 Message Date
Charlie Marsh
0e268c91ab Update tests 2023-08-16 21:57:05 -04:00
Charlie Marsh
dbe62cc741 Merge branch 'main' into evanrittenhouse_5073 2023-08-16 21:54:49 -04:00
Charlie Marsh
e38e8c0a51 Use functools.reduce 2023-08-16 21:54:45 -04:00
Evan Rittenhouse
b6d786fb10 Implement reviewer comments 2023-08-11 12:11:40 -05:00
Evan Rittenhouse
a12a71a845 Initial implementation for RUF017 2023-08-10 23:22:12 -05:00
364 changed files with 22924 additions and 27786 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.0
uses: cloudflare/wrangler-action@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

6
Cargo.lock generated
View File

@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.285"
version = "0.0.284"
dependencies = [
"anyhow",
"clap",
@@ -2064,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.285"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2164,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.285"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.285
rev: v0.0.284
hooks:
- id: ruff
```

View File

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

View File

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

View File

@@ -69,7 +69,6 @@ g_action.set_enabled(True)
settings.set_enable_developer_extras(True)
foo.is_(True)
bar.is_not(False)
next(iter([]), False)
class Registry:
def __init__(self) -> None:

View File

@@ -22,10 +22,6 @@ tuple(
"o"]
)
)
set(set())
set(list())
set(tuple())
sorted(reversed())
# Nested sorts with differing keyword arguments. Not flagged.
sorted(sorted(x, key=lambda y: y))

View File

@@ -1,29 +1,22 @@
def not_checked():
import math
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tkinter # unconventional
def unconventional():
import altair
import matplotlib.pyplot
import numpy
import pandas
import seaborn
import tkinter
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tkinter as tkr # unconventional
def unconventional_aliases():
import altair as altr
import matplotlib.pyplot as plot
import numpy as nmp
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tkinter as tk
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tkinter as tk # conventional

View File

@@ -1,13 +0,0 @@
# PIE808
range(0, 10)
# OK
range(x, 10)
range(-15, 10)
range(10)
range(0)
range(0, 10, x)
range(0, 10, 1)
range(0, 10, step=1)
range(start=0, stop=10)
range(0, stop=10)

View File

@@ -64,8 +64,3 @@ def test_implicit_str_concat_no_parens(param1, param2, param3):
@pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
...
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
def test_csv_with_parens(param1, param2):
...

View File

@@ -1,4 +1,3 @@
from pickle import PicklingError, UnpicklingError
import socket
import pytest
@@ -21,12 +20,6 @@ def test_error_no_argument_given():
with pytest.raises(socket.error):
raise ValueError("Can't divide 1 by 0")
with pytest.raises(PicklingError):
raise PicklingError("Can't pickle")
with pytest.raises(UnpicklingError):
raise UnpicklingError("Can't unpickle")
def test_error_match_is_empty():
with pytest.raises(ValueError, match=None):

View File

@@ -16,38 +16,11 @@ def test_error_expr_simple(x):
...
@pytest.mark.parametrize(
"x",
[
(a, b),
# comment
(a, b),
(b, c),
],
)
@pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
def test_error_expr_complex(x):
...
@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
def test_error_parentheses(x):
...
@pytest.mark.parametrize(
"x",
[
a,
b,
(a),
c,
((a)),
],
)
def test_error_parentheses_trailing_comma(x):
...
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...

View File

@@ -19,20 +19,11 @@ raise TypeError ()
raise TypeError \
()
# RSE102
raise TypeError \
();
# RSE102
raise TypeError(
)
# RSE102
raise (TypeError) (
)
# RSE102
raise TypeError(
# Hello, world!
@@ -61,10 +52,3 @@ class Class:
# OK
raise Class.error()
import ctypes
# OK
raise ctypes.WinError(1)

View File

@@ -31,8 +31,6 @@ for key in list(obj.keys()):
key in (obj or {}).keys() # SIM118
(key) in (obj or {}).keys() # SIM118
from typing import KeysView

View File

@@ -27,8 +27,6 @@ def f(cls, x):
###
lambda x: print("Hello, world!")
lambda: print("Hello, world!")
class C:
###

View File

@@ -28,6 +28,3 @@ mdtypes_template = {
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
#: Okay
a = (1,

View File

@@ -25,12 +25,6 @@ if (True) == TrueElement or x == TrueElement:
if res == True != False:
pass
if(True) == TrueElement or x == TrueElement:
pass
if (yield i) == True:
print("even")
#: Okay
if x not in y:
pass

View File

@@ -133,8 +133,3 @@ def scope():
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
class TemperatureScales(Enum):
CELSIUS = (lambda deg_c: deg_c)
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)

View File

@@ -14,12 +14,7 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
"{0:.{prec}g}".format(1.23, prec=15) # OK
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars)
"{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements)
"{:*^30s}".format("centered")
## f-strings

View File

@@ -1,11 +1,10 @@
class Person: # [eq-without-hash]
class Person:
def __init__(self):
self.name = "monty"
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
self.name = "python"
@@ -15,9 +14,3 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None

View File

@@ -1,62 +0,0 @@
import abc
class Person:
def developer_greeting(self, name): # [no-self-use]
print(f"Greetings {name}!")
def greeting_1(self): # [no-self-use]
print("Hello!")
def greeting_2(self): # [no-self-use]
print("Hi!")
# OK
def developer_greeting():
print("Greetings developer!")
# OK
class Person:
name = "Paris"
def __init__(self):
pass
def __cmp__(self, other):
print(24)
def __repr__(self):
return "Person"
def func(self):
...
def greeting_1(self):
print(f"Hello from {self.name} !")
@staticmethod
def greeting_2():
print("Hi!")
class Base(abc.ABC):
"""abstract class"""
@abstractmethod
def abstract_method(self):
"""abstract method could not be a function"""
raise NotImplementedError
class Sub(Base):
@override
def abstract_method(self):
print("concret method")
class Prop:
@property
def count(self):
return 24

View File

@@ -32,7 +32,3 @@ foo not in {"a", "b", "c"} # Uses membership test already.
foo == "a" # Single comparison.
foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.

View File

@@ -1,3 +0,0 @@
from sys import *
exit(0)

View File

@@ -1,15 +1,15 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{Context, Result};
use anyhow::{bail, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_ast::{
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_python_parser::{lexer, AsMode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -89,49 +89,78 @@ pub(crate) fn remove_argument<T: Ranged>(
argument: &T,
arguments: &Arguments,
parentheses: Parentheses,
source: &str,
locator: &Locator,
source_type: PySourceType,
) -> Result<Edit> {
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());
// TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 {
let mut fix_start = None;
let mut fix_end = None;
if !after.is_empty() {
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut tokenizer = SimpleTokenizer::starts_at(argument.end(), source);
if arguments
.args
.iter()
.map(Expr::start)
.chain(arguments.keywords.iter().map(Keyword::start))
.any(|location| location > argument.start())
{
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if seen_comma {
if tok.is_non_logical_newline() {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if tok.is_newline() {
range.end()
} else {
range.start()
});
break;
}
if range.start() == argument.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && tok.is_comma() {
seen_comma = true;
}
}
} else {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if range.start() == argument.start() {
fix_end = Some(argument.end());
break;
}
if tok.is_comma() {
fix_start = Some(range.start());
}
}
}
// Find the trailing comma.
tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
// Find the next non-whitespace token.
let next = tokenizer
.find(|token| {
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
})
.context("Unable to find next token")?;
Ok(Edit::deletion(argument.start(), next.start()))
} else if let Some(previous) = before.iter().map(Ranged::end).max() {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
let mut tokenizer = SimpleTokenizer::starts_at(previous, source);
// Find the trailing comma.
let comma = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
Ok(Edit::deletion(comma.start(), argument.end()))
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
}
} else {
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
// Only one argument; remove it (but preserve parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {

View File

@@ -13,7 +13,6 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {

View File

@@ -1,36 +0,0 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@@ -1,5 +1,4 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_ast::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -30,7 +29,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),
},
binding.range(),
binding.range,
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {

View File

@@ -7,6 +7,10 @@ use crate::rules::flake8_simplify;
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
flake8_simplify::rules::key_in_dict_for(
checker,
&comprehension.target,
&comprehension.iter,
);
}
}

View File

@@ -1,8 +1,8 @@
use ruff_python_ast::Stmt;
use ruff_python_ast::{self as ast, Stmt};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, perflint, pyupgrade};
use crate::rules::{flake8_bugbear, perflint};
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
@@ -11,18 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
let Stmt::For(ast::StmtFor {
target, iter, body, ..
}) = checker.semantic.current_statement()
else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, stmt_for);
}
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt_for);
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
}
}

View File

@@ -1,6 +1,5 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::Ranged;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use crate::checkers::ast::Checker;
@@ -30,7 +29,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -83,7 +81,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range(),
binding.range,
));
}
}
@@ -113,21 +111,25 @@ 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| {
checker.semantic.different_branches(left, right)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
let line = checker.locator.compute_line_index(shadowed.range.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range(),
binding.range,
));
}
}
@@ -169,7 +171,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(node_id) = shadowed.source else {
let Some(statement_id) = shadowed.source else {
continue;
};
@@ -177,7 +179,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(node_id)
.statement(statement_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
@@ -205,20 +207,24 @@ 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| {
checker.semantic.different_branches(left, right)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.start());
let line = checker.locator.compute_line_index(shadowed.range.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
},
binding.range(),
binding.range,
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
@@ -303,12 +309,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -432,7 +432,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::deprecated_unittest_alias(checker, func);
}
if checker.enabled(Rule::SuperCallWithParameters) {
pyupgrade::rules::super_call_with_parameters(checker, call);
pyupgrade::rules::super_call_with_parameters(checker, expr, func, args);
}
if checker.enabled(Rule::UnnecessaryEncodeUTF8) {
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
@@ -531,9 +531,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
}
if checker.enabled(Rule::UnnecessaryRangeStart) {
flake8_pie::rules::unnecessary_range_start(checker, call);
}
if checker.enabled(Rule::ExecBuiltin) {
flake8_bandit::rules::exec_used(checker, func);
}
@@ -1178,7 +1175,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::magic_value_comparison(checker, left, comparators);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_compare(checker, compare);
flake8_simplify::rules::key_in_dict_compare(checker, expr, left, ops, comparators);
}
if checker.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators);

View File

@@ -338,6 +338,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
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 checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
@@ -464,17 +467,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
if checker.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(checker, class_def);
flake8_pyi::rules::pass_in_class_body(checker, stmt, body);
}
}
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, body);
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
}
if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::DuplicateClassFieldDefinition) {
flake8_pie::rules::duplicate_class_field_definition(checker, body);
flake8_pie::rules::duplicate_class_field_definition(checker, stmt, body);
}
if checker.enabled(Rule::NonUniqueEnums) {
flake8_pie::rules::non_unique_enums(checker, stmt, body);
@@ -1139,7 +1142,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1149,7 +1152,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
with_stmt,
with_,
checker.semantic.current_statement_parent(),
);
}
@@ -1168,21 +1171,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
perflint::rules::try_except_in_loop(checker, body);
}
}
Stmt::For(
for_stmt @ ast::StmtFor {
target,
body,
iter,
orelse,
is_async,
..
},
) => {
if checker.any_enabled(&[
Rule::UnusedLoopControlVariable,
Rule::IncorrectDictIterator,
Rule::YieldInForLoop,
]) {
Stmt::For(ast::StmtFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
checker.deferred.for_loops.push(checker.semantic.snapshot());
}
if checker.enabled(Rule::LoopVariableOverridesIterator) {
@@ -1203,6 +1200,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
}
if stmt.is_for_stmt() {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, target, iter);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, target, body);
}
@@ -1212,17 +1220,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, for_stmt);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
}
Stmt::Try(ast::StmtTry {
body,

View File

@@ -32,8 +32,8 @@ use itertools::Itertools;
use log::error;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged,
Stmt, Suite, UnaryOp,
ExprContext, Keyword, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt,
Suite, UnaryOp,
};
use ruff_text_size::{TextRange, TextSize};
@@ -193,22 +193,18 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
/// Returns the [`IsolationLevel`] for fixes in the current context.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.statement_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
@@ -267,7 +263,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
self.semantic.push_statement(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -623,28 +619,16 @@ where
}
}
// Iterate over the `body`, then the `handlers`, then the `orelse`, then the
// `finalbody`, but treat the body and the `orelse` as a single branch for
// flow analysis purposes.
let branch = self.semantic.push_branch();
self.semantic.handled_exceptions.push(handled_exceptions);
self.visit_body(body);
self.semantic.handled_exceptions.pop();
self.semantic.pop_branch();
for except_handler in handlers {
self.semantic.push_branch();
self.visit_except_handler(except_handler);
self.semantic.pop_branch();
}
self.semantic.set_branch(branch);
self.visit_body(orelse);
self.semantic.pop_branch();
self.semantic.push_branch();
self.visit_body(finalbody);
self.semantic.pop_branch();
}
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
@@ -724,7 +708,6 @@ where
) => {
self.visit_boolean_test(test);
self.semantic.push_branch();
if typing::is_type_checking_block(stmt_if, &self.semantic) {
if self.semantic.at_top_level() {
self.importer.visit_type_checking_block(stmt);
@@ -733,12 +716,9 @@ where
} else {
self.visit_body(body);
}
self.semantic.pop_branch();
for clause in elif_else_clauses {
self.semantic.push_branch();
self.visit_elif_else_clause(clause);
self.semantic.pop_branch();
}
}
_ => visitor::walk_stmt(self, stmt),
@@ -779,7 +759,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_statement();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -815,7 +795,7 @@ where
return;
}
self.semantic.push_node(expr);
self.semantic.push_expression(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -894,20 +874,18 @@ where
},
) => {
// Visit the default arguments, but avoid the body, which will be deferred.
if let Some(parameters) = parameters {
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
}
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
}
}
@@ -1235,7 +1213,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
self.semantic.pop_expression();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1373,17 +1351,6 @@ where
}
}
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
self.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
}
self.semantic.push_branch();
self.visit_body(&match_case.body);
self.semantic.pop_branch();
}
fn visit_type_param(&mut self, type_param: &'b ast::TypeParam) {
// Step 1: Binding
match type_param {
@@ -1867,9 +1834,7 @@ impl<'a> Checker<'a> {
range: _,
}) = expr
{
if let Some(parameters) = parameters {
self.visit_parameters(parameters);
}
self.visit_parameters(parameters);
self.visit_expr(body);
} else {
unreachable!("Expected Expr::Lambda");
@@ -1890,7 +1855,7 @@ impl<'a> Checker<'a> {
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range())))
Some(names.iter().map(|name| (*name, binding.range)))
}
_ => None,
})

View File

@@ -1,10 +1,10 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_ast::Ranged;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_codegen::Stylist;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{

View File

@@ -216,7 +216,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
@@ -708,7 +707,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pie, "800") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessarySpread),
(Flake8Pie, "804") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryDictKwargs),
(Flake8Pie, "807") => (RuleGroup::Unspecified, rules::flake8_pie::rules::ReimplementedListBuiltin),
(Flake8Pie, "808") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryRangeStart),
(Flake8Pie, "810") => (RuleGroup::Unspecified, rules::flake8_pie::rules::MultipleStartsEndsWith),
// flake8-commas

View File

@@ -405,7 +405,7 @@ y = 2
z = x + 1";
assert_eq!(
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22))])
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22)),])
);
let contents = "x = 1

View File

@@ -2,8 +2,9 @@ use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::{TextRange, TextSize};
use ruff_python_semantic::Definition;
use ruff_text_size::TextRange;
pub(crate) mod extraction;
pub(crate) mod google;
@@ -27,34 +28,43 @@ impl<'a> Docstring<'a> {
DocstringBody { docstring: self }
}
pub(crate) fn start(&self) -> TextSize {
self.expr.start()
}
pub(crate) fn end(&self) -> TextSize {
self.expr.end()
}
pub(crate) fn range(&self) -> TextRange {
self.expr.range()
}
pub(crate) fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
}
impl Ranged for Docstring<'_> {
fn range(&self) -> TextRange {
self.expr.range()
}
}
#[derive(Copy, Clone)]
pub(crate) struct DocstringBody<'a> {
docstring: &'a Docstring<'a>,
}
impl<'a> DocstringBody<'a> {
#[inline]
pub(crate) fn start(self) -> TextSize {
self.range().start()
}
pub(crate) fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
pub(crate) fn as_str(self) -> &'a str {
&self.docstring.contents[self.docstring.body_range]
}
}
impl Ranged for DocstringBody<'_> {
fn range(&self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
}
impl Deref for DocstringBody<'_> {
type Target = str;

View File

@@ -2,7 +2,6 @@ use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
@@ -367,12 +366,6 @@ impl<'a> SectionContext<'a> {
}
}
impl Ranged for SectionContext<'_> {
fn range(&self) -> TextRange {
self.range()
}
}
impl Debug for SectionContext<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SectionContext")

View File

@@ -194,7 +194,7 @@ impl<'a> Importer<'a> {
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the start of the file, since it could
// be shadowed between the import and the current location.
if imported_name.start() > at {
if imported_name.range().start() > at {
return Some(Err(ResolutionError::ImportAfterUsage));
}
@@ -301,14 +301,12 @@ impl<'a> Importer<'a> {
}
if let Stmt::ImportFrom(ast::StmtImportFrom {
module: name,
names,
level,
range: _,
..
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{
import_from = Some(*stmt);
}

View File

@@ -33,7 +33,7 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
err
)
})?;
let code = notebook.source_code().to_string();
let code = notebook.content().to_string();
notebook.update_cell_content(&code);
let mut writer = Vec::new();
notebook.write_inner(&mut writer)?;
@@ -103,7 +103,7 @@ pub struct Notebook {
/// separated by a newline and a trailing newline. The trailing newline
/// is added to make sure that each cell ends with a newline which will
/// be removed when updating the cell content.
source_code: String,
content: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<JupyterIndex>,
@@ -132,8 +132,8 @@ impl Notebook {
}
/// Read the Jupyter Notebook from its JSON string.
pub fn from_source_code(source_code: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(source_code))
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(contents))
}
/// Read a Jupyter Notebook from a [`Read`] implementor.
@@ -268,7 +268,7 @@ impl Notebook {
// The additional newline at the end is to maintain consistency for
// all cells. These newlines will be removed before updating the
// source code with the transformed content. Refer `update_cell_content`.
source_code: contents.join("\n") + "\n",
content: contents.join("\n") + "\n",
cell_offsets,
valid_code_cells,
trailing_newline,
@@ -404,8 +404,8 @@ impl Notebook {
/// Return the notebook content.
///
/// This is the concatenation of all Python code cells.
pub fn source_code(&self) -> &str {
&self.source_code
pub(crate) fn content(&self) -> &str {
&self.content
}
/// Return the Jupyter notebook index.
@@ -424,13 +424,12 @@ impl Notebook {
}
/// Update the notebook with the given sourcemap and transformed content.
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: String) {
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: &str) {
// Cell offsets must be updated before updating the cell content as
// it depends on the offsets to extract the cell content.
self.index.take();
self.update_cell_offsets(source_map);
self.update_cell_content(&transformed);
self.source_code = transformed;
self.update_cell_content(transformed);
self.content = transformed.to_string();
}
/// Return a slice of [`Cell`] in the Jupyter notebook.
@@ -477,16 +476,14 @@ mod tests {
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::test::{
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
};
use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path};
use crate::{assert_messages, settings};
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
let path = test_resource_path("fixtures/jupyter/cell").join(path);
let source_code = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&source_code)?)
let contents = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
}
#[test]
@@ -539,7 +536,7 @@ mod tests {
fn test_concat_notebook() -> Result<()> {
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
assert_eq!(
notebook.source_code,
notebook.content,
r#"def unused_variable():
x = 1
y = 2
@@ -581,64 +578,49 @@ print("after empty cells")
#[test]
fn test_import_sorting() -> Result<()> {
let path = "isort.ipynb".to_string();
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("isort_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnsortedImports),
)?;
assert_messages!(messages, path, source_notebook);
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("ipy_escape_command_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(messages, path, source_notebook);
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_unused_variable() -> Result<()> {
let path = "unused_variable.ipynb".to_string();
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("unused_variable_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedVariable),
)?;
assert_messages!(messages, path, source_notebook);
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let path = "before_fix.ipynb".to_string();
let TestedNotebook {
linted_notebook: fixed_notebook,
..
} = test_notebook_path(
let (_, _, source_kind) = test_notebook_path(
path,
Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
let mut writer = Vec::new();
fixed_notebook.write_inner(&mut writer)?;
source_kind.expect_jupyter().write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
let expected =
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;

View File

@@ -63,7 +63,7 @@ pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult<(Vec<Message>, Option<ImportMap>)>,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, SourceKind>,
pub transformed: Cow<'a, str>,
/// The number of fixes applied for each [`Rule`].
pub fixed: FixTable,
}
@@ -335,19 +335,19 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source
/// code.
pub fn lint_only(
contents: &str,
path: &Path,
package: Option<&Path>,
settings: &Settings,
noqa: flags::Noqa,
source_kind: &SourceKind,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
let locator = Locator::new(contents);
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -374,7 +374,7 @@ pub fn lint_only(
&directives,
settings,
noqa,
Some(source_kind),
source_kind,
source_type,
);
@@ -416,14 +416,15 @@ fn diagnostics_to_messages(
/// Generate `Diagnostic`s from source code content, iteratively autofixing
/// until stable.
pub fn lint_fix<'a>(
contents: &'a str,
path: &Path,
package: Option<&Path>,
noqa: flags::Noqa,
settings: &Settings,
source_kind: &'a SourceKind,
source_kind: &mut SourceKind,
source_type: PySourceType,
) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(source_kind);
let mut transformed = Cow::Borrowed(contents);
// Track the number of fixed errors across iterations.
let mut fixed = FxHashMap::default();
@@ -438,10 +439,10 @@ pub fn lint_fix<'a>(
loop {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(transformed.source_code());
let locator = Locator::new(&transformed);
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -481,7 +482,7 @@ pub fn lint_fix<'a>(
if parseable && result.error.is_some() {
report_autofix_syntax_error(
path,
transformed.source_code(),
&transformed,
&result.error.unwrap(),
fixed.keys().copied(),
);
@@ -502,7 +503,12 @@ pub fn lint_fix<'a>(
*fixed.entry(rule).or_default() += count;
}
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
if let SourceKind::Jupyter(notebook) = source_kind {
notebook.update(&source_map, &fixed_contents);
}
// Store the fixed contents.
transformed = Cow::Owned(fixed_contents);
// Increment the iteration count.
iterations += 1;
@@ -511,7 +517,7 @@ pub fn lint_fix<'a>(
continue;
}
report_failed_to_converge_error(path, transformed.source_code(), &result.data.0);
report_failed_to_converge_error(path, &transformed, &result.data.0);
}
return Ok(FixerResult {

View File

@@ -18,7 +18,7 @@ impl Emitter for AzureEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let location = if context.is_notebook(message.filename()) {
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -20,7 +20,7 @@ impl Emitter for GithubEmitter {
) -> anyhow::Result<()> {
for message in messages {
let source_location = message.compute_start_location();
let location = if context.is_notebook(message.filename()) {
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -63,7 +63,7 @@ impl Serialize for SerializedMessages<'_> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let lines = if self.context.is_notebook(message.filename()) {
let lines = if self.context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({

View File

@@ -13,6 +13,7 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::source_kind::SourceKind;
#[derive(Default)]
pub struct GroupedEmitter {
@@ -65,7 +66,10 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
jupyter_index: context.notebook(message.filename()).map(Notebook::index),
jupyter_index: context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index),
message,
show_fix_status: self.show_fix_status,
show_source: self.show_source,

View File

@@ -45,7 +45,7 @@ impl Emitter for JunitEmitter {
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_notebook(message.filename()) {
let location = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -3,8 +3,10 @@ use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use crate::source_kind::SourceKind;
pub use azure::AzureEmitter;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
@@ -15,11 +17,8 @@ pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
pub use text::TextEmitter;
use crate::jupyter::Notebook;
mod azure;
mod diff;
mod github;
@@ -130,31 +129,33 @@ pub trait Emitter {
/// Context passed to [`Emitter`].
pub struct EmitterContext<'a> {
notebooks: &'a FxHashMap<String, Notebook>,
source_kind: &'a FxHashMap<String, SourceKind>,
}
impl<'a> EmitterContext<'a> {
pub fn new(notebooks: &'a FxHashMap<String, Notebook>) -> Self {
Self { notebooks }
pub fn new(source_kind: &'a FxHashMap<String, SourceKind>) -> Self {
Self { source_kind }
}
/// Tests if the file with `name` is a jupyter notebook.
pub fn is_notebook(&self, name: &str) -> bool {
self.notebooks.contains_key(name)
pub fn is_jupyter_notebook(&self, name: &str) -> bool {
self.source_kind
.get(name)
.is_some_and(SourceKind::is_jupyter)
}
pub fn notebook(&self, name: &str) -> Option<&Notebook> {
self.notebooks.get(name)
pub fn source_kind(&self, name: &str) -> Option<&SourceKind> {
self.source_kind.get(name)
}
}
#[cfg(test)]
mod tests {
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};

View File

@@ -19,7 +19,7 @@ impl Emitter for PylintEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let row = if context.is_notebook(message.filename()) {
let row = if context.is_jupyter_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
OneIndexed::from_zero_indexed(0)

View File

@@ -6,9 +6,9 @@ use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use ruff_text_size::{TextRange, TextSize};
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{JupyterIndex, Notebook};
@@ -16,6 +16,7 @@ use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::source_kind::SourceKind;
bitflags! {
#[derive(Default)]
@@ -71,7 +72,10 @@ impl Emitter for TextEmitter {
)?;
let start_location = message.compute_start_location();
let jupyter_index = context.notebook(message.filename()).map(Notebook::index);
let jupyter_index = context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index);
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {

View File

@@ -4,7 +4,6 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_ast::Ranged;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
pub(crate) struct Renamer;
@@ -221,12 +220,12 @@ impl Renamer {
BindingKind::Import(_) | BindingKind::FromImport(_) => {
if binding.is_alias() {
// Ex) Rename `import pandas as alias` to `import pandas as pd`.
Some(Edit::range_replacement(target.to_string(), binding.range()))
Some(Edit::range_replacement(target.to_string(), binding.range))
} else {
// Ex) Rename `import pandas` to `import pandas as pd`.
Some(Edit::range_replacement(
format!("{name} as {target}"),
binding.range(),
binding.range,
))
}
}
@@ -235,7 +234,7 @@ impl Renamer {
let module_name = import.call_path.first().unwrap();
Some(Edit::range_replacement(
format!("{module_name} as {target}"),
binding.range(),
binding.range,
))
}
// Avoid renaming builtins and other "special" bindings.
@@ -255,7 +254,7 @@ impl Renamer {
| BindingKind::FunctionDefinition(_)
| BindingKind::Deletion
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range()))
Some(Edit::range_replacement(target.to_string(), binding.range))
}
}
}

View File

@@ -4,8 +4,7 @@ use ruff_python_ast::{self as ast, Constant, Expr};
pub(super) fn is_allowed_func_call(name: &str) -> bool {
matches!(
name,
"__setattr__"
| "append"
"append"
| "assertEqual"
| "assertEquals"
| "assertNotEqual"
@@ -27,13 +26,13 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
| "int"
| "is_"
| "is_not"
| "next"
| "param"
| "pop"
| "remove"
| "set_blocking"
| "set_enabled"
| "setattr"
| "__setattr__"
| "setdefault"
| "str"
)

View File

@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:87:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
|
86 | # FBT001: Boolean positional arg in function definition
87 | def foo(self, value: bool) -> None:
85 | # FBT001: Boolean positional arg in function definition
86 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
88 | pass
87 | pass
|

View File

@@ -90,7 +90,8 @@ impl AlwaysAutofixableViolation for DuplicateHandlerException {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateHandlerException { names } = self;
if let [name] = names.as_slice() {
if names.len() == 1 {
let name = &names[0];
format!("Exception handler with duplicate exception: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");

View File

@@ -184,10 +184,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
return false;
}
if parameters
.as_ref()
.is_some_and(|parameters| parameters.includes(&loaded.id))
{
if parameters.includes(&loaded.id) {
return false;
}

View File

@@ -76,20 +76,17 @@ where
range: _,
}) => {
visitor::walk_expr(self, body);
if let Some(parameters) = parameters {
for ParameterWithDefault {
parameter,
default: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
self.names.remove(parameter.name.as_str());
}
for ParameterWithDefault {
parameter,
default: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
self.names.remove(parameter.name.as_str());
}
}
_ => visitor::walk_expr(self, expr),

View File

@@ -1,9 +1,9 @@
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_ast::{helpers, visitor};
use crate::checkers::ast::Checker;
@@ -105,16 +105,16 @@ where
}
/// B007
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
let control_names = {
let mut finder = NameFinder::new();
finder.visit_expr(stmt_for.target.as_ref());
finder.visit_expr(target);
finder.names
};
let used_names = {
let mut finder = NameFinder::new();
for stmt in &stmt_for.body {
for stmt in body {
finder.visit_stmt(stmt);
}
finder.names
@@ -132,10 +132,9 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast
}
// Avoid fixing any variables that _may_ be used, but undetectably so.
let certainty =
Certainty::from(!helpers::uses_magic_variable_access(&stmt_for.body, |id| {
checker.semantic().is_builtin(id)
}));
let certainty = Certainty::from(!helpers::uses_magic_variable_access(body, |id| {
checker.semantic().is_builtin(id)
}));
// Attempt to rename the variable by prepending an underscore, but avoid
// applying the fix if doing so wouldn't actually cause us to ignore the
@@ -164,7 +163,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.filter(|binding| binding.start() >= expr.start())
.filter(|binding| binding.range.start() >= expr.range().start())
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(

View File

@@ -765,14 +765,13 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
outer_call.args = match outer_call.args.split_first() {
Some((first, rest)) => {
let inner_call = match_call(&first.value)?;
inner_call
.args
.iter()
.filter(|argument| argument.keyword.is_none())
.take(1)
.chain(rest.iter())
.cloned()
.collect::<Vec<_>>()
if let Some(iterable) = inner_call.args.first() {
let mut args = vec![iterable.clone()];
args.extend_from_slice(rest);
args
} else {
bail!("Expected at least one argument in inner function call");
}
}
None => bail!("Expected at least one argument in outer function call"),
};
@@ -1045,26 +1044,8 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let (whitespace_after, whitespace_before, elt, for_in, lpar, rpar) = match &call.args[0].value {
Expression::ListComp(list_comp) => (
&list_comp.lbracket.whitespace_after,
&list_comp.rbracket.whitespace_before,
&list_comp.elt,
&list_comp.for_in,
&list_comp.lpar,
&list_comp.rpar,
),
Expression::SetComp(set_comp) => (
&set_comp.lbrace.whitespace_after,
&set_comp.rbrace.whitespace_before,
&set_comp.elt,
&set_comp.for_in,
&set_comp.lpar,
&set_comp.rpar,
),
_ => {
bail!("Expected Expression::ListComp | Expression::SetComp");
}
let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!("Expected Expression::ListComp");
};
let mut new_empty_lines = vec![];
@@ -1073,7 +1054,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
first_line,
empty_lines,
..
}) = &whitespace_after
}) = &list_comp.lbracket.whitespace_after
{
// If there's a comment on the line after the opening bracket, we need
// to preserve it. The way we do this is by adding a new empty line
@@ -1162,7 +1143,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
..
},
..
}) = &whitespace_before
}) = &list_comp.rbracket.whitespace_before
{
Some(format!("{}{}", whitespace.0, comment.0))
} else {
@@ -1170,10 +1151,10 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
};
call.args[0].value = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: elt.clone(),
for_in: for_in.clone(),
lpar: lpar.clone(),
rpar: rpar.clone(),
elt: list_comp.elt.clone(),
for_in: list_comp.for_in.clone(),
lpar: list_comp.lpar.clone(),
rpar: list_comp.rpar.clone(),
}));
let whitespace_after_arg = match &call.args[0].comma {

View File

@@ -69,31 +69,30 @@ pub(crate) fn unnecessary_comprehension_any_all(
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if !matches!(id.as_str(), "all" | "any") {
return;
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 {
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0]
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);
}
let [arg] = args else {
return;
};
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = arg
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);
}
/// Return `true` if the [`Expr`] contains an `await` expression.

View File

@@ -103,10 +103,7 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if late_binding(parameters, body) {
return;
}
}
@@ -137,10 +134,7 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if late_binding(parameters, body) {
return;
}
}
@@ -177,10 +171,7 @@ pub(crate) fn unnecessary_map(
return;
}
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if late_binding(parameters, body) {
return;
}
}
@@ -249,7 +240,7 @@ struct LateBindingVisitor<'a> {
/// The arguments to the current lambda.
parameters: &'a Parameters,
/// The arguments to any lambdas within the current lambda body.
lambdas: Vec<Option<&'a Parameters>>,
lambdas: Vec<&'a Parameters>,
/// Whether any names within the current lambda body are late-bound within nested lambdas.
late_bound: bool,
}
@@ -270,7 +261,7 @@ impl<'a> Visitor<'a> for LateBindingVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match expr {
Expr::Lambda(ast::ExprLambda { parameters, .. }) => {
self.lambdas.push(parameters.as_deref());
self.lambdas.push(parameters);
visitor::walk_expr(self, expr);
self.lambdas.pop();
}
@@ -284,11 +275,11 @@ impl<'a> Visitor<'a> for LateBindingVisitor<'a> {
// If the name is defined in the current lambda...
if self.parameters.includes(id) {
// And isn't overridden by any nested lambdas...
if !self.lambdas.iter().any(|parameters| {
parameters
.as_ref()
.is_some_and(|parameters| parameters.includes(id))
}) {
if !self
.lambdas
.iter()
.any(|parameters| parameters.includes(id))
{
// Then it's late-bound.
self.late_bound = true;
}

View File

@@ -365,8 +365,8 @@ C414.py:19:1: C414 [*] Unnecessary `list` call within `tuple()`
23 | | )
24 | | )
| |_^ C414
25 | set(set())
26 | set(list())
25 |
26 | # Nested sorts with differing keyword arguments. Not flagged.
|
= help: Remove the inner `list` call
@@ -380,91 +380,8 @@ C414.py:19:1: C414 [*] Unnecessary `list` call within `tuple()`
22 21 | "o"]
23 22 | )
24 |-)
25 23 | set(set())
26 24 | set(list())
27 25 | set(tuple())
C414.py:25:1: C414 [*] Unnecessary `set` call within `set()`
|
23 | )
24 | )
25 | set(set())
| ^^^^^^^^^^ C414
26 | set(list())
27 | set(tuple())
|
= help: Remove the inner `set` call
Suggested fix
22 22 | "o"]
23 23 | )
24 24 | )
25 |-set(set())
25 |+set()
26 26 | set(list())
27 27 | set(tuple())
28 28 | sorted(reversed())
C414.py:26:1: C414 [*] Unnecessary `list` call within `set()`
|
24 | )
25 | set(set())
26 | set(list())
| ^^^^^^^^^^^ C414
27 | set(tuple())
28 | sorted(reversed())
|
= help: Remove the inner `list` call
Suggested fix
23 23 | )
24 24 | )
25 25 | set(set())
26 |-set(list())
26 |+set()
27 27 | set(tuple())
28 28 | sorted(reversed())
29 29 |
C414.py:27:1: C414 [*] Unnecessary `tuple` call within `set()`
|
25 | set(set())
26 | set(list())
27 | set(tuple())
| ^^^^^^^^^^^^ C414
28 | sorted(reversed())
|
= help: Remove the inner `tuple` call
Suggested fix
24 24 | )
25 25 | set(set())
26 26 | set(list())
27 |-set(tuple())
27 |+set()
28 28 | sorted(reversed())
29 29 |
30 30 | # Nested sorts with differing keyword arguments. Not flagged.
C414.py:28:1: C414 [*] Unnecessary `reversed` call within `sorted()`
|
26 | set(list())
27 | set(tuple())
28 | sorted(reversed())
| ^^^^^^^^^^^^^^^^^^ C414
29 |
30 | # Nested sorts with differing keyword arguments. Not flagged.
|
= help: Remove the inner `reversed` call
Suggested fix
25 25 | set(set())
26 26 | set(list())
27 27 | set(tuple())
28 |-sorted(reversed())
28 |+sorted()
29 29 |
30 30 | # Nested sorts with differing keyword arguments. Not flagged.
31 31 | sorted(sorted(x, key=lambda y: y))
25 23 |
26 24 | # Nested sorts with differing keyword arguments. Not flagged.
27 25 | sorted(sorted(x, key=lambda y: y))

View File

@@ -77,7 +77,7 @@ C419.py:7:5: C419 [*] Unnecessary list comprehension.
9 9 | any({x.id for x in bar})
10 10 |
C419.py:9:5: C419 [*] Unnecessary list comprehension.
C419.py:9:5: C419 Unnecessary list comprehension.
|
7 | [x.id for x in bar], # second comment
8 | ) # third comment
@@ -88,16 +88,6 @@ C419.py:9:5: C419 [*] Unnecessary list comprehension.
|
= help: Remove unnecessary list comprehension
Suggested fix
6 6 | all( # first comment
7 7 | [x.id for x in bar], # second comment
8 8 | ) # third comment
9 |-any({x.id for x in bar})
9 |+any(x.id for x in bar)
10 10 |
11 11 | # OK
12 12 | all(x.id for x in bar)
C419.py:24:5: C419 [*] Unnecessary list comprehension.
|
22 | # Special comment handling

View File

@@ -6,43 +6,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usage of `datetime.date.fromtimestamp()`.
///
/// ## 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.date.fromtimestamp(ts)` returns a naive datetime object.
/// Instead, use `datetime.datetime.fromtimestamp(ts, tz=)` to return a
/// timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.date.fromtimestamp(946684800)
/// ```
///
/// 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 CallDateFromtimestamp;
@@ -56,6 +19,12 @@ impl Violation for CallDateFromtimestamp {
}
}
/// Checks for `datetime.date.fromtimestamp()`. (DTZ012)
///
/// ## Why is this bad?
///
/// It uses the system local timezone.
/// Use `datetime.datetime.fromtimestamp(, tz=).date()` instead.
pub(crate) fn call_date_fromtimestamp(checker: &mut Checker, func: &Expr, location: TextRange) {
if checker
.semantic()

View File

@@ -6,42 +6,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usage of `datetime.date.today()`.
///
/// ## 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.date.today` returns a naive datetime object. Instead, use
/// `datetime.datetime.now(tz=).date()` to return a timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.today()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.timezone.utc).date()
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.UTC).date()
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDateToday;
@@ -55,6 +19,12 @@ impl Violation for CallDateToday {
}
}
/// Checks for `datetime.date.today()`. (DTZ011)
///
/// ## Why is this bad?
///
/// It uses the system local timezone.
/// Use `datetime.datetime.now(tz=).date()` instead.
pub(crate) fn call_date_today(checker: &mut Checker, func: &Expr, location: TextRange) {
if checker
.semantic()

View File

@@ -2,7 +2,6 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Ranged;
use ruff_python_semantic::{Binding, Imported};
use crate::checkers::ast::Checker;
@@ -77,18 +76,16 @@ pub(crate) fn unconventional_import_alias(
name: qualified_name,
asname: expected_alias.to_string(),
},
binding.range(),
binding.range,
);
if checker.patch(diagnostic.kind.rule()) {
if !import.is_submodule_import() {
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
Some(diagnostic)

View File

@@ -1,238 +1,132 @@
---
source: crates/ruff/src/rules/flake8_import_conventions/mod.rs
---
defaults.py:6:12: ICN001 [*] `altair` should be imported as `alt`
defaults.py:3:8: ICN001 `altair` should be imported as `alt`
|
5 | def unconventional():
6 | import altair
| ^^^^^^ ICN001
7 | import matplotlib.pyplot
8 | import numpy
1 | import math # not checked
2 |
3 | import altair # unconventional
| ^^^^^^ ICN001
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
|
= help: Alias `altair` to `alt`
Suggested fix
3 3 |
4 4 |
5 5 | def unconventional():
6 |- import altair
6 |+ import altair as alt
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 9 | import pandas
defaults.py:7:12: ICN001 `matplotlib.pyplot` should be imported as `plt`
defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
5 | def unconventional():
6 | import altair
7 | import matplotlib.pyplot
| ^^^^^^^^^^^^^^^^^ ICN001
8 | import numpy
9 | import pandas
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^^^^^^^^ ICN001
5 | import numpy # unconventional
6 | import pandas # unconventional
|
= help: Alias `matplotlib.pyplot` to `plt`
defaults.py:8:12: ICN001 [*] `numpy` should be imported as `np`
|
6 | import altair
7 | import matplotlib.pyplot
8 | import numpy
| ^^^^^ ICN001
9 | import pandas
10 | import seaborn
|
= help: Alias `numpy` to `np`
defaults.py:5:8: ICN001 `numpy` should be imported as `np`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
| ^^^^^ ICN001
6 | import pandas # unconventional
7 | import seaborn # unconventional
|
= help: Alias `numpy` to `np`
Suggested fix
5 5 | def unconventional():
6 6 | import altair
7 7 | import matplotlib.pyplot
8 |- import numpy
8 |+ import numpy as np
9 9 | import pandas
10 10 | import seaborn
11 11 | import tkinter
defaults.py:6:8: ICN001 `pandas` should be imported as `pd`
|
4 | import matplotlib.pyplot # unconventional
5 | import numpy # unconventional
6 | import pandas # unconventional
| ^^^^^^ ICN001
7 | import seaborn # unconventional
8 | import tkinter # unconventional
|
= help: Alias `pandas` to `pd`
defaults.py:9:12: ICN001 [*] `pandas` should be imported as `pd`
|
7 | import matplotlib.pyplot
8 | import numpy
9 | import pandas
| ^^^^^^ ICN001
10 | import seaborn
11 | import tkinter
|
= help: Alias `pandas` to `pd`
defaults.py:7:8: ICN001 `seaborn` should be imported as `sns`
|
5 | import numpy # unconventional
6 | import pandas # unconventional
7 | import seaborn # unconventional
| ^^^^^^^ ICN001
8 | import tkinter # unconventional
|
= help: Alias `seaborn` to `sns`
Suggested fix
6 6 | import altair
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 |- import pandas
9 |+ import pandas as pd
10 10 | import seaborn
11 11 | import tkinter
12 12 |
defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
defaults.py:8:8: ICN001 `tkinter` should be imported as `tk`
|
8 | import numpy
9 | import pandas
10 | import seaborn
| ^^^^^^^ ICN001
11 | import tkinter
|
= help: Alias `seaborn` to `sns`
Suggested fix
7 7 | import matplotlib.pyplot
8 8 | import numpy
9 9 | import pandas
10 |- import seaborn
10 |+ import seaborn as sns
11 11 | import tkinter
12 12 |
13 13 |
defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
|
9 | import pandas
10 | import seaborn
11 | import tkinter
| ^^^^^^^ ICN001
6 | import pandas # unconventional
7 | import seaborn # unconventional
8 | import tkinter # unconventional
| ^^^^^^^ ICN001
9 |
10 | import altair as altr # unconventional
|
= help: Alias `tkinter` to `tk`
Suggested fix
8 8 | import numpy
9 9 | import pandas
10 10 | import seaborn
11 |- import tkinter
11 |+ import tkinter as tk
12 12 |
13 13 |
14 14 | def unconventional_aliases():
defaults.py:15:22: ICN001 [*] `altair` should be imported as `alt`
defaults.py:10:18: ICN001 `altair` should be imported as `alt`
|
14 | def unconventional_aliases():
15 | import altair as altr
| ^^^^ ICN001
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
8 | import tkinter # unconventional
9 |
10 | import altair as altr # unconventional
| ^^^^ ICN001
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
|
= help: Alias `altair` to `alt`
Suggested fix
12 12 |
13 13 |
14 14 | def unconventional_aliases():
15 |- import altair as altr
15 |+ import altair as alt
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
defaults.py:16:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
defaults.py:11:29: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
14 | def unconventional_aliases():
15 | import altair as altr
16 | import matplotlib.pyplot as plot
| ^^^^ ICN001
17 | import numpy as nmp
18 | import pandas as pdas
10 | import altair as altr # unconventional
11 | import matplotlib.pyplot as plot # unconventional
| ^^^^ ICN001
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
|
= help: Alias `matplotlib.pyplot` to `plt`
Suggested fix
13 13 |
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 |- import matplotlib.pyplot as plot
16 |+ import matplotlib.pyplot as plt
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
defaults.py:17:21: ICN001 [*] `numpy` should be imported as `np`
defaults.py:12:17: ICN001 `numpy` should be imported as `np`
|
15 | import altair as altr
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
| ^^^ ICN001
18 | import pandas as pdas
19 | import seaborn as sbrn
10 | import altair as altr # unconventional
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
| ^^^ ICN001
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
|
= help: Alias `numpy` to `np`
Suggested fix
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 |- import numpy as nmp
17 |+ import numpy as np
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
defaults.py:18:22: ICN001 [*] `pandas` should be imported as `pd`
defaults.py:13:18: ICN001 `pandas` should be imported as `pd`
|
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
18 | import pandas as pdas
| ^^^^ ICN001
19 | import seaborn as sbrn
20 | import tkinter as tkr
11 | import matplotlib.pyplot as plot # unconventional
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
| ^^^^ ICN001
14 | import seaborn as sbrn # unconventional
15 | import tkinter as tkr # unconventional
|
= help: Alias `pandas` to `pd`
Suggested fix
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 |- import pandas as pdas
18 |+ import pandas as pd
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
21 21 |
defaults.py:19:23: ICN001 [*] `seaborn` should be imported as `sns`
defaults.py:14:19: ICN001 `seaborn` should be imported as `sns`
|
17 | import numpy as nmp
18 | import pandas as pdas
19 | import seaborn as sbrn
| ^^^^ ICN001
20 | import tkinter as tkr
12 | import numpy as nmp # unconventional
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
| ^^^^ ICN001
15 | import tkinter as tkr # unconventional
|
= help: Alias `seaborn` to `sns`
Suggested fix
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 |- import seaborn as sbrn
19 |+ import seaborn as sns
20 20 | import tkinter as tkr
21 21 |
22 22 |
defaults.py:20:23: ICN001 [*] `tkinter` should be imported as `tk`
defaults.py:15:19: ICN001 `tkinter` should be imported as `tk`
|
18 | import pandas as pdas
19 | import seaborn as sbrn
20 | import tkinter as tkr
| ^^^ ICN001
13 | import pandas as pdas # unconventional
14 | import seaborn as sbrn # unconventional
15 | import tkinter as tkr # unconventional
| ^^^ ICN001
16 |
17 | import altair as alt # conventional
|
= help: Alias `tkinter` to `tk`
Suggested fix
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 |- import tkinter as tkr
20 |+ import tkinter as tk
21 21 |
22 22 |
23 23 | def conventional_aliases():

View File

@@ -15,7 +15,6 @@ mod tests {
#[test_case(Rule::DuplicateClassFieldDefinition, Path::new("PIE794.py"))]
#[test_case(Rule::UnnecessaryDictKwargs, Path::new("PIE804.py"))]
#[test_case(Rule::MultipleStartsEndsWith, Path::new("PIE810.py"))]
#[test_case(Rule::UnnecessaryRangeStart, Path::new("PIE808.py"))]
#[test_case(Rule::UnnecessaryPass, Path::new("PIE790.py"))]
#[test_case(Rule::UnnecessarySpread, Path::new("PIE800.py"))]
#[test_case(Rule::ReimplementedListBuiltin, Path::new("PIE807.py"))]

View File

@@ -1,9 +1,9 @@
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use rustc_hash::FxHashSet;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use crate::autofix;
use crate::checkers::ast::Checker;
@@ -49,7 +49,11 @@ impl AlwaysAutofixableViolation for DuplicateClassFieldDefinition {
}
/// PIE794
pub(crate) fn duplicate_class_field_definition(checker: &mut Checker, body: &[Stmt]) {
pub(crate) fn duplicate_class_field_definition(
checker: &mut Checker,
parent: &Stmt,
body: &[Stmt],
) {
let mut seen_targets: FxHashSet<&str> = FxHashSet::default();
for stmt in body {
// Extract the property name from the assignment statement.
@@ -81,11 +85,11 @@ pub(crate) fn duplicate_class_field_definition(checker: &mut Checker, body: &[St
if checker.patch(diagnostic.kind.rule()) {
let edit = autofix::edits::delete_stmt(
stmt,
Some(stmt),
Some(parent),
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::suggested(edit).isolate(checker.statement_isolation()));
diagnostic.set_fix(Fix::suggested(edit).isolate(checker.isolation(Some(parent))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -4,7 +4,6 @@ pub(crate) use no_unnecessary_pass::*;
pub(crate) use non_unique_enums::*;
pub(crate) use reimplemented_list_builtin::*;
pub(crate) use unnecessary_dict_kwargs::*;
pub(crate) use unnecessary_range_start::*;
pub(crate) use unnecessary_spread::*;
mod duplicate_class_field_definition;
@@ -13,5 +12,4 @@ mod no_unnecessary_pass;
mod non_unique_enums;
mod reimplemented_list_builtin;
mod unnecessary_dict_kwargs;
mod unnecessary_range_start;
mod unnecessary_spread;

View File

@@ -50,28 +50,28 @@ impl AlwaysAutofixableViolation for UnnecessaryPass {
/// PIE790
pub(crate) fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
let [first, second, ..] = body else {
return;
};
// This only catches the case in which a docstring makes a `pass` statement
// redundant. Consider removing all `pass` statements instead.
if !is_docstring_stmt(first) {
return;
}
if body.len() > 1 {
// This only catches the case in which a docstring makes a `pass` statement
// redundant. Consider removing all `pass` statements instead.
if !is_docstring_stmt(&body[0]) {
return;
}
// The second statement must be a `pass` statement.
if !second.is_pass_stmt() {
return;
}
// The second statement must be a `pass` statement.
let stmt = &body[1];
if !stmt.is_pass_stmt() {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryPass, second.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = if let Some(index) = trailing_comment_start_offset(second, checker.locator()) {
Edit::range_deletion(second.range().add_end(index))
} else {
autofix::edits::delete_stmt(second, None, checker.locator(), checker.indexer())
};
diagnostic.set_fix(Fix::automatic(edit));
let mut diagnostic = Diagnostic::new(UnnecessaryPass, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.locator()) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
autofix::edits::delete_stmt(stmt, None, checker.locator(), checker.indexer())
};
diagnostic.set_fix(Fix::automatic(edit));
}
checker.diagnostics.push(diagnostic);
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -59,7 +59,12 @@ pub(crate) fn reimplemented_list_builtin(checker: &mut Checker, expr: &ExprLambd
range: _,
} = expr;
if parameters.is_none() {
if parameters.args.is_empty()
&& parameters.kwonlyargs.is_empty()
&& parameters.posonlyargs.is_empty()
&& parameters.vararg.is_none()
&& parameters.kwarg.is_none()
{
if let Expr::List(ast::ExprList { elts, .. }) = body.as_ref() {
if elts.is_empty() {
let mut diagnostic = Diagnostic::new(ReimplementedListBuiltin, expr.range());

View File

@@ -1,94 +0,0 @@
use num_bigint::BigInt;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::autofix::edits::{remove_argument, Parentheses};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for `range` calls with an unnecessary `start` argument.
///
/// ## Why is this bad?
/// `range(0, x)` is equivalent to `range(x)`, as `0` is the default value for
/// the `start` argument. Omitting the `start` argument makes the code more
/// concise and idiomatic.
///
/// ## Example
/// ```python
/// range(0, 3)
/// ```
///
/// Use instead:
/// ```python
/// range(3)
/// ```
///
/// ## References
/// - [Python documentation: `range`](https://docs.python.org/3/library/stdtypes.html#range)
#[violation]
pub struct UnnecessaryRangeStart;
impl AlwaysAutofixableViolation for UnnecessaryRangeStart {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `start` argument in `range`")
}
fn autofix_title(&self) -> String {
format!("Remove `start` argument")
}
}
/// PIE808
pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCall) {
// Verify that the call is to the `range` builtin.
let Expr::Name(ast::ExprName { id, .. }) = call.func.as_ref() else {
return;
};
if id != "range" {
return;
};
if !checker.semantic().is_builtin("range") {
return;
};
// `range` doesn't accept keyword arguments.
if !call.arguments.keywords.is_empty() {
return;
}
// Verify that the call has exactly two arguments (no `step`).
let [start, _] = call.arguments.args.as_slice() else {
return;
};
// Verify that the `start` argument is the literal `0`.
let Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) = start
else {
return;
};
if *value != BigInt::from(0) {
return;
};
let mut diagnostic = Diagnostic::new(UnnecessaryRangeStart, start.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
remove_argument(
&start,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
)
.map(Fix::automatic)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff/src/rules/flake8_pie/mod.rs
---
PIE808.py:2:7: PIE808 [*] Unnecessary `start` argument in `range`
|
1 | # PIE808
2 | range(0, 10)
| ^ PIE808
3 |
4 | # OK
|
= help: Remove `start` argument
Fix
1 1 | # PIE808
2 |-range(0, 10)
2 |+range(10)
3 3 |
4 4 | # OK
5 5 | range(x, 10)

View File

@@ -30,50 +30,46 @@ mod tests {
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.pyi"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))]
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.py"))]
#[test_case(Rule::CustomTypeVarReturnType, Path::new("PYI019.pyi"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))]
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.py"))]
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.pyi"))]
#[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.py"))]
#[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.pyi"))]
#[test_case(Rule::NoReturnArgumentAnnotationInStub, Path::new("PYI050.py"))]
#[test_case(Rule::NoReturnArgumentAnnotationInStub, Path::new("PYI050.pyi"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.py"))]
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.pyi"))]
#[test_case(Rule::NumericLiteralTooLong, Path::new("PYI054.py"))]
#[test_case(Rule::NumericLiteralTooLong, Path::new("PYI054.pyi"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.py"))]
#[test_case(Rule::NonEmptyStubBody, Path::new("PYI010.pyi"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.py"))]
#[test_case(Rule::PassInClassBody, Path::new("PYI012.pyi"))]
#[test_case(Rule::PassStatementStubBody, Path::new("PYI009.py"))]
#[test_case(Rule::PassStatementStubBody, Path::new("PYI009.pyi"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.py"))]
#[test_case(Rule::QuotedAnnotationInStub, Path::new("PYI020.pyi"))]
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.py"))]
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.pyi"))]
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.py"))]
#[test_case(Rule::RedundantNumericUnion, Path::new("PYI041.pyi"))]
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.py"))]
#[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))]
#[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))]
#[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))]
#[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))]
#[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.pyi"))]
#[test_case(Rule::StringOrBytesTooLong, Path::new("PYI053.py"))]
#[test_case(Rule::StringOrBytesTooLong, Path::new("PYI053.pyi"))]
#[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.py"))]
#[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.pyi"))]
#[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))]
#[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))]
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))]
#[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))]
#[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))]
#[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))]
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))]
#[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))]
#[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))]
@@ -82,12 +78,8 @@ mod tests {
#[test_case(Rule::UnaliasedCollectionsAbcSetImport, Path::new("PYI025.pyi"))]
#[test_case(Rule::UnannotatedAssignmentInStub, Path::new("PYI052.py"))]
#[test_case(Rule::UnannotatedAssignmentInStub, Path::new("PYI052.pyi"))]
#[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))]
#[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))]
#[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.py"))]
#[test_case(Rule::UnnecessaryLiteralUnion, Path::new("PYI030.pyi"))]
#[test_case(Rule::UnnecessaryTypeUnion, Path::new("PYI055.py"))]
#[test_case(Rule::UnnecessaryTypeUnion, Path::new("PYI055.pyi"))]
#[test_case(Rule::StringOrBytesTooLong, Path::new("PYI053.py"))]
#[test_case(Rule::StringOrBytesTooLong, Path::new("PYI053.pyi"))]
#[test_case(Rule::UnprefixedTypeParam, Path::new("PYI001.py"))]
#[test_case(Rule::UnprefixedTypeParam, Path::new("PYI001.pyi"))]
#[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.py"))]
@@ -96,18 +88,24 @@ mod tests {
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))]
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.py"))]
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
#[test_case(Rule::UnsupportedMethodCallOnAll, Path::new("PYI056.py"))]
#[test_case(Rule::UnsupportedMethodCallOnAll, Path::new("PYI056.pyi"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.py"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.pyi"))]
#[test_case(Rule::UnusedPrivateProtocol, Path::new("PYI046.py"))]
#[test_case(Rule::UnusedPrivateProtocol, Path::new("PYI046.pyi"))]
#[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.py"))]
#[test_case(Rule::UnusedPrivateTypeAlias, Path::new("PYI047.pyi"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.py"))]
#[test_case(Rule::UnusedPrivateTypeVar, Path::new("PYI018.pyi"))]
#[test_case(Rule::UnusedPrivateTypedDict, Path::new("PYI049.py"))]
#[test_case(Rule::UnusedPrivateTypedDict, Path::new("PYI049.pyi"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.py"))]
#[test_case(Rule::RedundantLiteralUnion, Path::new("PYI051.pyi"))]
#[test_case(Rule::UnnecessaryTypeUnion, Path::new("PYI055.py"))]
#[test_case(Rule::UnnecessaryTypeUnion, Path::new("PYI055.pyi"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -118,15 +116,15 @@ mod tests {
Ok(())
}
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
fn py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("py38_{}_{}", rule_code.noqa_code(), path.to_string_lossy());
#[test_case(Path::new("PYI019.py"))]
#[test_case(Path::new("PYI019.pyi"))]
fn custom_type_var_return_type(path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", "PYI019", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::Settings {
target_version: PythonVersion::Py38,
..settings::Settings::for_rule(rule_code)
target_version: PythonVersion::Py312,
..settings::Settings::for_rules(vec![Rule::CustomTypeVarReturnType])
},
)?;
assert_messages!(snapshot, diagnostics);

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{Expr, ExprConstant, Ranged, Stmt, StmtExpr};
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Constant, Expr, ExprConstant, Ranged, Stmt, StmtExpr};
use crate::autofix;
use crate::checkers::ast::Checker;
@@ -43,7 +44,11 @@ impl Violation for EllipsisInNonEmptyClassBody {
}
/// PYI013
pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[Stmt]) {
pub(crate) fn ellipsis_in_non_empty_class_body(
checker: &mut Checker,
parent: &Stmt,
body: &[Stmt],
) {
// If the class body contains a single statement, then it's fine for it to be an ellipsis.
if body.len() == 1 {
return;
@@ -54,24 +59,24 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[St
continue;
};
if matches!(
value.as_ref(),
Expr::Constant(ExprConstant {
value: Constant::Ellipsis,
..
})
) {
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = autofix::edits::delete_stmt(
stmt,
Some(stmt),
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
}
checker.diagnostics.push(diagnostic);
let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() else {
continue;
};
if !value.is_ellipsis() {
continue;
}
let mut diagnostic = Diagnostic::new(EllipsisInNonEmptyClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit = autofix::edits::delete_stmt(
stmt,
Some(parent),
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent))));
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -21,7 +21,6 @@ pub(crate) use quoted_annotation_in_stub::*;
pub(crate) use redundant_literal_union::*;
pub(crate) use redundant_numeric_union::*;
pub(crate) use simple_defaults::*;
use std::fmt;
pub(crate) use str_or_repr_defined_in_stub::*;
pub(crate) use string_or_bytes_too_long::*;
pub(crate) use stub_body_multiple_statements::*;
@@ -70,26 +69,3 @@ mod unrecognized_platform;
mod unrecognized_version_info;
mod unsupported_method_call_on_all;
mod unused_private_type_definition;
// TODO(charlie): Replace this with a common utility for selecting the appropriate source
// module for a given `typing` member.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TypingModule {
Typing,
TypingExtensions,
}
impl TypingModule {
fn as_str(self) -> &'static str {
match self {
TypingModule::Typing => "typing",
TypingModule::TypingExtensions => "typing_extensions",
}
}
}
impl fmt::Display for TypingModule {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(self.as_str())
}
}

View File

@@ -22,7 +22,10 @@ impl AlwaysAutofixableViolation for NonEmptyStubBody {
/// PYI010
pub(crate) fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) {
if let [Stmt::Expr(ast::StmtExpr { value, range: _ })] = body {
if body.len() != 1 {
return;
}
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = &body[0] {
if let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() {
if matches!(value, Constant::Ellipsis | Constant::Str(_)) {
return;

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Ranged};
use crate::autofix;
use crate::checkers::ast::Checker;
@@ -21,22 +22,26 @@ impl AlwaysAutofixableViolation for PassInClassBody {
}
/// PYI012
pub(crate) fn pass_in_class_body(checker: &mut Checker, class_def: &ast::StmtClassDef) {
pub(crate) fn pass_in_class_body(checker: &mut Checker, parent: &Stmt, body: &[Stmt]) {
// `pass` is required in these situations (or handled by `pass_statement_stub_body`).
if class_def.body.len() < 2 {
if body.len() < 2 {
return;
}
for stmt in &class_def.body {
for stmt in body {
if !stmt.is_pass_stmt() {
continue;
}
let mut diagnostic = Diagnostic::new(PassInClassBody, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
let edit =
autofix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
let edit = autofix::edits::delete_stmt(
stmt,
Some(parent),
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.isolation(Some(parent))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Ranged, Stmt};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -21,15 +22,15 @@ impl AlwaysAutofixableViolation for PassStatementStubBody {
/// PYI009
pub(crate) fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) {
let [stmt] = body else {
if body.len() != 1 {
return;
};
if stmt.is_pass_stmt() {
let mut diagnostic = Diagnostic::new(PassStatementStubBody, stmt.range());
}
if body[0].is_pass_stmt() {
let mut diagnostic = Diagnostic::new(PassStatementStubBody, body[0].range());
if checker.patch(Rule::PassStatementStubBody) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("..."),
stmt.range(),
body[0].range(),
)));
};
checker.diagnostics.push(diagnostic);

View File

@@ -1,18 +1,17 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::{
self as ast, Arguments, Constant, Expr, Operator, ParameterWithDefault, Parameters, Ranged,
Stmt, UnaryOp,
};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
use crate::rules::flake8_pyi::rules::TypingModule;
use crate::settings::types::PythonVersion;
#[violation]
pub struct TypedArgumentDefaultInStub;
@@ -125,7 +124,6 @@ impl Violation for UnassignedSpecialVariableInStub {
/// ```
#[violation]
pub struct TypeAliasWithoutAnnotation {
module: TypingModule,
name: String,
value: String,
}
@@ -133,16 +131,12 @@ pub struct TypeAliasWithoutAnnotation {
impl AlwaysAutofixableViolation for TypeAliasWithoutAnnotation {
#[derive_message_formats]
fn message(&self) -> String {
let TypeAliasWithoutAnnotation {
module,
name,
value,
} = self;
format!("Use `{module}.TypeAlias` for type alias, e.g., `{name}: TypeAlias = {value}`")
let TypeAliasWithoutAnnotation { name, value } = self;
format!("Use `typing.TypeAlias` for type alias, e.g., `{name}: typing.TypeAlias = {value}`")
}
fn autofix_title(&self) -> String {
"Add `TypeAlias` annotation".to_string()
"Add `typing.TypeAlias` annotation".to_string()
}
}
@@ -612,7 +606,7 @@ pub(crate) fn unassigned_special_variable_in_stub(
));
}
/// PYI026
/// PIY026
pub(crate) fn type_alias_without_annotation(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
let [target] = targets else {
return;
@@ -626,15 +620,8 @@ pub(crate) fn type_alias_without_annotation(checker: &mut Checker, value: &Expr,
return;
}
let module = if checker.settings.target_version >= PythonVersion::Py310 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
};
let mut diagnostic = Diagnostic::new(
TypeAliasWithoutAnnotation {
module,
name: id.to_string(),
value: checker.generator().expr(value),
},
@@ -643,7 +630,7 @@ pub(crate) fn type_alias_without_annotation(checker: &mut Checker, value: &Expr,
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import(module.as_str(), "TypeAlias"),
&ImportRequest::import("typing", "TypeAlias"),
target.start(),
checker.semantic(),
)?;

View File

@@ -99,7 +99,10 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
diagnostic.set_fix(
Fix::automatic(edit)
.isolate(checker.isolation(checker.semantic().current_statement_parent())),
);
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,5 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Ranged;
use ruff_python_semantic::Imported;
use ruff_python_semantic::{Binding, BindingKind};
@@ -64,7 +63,7 @@ pub(crate) fn unaliased_collections_abc_set_import(
return None;
}
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range());
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available("AbstractSet") {
diagnostic.try_set_fix(|| {

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::Scope;
use crate::checkers::ast::Checker;
@@ -192,7 +192,7 @@ pub(crate) fn unused_private_type_var(
UnusedPrivateTypeVar {
name: id.to_string(),
},
binding.range(),
binding.range,
));
}
}
@@ -234,7 +234,7 @@ pub(crate) fn unused_private_protocol(
UnusedPrivateProtocol {
name: class_def.name.to_string(),
},
binding.range(),
binding.range,
));
}
}
@@ -280,7 +280,7 @@ pub(crate) fn unused_private_type_alias(
UnusedPrivateTypeAlias {
name: id.to_string(),
},
binding.range(),
binding.range,
));
}
}
@@ -321,7 +321,7 @@ pub(crate) fn unused_private_typed_dict(
UnusedPrivateTypedDict {
name: class_def.name.to_string(),
},
binding.range(),
binding.range,
));
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny: TypeAlias = Any`
PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny: typing.TypeAlias = Any`
|
1 | from typing import Literal, Any
2 |
@@ -10,7 +10,7 @@ PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny:
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
|
= help: Add `TypeAlias` annotation
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
@@ -22,7 +22,7 @@ PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny:
5 5 | Foo = Literal["foo"]
6 6 | IntOrStr = int | str
PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `OptionalStr: TypeAlias = typing.Optional[str]`
PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `OptionalStr: typing.TypeAlias = typing.Optional[str]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
@@ -30,7 +30,7 @@ PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Optiona
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
|
= help: Add `TypeAlias` annotation
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
@@ -43,7 +43,7 @@ PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Optiona
6 6 | IntOrStr = int | str
7 7 | AliasNone = None
PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: TypeAlias = Literal["foo"]`
PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: typing.TypeAlias = Literal["foo"]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
@@ -52,7 +52,7 @@ PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: Ty
6 | IntOrStr = int | str
7 | AliasNone = None
|
= help: Add `TypeAlias` annotation
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
@@ -66,7 +66,7 @@ PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: Ty
7 7 | AliasNone = None
8 8 |
PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrStr: TypeAlias = int | str`
PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrStr: typing.TypeAlias = int | str`
|
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
@@ -74,7 +74,7 @@ PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrSt
| ^^^^^^^^ PYI026
7 | AliasNone = None
|
= help: Add `TypeAlias` annotation
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
@@ -89,7 +89,7 @@ PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrSt
8 8 |
9 9 | NewAny: typing.TypeAlias = Any
PYI026.pyi:7:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `AliasNone: TypeAlias = None`
PYI026.pyi:7:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `AliasNone: typing.TypeAlias = None`
|
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
@@ -98,7 +98,7 @@ PYI026.pyi:7:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `AliasNo
8 |
9 | NewAny: typing.TypeAlias = Any
|
= help: Add `TypeAlias` annotation
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View File

@@ -1,117 +0,0 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI026.pyi:3:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g., `NewAny: TypeAlias = Any`
|
1 | from typing import Literal, Any
2 |
3 | NewAny = Any
| ^^^^^^ PYI026
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
|
= help: Add `TypeAlias` annotation
Suggested fix
1 1 | from typing import Literal, Any
2 |+import typing_extensions
2 3 |
3 |-NewAny = Any
4 |+NewAny: typing_extensions.TypeAlias = Any
4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str
PYI026.pyi:4:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g., `OptionalStr: TypeAlias = typing.Optional[str]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
| ^^^^^^^^^^^ PYI026
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
|
= help: Add `TypeAlias` annotation
Suggested fix
1 1 | from typing import Literal, Any
2 |+import typing_extensions
2 3 |
3 4 | NewAny = Any
4 |-OptionalStr = typing.Optional[str]
5 |+OptionalStr: typing_extensions.TypeAlias = typing.Optional[str]
5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str
7 8 | AliasNone = None
PYI026.pyi:5:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g., `Foo: TypeAlias = Literal["foo"]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
| ^^^ PYI026
6 | IntOrStr = int | str
7 | AliasNone = None
|
= help: Add `TypeAlias` annotation
Suggested fix
1 1 | from typing import Literal, Any
2 |+import typing_extensions
2 3 |
3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str]
5 |-Foo = Literal["foo"]
6 |+Foo: typing_extensions.TypeAlias = Literal["foo"]
6 7 | IntOrStr = int | str
7 8 | AliasNone = None
8 9 |
PYI026.pyi:6:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g., `IntOrStr: TypeAlias = int | str`
|
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
| ^^^^^^^^ PYI026
7 | AliasNone = None
|
= help: Add `TypeAlias` annotation
Suggested fix
1 1 | from typing import Literal, Any
2 |+import typing_extensions
2 3 |
3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"]
6 |-IntOrStr = int | str
7 |+IntOrStr: typing_extensions.TypeAlias = int | str
7 8 | AliasNone = None
8 9 |
9 10 | NewAny: typing.TypeAlias = Any
PYI026.pyi:7:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g., `AliasNone: TypeAlias = None`
|
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
7 | AliasNone = None
| ^^^^^^^^^ PYI026
8 |
9 | NewAny: typing.TypeAlias = Any
|
= help: Add `TypeAlias` annotation
Suggested fix
1 1 | from typing import Literal, Any
2 |+import typing_extensions
2 3 |
3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str
7 |-AliasNone = None
8 |+AliasNone: typing_extensions.TypeAlias = None
8 9 |
9 10 | NewAny: typing.TypeAlias = Any
10 11 | OptionalStr: TypeAlias = typing.Optional[str]

View File

@@ -11,7 +11,6 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::settings::types::IdentifierPattern;
use crate::test::test_path;
use crate::{assert_messages, settings};
@@ -144,7 +143,7 @@ mod tests {
Rule::PytestRaisesTooBroad,
Path::new("PT011.py"),
Settings {
raises_extend_require_match_for: vec![IdentifierPattern::new("ZeroDivisionError").unwrap()],
raises_extend_require_match_for: vec!["ZeroDivisionError".to_string()],
..Settings::default()
},
"PT011_extend_broad_exceptions"
@@ -153,29 +152,11 @@ mod tests {
Rule::PytestRaisesTooBroad,
Path::new("PT011.py"),
Settings {
raises_require_match_for: vec![IdentifierPattern::new("ZeroDivisionError").unwrap()],
raises_require_match_for: vec!["ZeroDivisionError".to_string()],
..Settings::default()
},
"PT011_replace_broad_exceptions"
)]
#[test_case(
Rule::PytestRaisesTooBroad,
Path::new("PT011.py"),
Settings {
raises_require_match_for: vec![IdentifierPattern::new("*").unwrap()],
..Settings::default()
},
"PT011_glob_all"
)]
#[test_case(
Rule::PytestRaisesTooBroad,
Path::new("PT011.py"),
Settings {
raises_require_match_for: vec![IdentifierPattern::new("pickle.*").unwrap()],
..Settings::default()
},
"PT011_glob_prefix"
)]
#[test_case(
Rule::PytestRaisesWithMultipleStatements,
Path::new("PT012.py"),

View File

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

View File

@@ -5,13 +5,12 @@ use ruff_python_ast::{
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_codegen::Generator;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
@@ -216,17 +215,11 @@ pub struct PytestDuplicateParametrizeTestCases {
}
impl Violation for PytestDuplicateParametrizeTestCases {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let PytestDuplicateParametrizeTestCases { index } = self;
format!("Duplicate of test case at index {index} in `@pytest_mark.parametrize`")
}
fn autofix_title(&self) -> Option<String> {
Some("Remove duplicate test case".to_string())
}
}
fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
@@ -270,11 +263,12 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
/// Returns the range of the `name` argument of `@pytest.mark.parametrize`.
///
/// This accounts for parenthesized expressions. For example, the following code
/// will return the range marked with `^`:
/// This accounts for implicit string concatenation with parenthesis.
/// For example, the following code will return the range marked with `^`:
/// ```python
/// @pytest.mark.parametrize(("x"), [(1, 2)])
/// # ^^^^^
/// @pytest.mark.parametrize(("a, " "b"), [(1, 2)])
/// # ^^^^^^^^^^^
/// # implicit string concatenation with parenthesis
/// def test(a, b):
/// ...
/// ```
@@ -287,7 +281,7 @@ fn get_parametrize_name_range(
source_type: PySourceType,
) -> TextRange {
let mut locations = Vec::new();
let mut name_range = None;
let mut implicit_concat = None;
// The parenthesis are not part of the AST, so we need to tokenize the
// decorator to find them.
@@ -302,7 +296,7 @@ fn get_parametrize_name_range(
Tok::Lpar => locations.push(range.start()),
Tok::Rpar => {
if let Some(start) = locations.pop() {
name_range = Some(TextRange::new(start, range.end()));
implicit_concat = Some(TextRange::new(start, range.end()));
}
}
// Stop after the first argument.
@@ -310,7 +304,12 @@ fn get_parametrize_name_range(
_ => (),
}
}
name_range.unwrap_or_else(|| expr.range())
if let Some(range) = implicit_concat {
range
} else {
expr.range()
}
}
/// PT006
@@ -552,21 +551,6 @@ fn check_values(checker: &mut Checker, names: &Expr, values: &Expr) {
}
}
/// Given an element in a list, return the comma that follows it:
/// ```python
/// @pytest.mark.parametrize(
/// "x",
/// [.., (elt), ..],
/// ^^^^^
/// Tokenize this range to locate the comma.
/// )
/// ```
fn trailing_comma(element: &Expr, source: &str) -> Option<TextSize> {
SimpleTokenizer::starts_at(element.end(), source)
.find(|token| token.kind == SimpleTokenKind::Comma)
.map(|token| token.start())
}
/// PT014
fn check_duplicates(checker: &mut Checker, values: &Expr) {
let (Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. })) =
@@ -577,37 +561,16 @@ fn check_duplicates(checker: &mut Checker, values: &Expr) {
let mut seen: FxHashMap<ComparableExpr, usize> =
FxHashMap::with_capacity_and_hasher(elts.len(), BuildHasherDefault::default());
let mut prev = None;
for (index, element) in elts.iter().enumerate() {
let expr = ComparableExpr::from(element);
for (index, elt) in elts.iter().enumerate() {
let expr = ComparableExpr::from(elt);
seen.entry(expr)
.and_modify(|index| {
let mut diagnostic = Diagnostic::new(
checker.diagnostics.push(Diagnostic::new(
PytestDuplicateParametrizeTestCases { index: *index },
element.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(prev) = prev {
let values_end = values.range().end() - TextSize::new(1);
let previous_end = trailing_comma(prev, checker.locator().contents())
.unwrap_or(values_end);
let element_end = trailing_comma(element, checker.locator().contents())
.unwrap_or(values_end);
let deletion_range = TextRange::new(previous_end, element_end);
if !checker
.indexer()
.comment_ranges()
.intersects(deletion_range)
{
diagnostic
.set_fix(Fix::suggested(Edit::range_deletion(deletion_range)));
}
}
}
checker.diagnostics.push(diagnostic);
elt.range(),
));
})
.or_insert(index);
prev = Some(element);
}
}
@@ -673,17 +636,19 @@ pub(crate) fn parametrize(checker: &mut Checker, decorators: &[Decorator]) {
}) = &decorator.expression
{
if checker.enabled(Rule::PytestParametrizeNamesWrongType) {
if let [names, ..] = args.as_slice() {
if let Some(names) = args.get(0) {
check_names(checker, decorator, names);
}
}
if checker.enabled(Rule::PytestParametrizeValuesWrongType) {
if let [names, values, ..] = args.as_slice() {
check_values(checker, names, values);
if let Some(names) = args.get(0) {
if let Some(values) = args.get(1) {
check_values(checker, names, values);
}
}
}
if checker.enabled(Rule::PytestDuplicateParametrizeTestCases) {
if let [_, values, ..] = args.as_slice() {
if let [_, values, ..] = &args[..] {
check_duplicates(checker, values);
}
}

View File

@@ -89,19 +89,18 @@ fn check_patch_call(call: &ast::ExprCall, index: usize) -> Option<Diagnostic> {
.find_argument("new", index)?
.as_lambda_expr()?;
// Walk the lambda body. If the lambda uses the arguments, then it's valid.
if let Some(parameters) = parameters {
let mut visitor = LambdaBodyVisitor {
parameters,
uses_args: false,
};
visitor.visit_expr(body);
if visitor.uses_args {
return None;
}
}
// Walk the lambda body.
let mut visitor = LambdaBodyVisitor {
parameters,
uses_args: false,
};
visitor.visit_expr(body);
Some(Diagnostic::new(PytestPatchWithLambda, call.func.range()))
if visitor.uses_args {
None
} else {
Some(Diagnostic::new(PytestPatchWithLambda, call.func.range()))
}
}
/// PT008

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::format_call_path;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt, WithItem};
use ruff_python_semantic::SemanticModel;
@@ -230,8 +231,7 @@ fn exception_needs_match(checker: &mut Checker, exception: &Expr) {
.semantic()
.resolve_call_path(exception)
.and_then(|call_path| {
let call_path = format_call_path(&call_path);
checker
let is_broad_exception = checker
.settings
.flake8_pytest_style
.raises_require_match_for
@@ -242,8 +242,12 @@ fn exception_needs_match(checker: &mut Checker, exception: &Expr) {
.flake8_pytest_style
.raises_extend_require_match_for,
)
.any(|pattern| pattern.matches(&call_path))
.then_some(call_path)
.any(|target| call_path == from_qualified_name(target));
if is_broad_exception {
Some(format_call_path(&call_path))
} else {
None
}
})
{
checker.diagnostics.push(Diagnostic::new(

View File

@@ -1,15 +1,12 @@
//! Settings for the `flake8-pytest-style` plugin.
use std::error::Error;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::settings::types::IdentifierPattern;
use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions};
use super::types;
fn default_broad_exceptions() -> Vec<IdentifierPattern> {
fn default_broad_exceptions() -> Vec<String> {
[
"BaseException",
"Exception",
@@ -19,7 +16,7 @@ fn default_broad_exceptions() -> Vec<IdentifierPattern> {
"EnvironmentError",
"socket.error",
]
.map(|pattern| IdentifierPattern::new(pattern).expect("invalid default exception pattern"))
.map(ToString::to_string)
.to_vec()
}
@@ -89,9 +86,6 @@ pub struct Options {
)]
/// List of exception names that require a match= parameter in a
/// `pytest.raises()` call.
///
/// Supports glob patterns. For more information on the glob syntax, refer
/// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
pub raises_require_match_for: Option<Vec<String>>,
#[option(
default = "[]",
@@ -106,9 +100,6 @@ pub struct Options {
/// the entire list.
/// Note that this option does not remove any exceptions from the default
/// list.
///
/// Supports glob patterns. For more information on the glob syntax, refer
/// to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
pub raises_extend_require_match_for: Option<Vec<String>>,
#[option(
default = "true",
@@ -129,44 +120,26 @@ pub struct Settings {
pub parametrize_names_type: types::ParametrizeNameType,
pub parametrize_values_type: types::ParametrizeValuesType,
pub parametrize_values_row_type: types::ParametrizeValuesRowType,
pub raises_require_match_for: Vec<IdentifierPattern>,
pub raises_extend_require_match_for: Vec<IdentifierPattern>,
pub raises_require_match_for: Vec<String>,
pub raises_extend_require_match_for: Vec<String>,
pub mark_parentheses: bool,
}
impl TryFrom<Options> for Settings {
type Error = SettingsError;
fn try_from(options: Options) -> Result<Self, Self::Error> {
Ok(Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
fixture_parentheses: options.fixture_parentheses.unwrap_or(true),
parametrize_names_type: options.parametrize_names_type.unwrap_or_default(),
parametrize_values_type: options.parametrize_values_type.unwrap_or_default(),
parametrize_values_row_type: options.parametrize_values_row_type.unwrap_or_default(),
raises_require_match_for: options
.raises_require_match_for
.map(|patterns| {
patterns
.into_iter()
.map(|pattern| IdentifierPattern::new(&pattern))
.collect()
})
.transpose()
.map_err(SettingsError::InvalidRaisesRequireMatchFor)?
.unwrap_or_else(default_broad_exceptions),
raises_extend_require_match_for: options
.raises_extend_require_match_for
.map(|patterns| {
patterns
.into_iter()
.map(|pattern| IdentifierPattern::new(&pattern))
.collect()
})
.transpose()
.map_err(SettingsError::InvalidRaisesExtendRequireMatchFor)?
.unwrap_or_default(),
mark_parentheses: options.mark_parentheses.unwrap_or(true),
})
}
}
}
impl From<Settings> for Options {
@@ -176,20 +149,8 @@ impl From<Settings> for Options {
parametrize_names_type: Some(settings.parametrize_names_type),
parametrize_values_type: Some(settings.parametrize_values_type),
parametrize_values_row_type: Some(settings.parametrize_values_row_type),
raises_require_match_for: Some(
settings
.raises_require_match_for
.iter()
.map(ToString::to_string)
.collect(),
),
raises_extend_require_match_for: Some(
settings
.raises_extend_require_match_for
.iter()
.map(ToString::to_string)
.collect(),
),
raises_require_match_for: Some(settings.raises_require_match_for),
raises_extend_require_match_for: Some(settings.raises_extend_require_match_for),
mark_parentheses: Some(settings.mark_parentheses),
}
}
@@ -208,32 +169,3 @@ impl Default for Settings {
}
}
}
/// Error returned by the [`TryFrom`] implementation of [`Settings`].
#[derive(Debug)]
pub enum SettingsError {
InvalidRaisesRequireMatchFor(glob::PatternError),
InvalidRaisesExtendRequireMatchFor(glob::PatternError),
}
impl fmt::Display for SettingsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SettingsError::InvalidRaisesRequireMatchFor(err) => {
write!(f, "invalid raises-require-match-for pattern: {err}")
}
SettingsError::InvalidRaisesExtendRequireMatchFor(err) => {
write!(f, "invalid raises-extend-require-match-for pattern: {err}")
}
}
}
}
impl Error for SettingsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
SettingsError::InvalidRaisesRequireMatchFor(err) => Some(err),
SettingsError::InvalidRaisesExtendRequireMatchFor(err) => Some(err),
}
}
}

View File

@@ -208,24 +208,5 @@ PT006.py:64:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe
64 |+@pytest.mark.parametrize(("param1", "param2", "param3"), [(1, 2, 3), (4, 5, 6)])
65 65 | def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
66 66 | ...
67 67 |
PT006.py:69:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expected `tuple`
|
69 | @pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
| ^^^^^^^^^^^^^^^^^ PT006
70 | def test_csv_with_parens(param1, param2):
71 | ...
|
= help: Use a `tuple` for parameter names
Suggested fix
66 66 | ...
67 67 |
68 68 |
69 |-@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
69 |+@pytest.mark.parametrize(("param1", "param2"), [(1, 2), (3, 4)])
70 70 | def test_csv_with_parens(param1, param2):
71 71 | ...

View File

@@ -170,24 +170,5 @@ PT006.py:64:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expe
64 |+@pytest.mark.parametrize(["param1", "param2", "param3"], [(1, 2, 3), (4, 5, 6)])
65 65 | def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
66 66 | ...
67 67 |
PT006.py:69:26: PT006 [*] Wrong name(s) type in `@pytest.mark.parametrize`, expected `list`
|
69 | @pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
| ^^^^^^^^^^^^^^^^^ PT006
70 | def test_csv_with_parens(param1, param2):
71 | ...
|
= help: Use a `list` for parameter names
Suggested fix
66 66 | ...
67 67 |
68 68 |
69 |-@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
69 |+@pytest.mark.parametrize(["param1", "param2"], [(1, 2), (3, 4)])
70 70 | def test_csv_with_parens(param1, param2):
71 71 | ...

View File

@@ -1,47 +1,47 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT011.py:18:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:17:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
17 | def test_error_no_argument_given():
18 | with pytest.raises(ValueError):
16 | def test_error_no_argument_given():
17 | with pytest.raises(ValueError):
| ^^^^^^^^^^ PT011
19 | raise ValueError("Can't divide 1 by 0")
18 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:21:24: PT011 `pytest.raises(socket.error)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:20:24: PT011 `pytest.raises(socket.error)` is too broad, set the `match` parameter or use a more specific exception
|
19 | raise ValueError("Can't divide 1 by 0")
20 |
21 | with pytest.raises(socket.error):
18 | raise ValueError("Can't divide 1 by 0")
19 |
20 | with pytest.raises(socket.error):
| ^^^^^^^^^^^^ PT011
22 | raise ValueError("Can't divide 1 by 0")
21 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:32:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:25:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
31 | def test_error_match_is_empty():
32 | with pytest.raises(ValueError, match=None):
24 | def test_error_match_is_empty():
25 | with pytest.raises(ValueError, match=None):
| ^^^^^^^^^^ PT011
33 | raise ValueError("Can't divide 1 by 0")
26 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:35:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:28:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
33 | raise ValueError("Can't divide 1 by 0")
34 |
35 | with pytest.raises(ValueError, match=""):
26 | raise ValueError("Can't divide 1 by 0")
27 |
28 | with pytest.raises(ValueError, match=""):
| ^^^^^^^^^^ PT011
36 | raise ValueError("Can't divide 1 by 0")
29 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:38:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:31:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
36 | raise ValueError("Can't divide 1 by 0")
37 |
38 | with pytest.raises(ValueError, match=f""):
29 | raise ValueError("Can't divide 1 by 0")
30 |
31 | with pytest.raises(ValueError, match=f""):
| ^^^^^^^^^^ PT011
39 | raise ValueError("Can't divide 1 by 0")
32 | raise ValueError("Can't divide 1 by 0")
|

View File

@@ -1,55 +1,55 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT011.py:13:24: PT011 `pytest.raises(ZeroDivisionError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:12:24: PT011 `pytest.raises(ZeroDivisionError)` is too broad, set the `match` parameter or use a more specific exception
|
12 | def test_ok_different_error_from_config():
13 | with pytest.raises(ZeroDivisionError):
11 | def test_ok_different_error_from_config():
12 | with pytest.raises(ZeroDivisionError):
| ^^^^^^^^^^^^^^^^^ PT011
14 | raise ZeroDivisionError("Can't divide by 0")
13 | raise ZeroDivisionError("Can't divide by 0")
|
PT011.py:18:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:17:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
17 | def test_error_no_argument_given():
18 | with pytest.raises(ValueError):
16 | def test_error_no_argument_given():
17 | with pytest.raises(ValueError):
| ^^^^^^^^^^ PT011
19 | raise ValueError("Can't divide 1 by 0")
18 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:21:24: PT011 `pytest.raises(socket.error)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:20:24: PT011 `pytest.raises(socket.error)` is too broad, set the `match` parameter or use a more specific exception
|
19 | raise ValueError("Can't divide 1 by 0")
20 |
21 | with pytest.raises(socket.error):
18 | raise ValueError("Can't divide 1 by 0")
19 |
20 | with pytest.raises(socket.error):
| ^^^^^^^^^^^^ PT011
22 | raise ValueError("Can't divide 1 by 0")
21 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:32:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:25:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
31 | def test_error_match_is_empty():
32 | with pytest.raises(ValueError, match=None):
24 | def test_error_match_is_empty():
25 | with pytest.raises(ValueError, match=None):
| ^^^^^^^^^^ PT011
33 | raise ValueError("Can't divide 1 by 0")
26 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:35:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:28:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
33 | raise ValueError("Can't divide 1 by 0")
34 |
35 | with pytest.raises(ValueError, match=""):
26 | raise ValueError("Can't divide 1 by 0")
27 |
28 | with pytest.raises(ValueError, match=""):
| ^^^^^^^^^^ PT011
36 | raise ValueError("Can't divide 1 by 0")
29 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:38:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:31:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
36 | raise ValueError("Can't divide 1 by 0")
37 |
38 | with pytest.raises(ValueError, match=f""):
29 | raise ValueError("Can't divide 1 by 0")
30 |
31 | with pytest.raises(ValueError, match=f""):
| ^^^^^^^^^^ PT011
39 | raise ValueError("Can't divide 1 by 0")
32 | raise ValueError("Can't divide 1 by 0")
|

View File

@@ -1,73 +0,0 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT011.py:13:24: PT011 `pytest.raises(ZeroDivisionError)` is too broad, set the `match` parameter or use a more specific exception
|
12 | def test_ok_different_error_from_config():
13 | with pytest.raises(ZeroDivisionError):
| ^^^^^^^^^^^^^^^^^ PT011
14 | raise ZeroDivisionError("Can't divide by 0")
|
PT011.py:18:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
17 | def test_error_no_argument_given():
18 | with pytest.raises(ValueError):
| ^^^^^^^^^^ PT011
19 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:21:24: PT011 `pytest.raises(socket.error)` is too broad, set the `match` parameter or use a more specific exception
|
19 | raise ValueError("Can't divide 1 by 0")
20 |
21 | with pytest.raises(socket.error):
| ^^^^^^^^^^^^ PT011
22 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:24:24: PT011 `pytest.raises(pickle.PicklingError)` is too broad, set the `match` parameter or use a more specific exception
|
22 | raise ValueError("Can't divide 1 by 0")
23 |
24 | with pytest.raises(PicklingError):
| ^^^^^^^^^^^^^ PT011
25 | raise PicklingError("Can't pickle")
|
PT011.py:27:24: PT011 `pytest.raises(pickle.UnpicklingError)` is too broad, set the `match` parameter or use a more specific exception
|
25 | raise PicklingError("Can't pickle")
26 |
27 | with pytest.raises(UnpicklingError):
| ^^^^^^^^^^^^^^^ PT011
28 | raise UnpicklingError("Can't unpickle")
|
PT011.py:32:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
31 | def test_error_match_is_empty():
32 | with pytest.raises(ValueError, match=None):
| ^^^^^^^^^^ PT011
33 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:35:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
33 | raise ValueError("Can't divide 1 by 0")
34 |
35 | with pytest.raises(ValueError, match=""):
| ^^^^^^^^^^ PT011
36 | raise ValueError("Can't divide 1 by 0")
|
PT011.py:38:24: PT011 `pytest.raises(ValueError)` is too broad, set the `match` parameter or use a more specific exception
|
36 | raise ValueError("Can't divide 1 by 0")
37 |
38 | with pytest.raises(ValueError, match=f""):
| ^^^^^^^^^^ PT011
39 | raise ValueError("Can't divide 1 by 0")
|

View File

@@ -1,22 +0,0 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT011.py:24:24: PT011 `pytest.raises(pickle.PicklingError)` is too broad, set the `match` parameter or use a more specific exception
|
22 | raise ValueError("Can't divide 1 by 0")
23 |
24 | with pytest.raises(PicklingError):
| ^^^^^^^^^^^^^ PT011
25 | raise PicklingError("Can't pickle")
|
PT011.py:27:24: PT011 `pytest.raises(pickle.UnpicklingError)` is too broad, set the `match` parameter or use a more specific exception
|
25 | raise PicklingError("Can't pickle")
26 |
27 | with pytest.raises(UnpicklingError):
| ^^^^^^^^^^^^^^^ PT011
28 | raise UnpicklingError("Can't unpickle")
|

View File

@@ -1,12 +1,12 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT011.py:13:24: PT011 `pytest.raises(ZeroDivisionError)` is too broad, set the `match` parameter or use a more specific exception
PT011.py:12:24: PT011 `pytest.raises(ZeroDivisionError)` is too broad, set the `match` parameter or use a more specific exception
|
12 | def test_ok_different_error_from_config():
13 | with pytest.raises(ZeroDivisionError):
11 | def test_ok_different_error_from_config():
12 | with pytest.raises(ZeroDivisionError):
| ^^^^^^^^^^^^^^^^^ PT011
14 | raise ZeroDivisionError("Can't divide by 0")
13 | raise ZeroDivisionError("Can't divide by 0")
|

View File

@@ -1,169 +1,44 @@
---
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
---
PT014.py:4:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
PT014.py:4:35: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
4 | @pytest.mark.parametrize("x", [1, 1, 2])
| ^ PT014
5 | def test_error_literal(x):
6 | ...
|
= help: Remove duplicate test case
Suggested fix
1 1 | import pytest
2 2 |
3 3 |
4 |-@pytest.mark.parametrize("x", [1, 1, 2])
4 |+@pytest.mark.parametrize("x", [1, 2])
5 5 | def test_error_literal(x):
6 6 | ...
7 7 |
PT014.py:14:35: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
PT014.py:14:35: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
| ^ PT014
15 | def test_error_expr_simple(x):
16 | ...
|
= help: Remove duplicate test case
Suggested fix
11 11 | c = 3
12 12 |
13 13 |
14 |-@pytest.mark.parametrize("x", [a, a, b, b, b, c])
14 |+@pytest.mark.parametrize("x", [a, b, b, b, c])
15 15 | def test_error_expr_simple(x):
16 16 | ...
17 17 |
PT014.py:14:41: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.parametrize`
PT014.py:14:41: PT014 Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
| ^ PT014
15 | def test_error_expr_simple(x):
16 | ...
|
= help: Remove duplicate test case
Suggested fix
11 11 | c = 3
12 12 |
13 13 |
14 |-@pytest.mark.parametrize("x", [a, a, b, b, b, c])
14 |+@pytest.mark.parametrize("x", [a, a, b, b, c])
15 15 | def test_error_expr_simple(x):
16 16 | ...
17 17 |
PT014.py:14:44: PT014 [*] Duplicate of test case at index 2 in `@pytest_mark.parametrize`
PT014.py:14:44: PT014 Duplicate of test case at index 2 in `@pytest_mark.parametrize`
|
14 | @pytest.mark.parametrize("x", [a, a, b, b, b, c])
| ^ PT014
15 | def test_error_expr_simple(x):
16 | ...
|
= help: Remove duplicate test case
Suggested fix
11 11 | c = 3
12 12 |
13 13 |
14 |-@pytest.mark.parametrize("x", [a, a, b, b, b, c])
14 |+@pytest.mark.parametrize("x", [a, a, b, b, c])
15 15 | def test_error_expr_simple(x):
16 16 | ...
17 17 |
PT014.py:24:9: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
PT014.py:19:40: PT014 Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
22 | (a, b),
23 | # comment
24 | (a, b),
| ^^^^^^ PT014
25 | (b, c),
26 | ],
19 | @pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
| ^^^^^^ PT014
20 | def test_error_expr_complex(x):
21 | ...
|
= help: Remove duplicate test case
PT014.py:32:39: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
32 | @pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
| ^ PT014
33 | def test_error_parentheses(x):
34 | ...
|
= help: Remove duplicate test case
Suggested fix
29 29 | ...
30 30 |
31 31 |
32 |-@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
32 |+@pytest.mark.parametrize("x", [a, b, c, ((a))])
33 33 | def test_error_parentheses(x):
34 34 | ...
35 35 |
PT014.py:32:48: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
32 | @pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
| ^ PT014
33 | def test_error_parentheses(x):
34 | ...
|
= help: Remove duplicate test case
Suggested fix
29 29 | ...
30 30 |
31 31 |
32 |-@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
32 |+@pytest.mark.parametrize("x", [a, b, (a), c])
33 33 | def test_error_parentheses(x):
34 34 | ...
35 35 |
PT014.py:42:10: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
40 | a,
41 | b,
42 | (a),
| ^ PT014
43 | c,
44 | ((a)),
|
= help: Remove duplicate test case
Suggested fix
39 39 | [
40 40 | a,
41 41 | b,
42 |- (a),
43 42 | c,
44 43 | ((a)),
45 44 | ],
PT014.py:44:11: PT014 [*] Duplicate of test case at index 0 in `@pytest_mark.parametrize`
|
42 | (a),
43 | c,
44 | ((a)),
| ^ PT014
45 | ],
46 | )
|
= help: Remove duplicate test case
Suggested fix
41 41 | b,
42 42 | (a),
43 43 | c,
44 |- ((a)),
45 44 | ],
46 45 | )
47 46 | def test_error_parentheses_trailing_comma(x):

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