Compare commits
7 Commits
logical-in
...
zanie/S608
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19e98a2c7 | ||
|
|
2424188bb2 | ||
|
|
a59172528c | ||
|
|
cd29761b9c | ||
|
|
4ac78d5725 | ||
|
|
2083352ae3 | ||
|
|
0e2ece5217 |
@@ -101,4 +101,5 @@ query = "INSERT table VALUES (%s)" % (var,)
|
||||
query = "REPLACE INTO table VALUES (%s)" % (var,)
|
||||
query = "REPLACE table VALUES (%s)" % (var,)
|
||||
|
||||
query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||
not_a_query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||
not_a_query = f"Please select a value from the list of possible variants: {variants}."
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
{"foo": 1, **{"bar": 1}} # PIE800
|
||||
|
||||
{**{"bar": 10}, "a": "b"} # PIE800
|
||||
|
||||
foo({**foo, **{"bar": True}}) # PIE800
|
||||
|
||||
{**foo, **{"bar": 10}} # PIE800
|
||||
|
||||
{ # PIE800
|
||||
"a": "b",
|
||||
# Preserve
|
||||
**{
|
||||
# all
|
||||
"bar": 10, # the
|
||||
# comments
|
||||
},
|
||||
}
|
||||
|
||||
{**foo, **buzz, **{bar: 10}} # PIE800
|
||||
|
||||
{**foo, "bar": True } # OK
|
||||
|
||||
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_21.py
vendored
Normal file
4
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_21.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Test for IPython-only builtins."""
|
||||
|
||||
x = 1
|
||||
display(x)
|
||||
74
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_22.ipynb
vendored
Normal file
74
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_22.ipynb
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "af8fee97-f9aa-47c2-b34c-b109a5f083d6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "acd5eb1e-6991-42b8-806f-20d17d7e571f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"display(1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f8f3f599-030e-48b3-bcd3-31d97f725c68",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.5"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -25,3 +25,6 @@ u = u
|
||||
|
||||
def hello():
|
||||
return"Hello"
|
||||
|
||||
f"foo"u"bar"
|
||||
f"foo" u"bar"
|
||||
|
||||
@@ -207,3 +207,22 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
# The fixed string will exceed the line length, but it's still smaller than the
|
||||
# existing line length, so it's fine.
|
||||
"<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
|
||||
# When fixing, trim the trailing empty string.
|
||||
raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
"".format(new_dict, d))
|
||||
|
||||
# When fixing, trim the trailing empty string.
|
||||
raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
.format(new_dict, d))
|
||||
|
||||
raise ValueError(
|
||||
"Conflicting configuration dicts: {!r} {!r}"
|
||||
"".format(new_dict, d)
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
"Conflicting configuration dicts: {!r} {!r}"
|
||||
"".format(new_dict, d)
|
||||
|
||||
)
|
||||
|
||||
25
crates/ruff_linter/resources/test/fixtures/refurb/FURB136.py
vendored
Normal file
25
crates/ruff_linter/resources/test/fixtures/refurb/FURB136.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
x = 1
|
||||
y = 2
|
||||
|
||||
x if x > y else y # FURB136
|
||||
|
||||
x if x >= y else y # FURB136
|
||||
|
||||
x if x < y else y # FURB136
|
||||
|
||||
x if x <= y else y # FURB136
|
||||
|
||||
y if x > y else x # FURB136
|
||||
|
||||
y if x >= y else x # FURB136
|
||||
|
||||
y if x < y else x # FURB136
|
||||
|
||||
y if x <= y else x # FURB136
|
||||
|
||||
x + y if x > y else y # OK
|
||||
|
||||
x if (
|
||||
x
|
||||
> y
|
||||
) else y # FURB136
|
||||
@@ -936,13 +936,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
flake8_trio::rules::zero_sleep_call(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(
|
||||
dict @ ast::ExprDict {
|
||||
keys,
|
||||
values,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
Rule::MultiValueRepeatedKeyLiteral,
|
||||
Rule::MultiValueRepeatedKeyVariable,
|
||||
@@ -950,7 +944,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
pyflakes::rules::repeated_keys(checker, dict);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessarySpread) {
|
||||
flake8_pie::rules::unnecessary_spread(checker, keys, values);
|
||||
flake8_pie::rules::unnecessary_spread(checker, dict);
|
||||
}
|
||||
}
|
||||
Expr::Set(ast::ExprSet { elts, range: _ }) => {
|
||||
@@ -1272,21 +1266,20 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::HardcodedTempFile) {
|
||||
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
|
||||
}
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string);
|
||||
}
|
||||
if checker.source_type.is_stub() {
|
||||
if checker.enabled(Rule::StringOrBytesTooLong) {
|
||||
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::IfExp(ast::ExprIfExp {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) => {
|
||||
Expr::IfExp(
|
||||
if_exp @ ast::ExprIfExp {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
},
|
||||
) => {
|
||||
if checker.enabled(Rule::IfElseBlockInsteadOfDictGet) {
|
||||
flake8_simplify::rules::if_exp_instead_of_dict_get(
|
||||
checker, expr, test, body, orelse,
|
||||
@@ -1301,6 +1294,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::IfExprWithTwistedArms) {
|
||||
flake8_simplify::rules::twisted_arms_in_ifexpr(checker, expr, test, body, orelse);
|
||||
}
|
||||
if checker.enabled(Rule::IfExprMinMax) {
|
||||
refurb::rules::if_expr_min_max(checker, if_exp);
|
||||
}
|
||||
}
|
||||
Expr::ListComp(
|
||||
comp @ ast::ExprListComp {
|
||||
|
||||
@@ -54,7 +54,7 @@ use ruff_python_semantic::{
|
||||
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
|
||||
StarImport, SubmoduleImport,
|
||||
};
|
||||
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
|
||||
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::checkers::ast::deferred::Deferred;
|
||||
@@ -1592,9 +1592,16 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
fn bind_builtins(&mut self) {
|
||||
for builtin in BUILTINS
|
||||
for builtin in PYTHON_BUILTINS
|
||||
.iter()
|
||||
.chain(MAGIC_GLOBALS.iter())
|
||||
.chain(
|
||||
self.source_type
|
||||
.is_ipynb()
|
||||
.then_some(IPYTHON_BUILTINS)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.copied()
|
||||
.chain(self.settings.builtins.iter().map(String::as_str))
|
||||
{
|
||||
|
||||
@@ -91,6 +91,10 @@ pub(crate) fn check_tokens(
|
||||
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::UnicodeKindPrefix) {
|
||||
pyupgrade::rules::unicode_kind_prefix(&mut diagnostics, tokens);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::InvalidCharacterBackspace,
|
||||
Rule::InvalidCharacterSub,
|
||||
|
||||
@@ -947,6 +947,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
|
||||
(Refurb, "136") => (RuleGroup::Preview, rules::refurb::rules::IfExprMinMax),
|
||||
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
|
||||
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
|
||||
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::error::Error;
|
||||
|
||||
use anyhow::Result;
|
||||
use libcst_native::{ImportAlias, Name, NameOrAttribute};
|
||||
use ruff_python_ast::{self as ast, PySourceType, Stmt, Suite};
|
||||
use ruff_python_ast::{self as ast, PySourceType, Stmt};
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
@@ -26,7 +26,7 @@ mod insertion;
|
||||
|
||||
pub(crate) struct Importer<'a> {
|
||||
/// The Python AST to which we are adding imports.
|
||||
python_ast: &'a Suite,
|
||||
python_ast: &'a [Stmt],
|
||||
/// The [`Locator`] for the Python AST.
|
||||
locator: &'a Locator<'a>,
|
||||
/// The [`Stylist`] for the Python AST.
|
||||
@@ -39,7 +39,7 @@ pub(crate) struct Importer<'a> {
|
||||
|
||||
impl<'a> Importer<'a> {
|
||||
pub(crate) fn new(
|
||||
python_ast: &'a Suite,
|
||||
python_ast: &'a [Stmt],
|
||||
locator: &'a Locator<'a>,
|
||||
stylist: &'a Stylist<'a>,
|
||||
) -> Self {
|
||||
|
||||
@@ -639,7 +639,7 @@ mod tests {
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::source_kind::SourceKind;
|
||||
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
|
||||
use crate::test::{assert_notebook_path, test_contents, TestedNotebook};
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
|
||||
@@ -655,7 +655,7 @@ mod tests {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
} = assert_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::LinterSettings::for_rule(Rule::UnsortedImports),
|
||||
@@ -672,7 +672,7 @@ mod tests {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
} = assert_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::LinterSettings::for_rule(Rule::UnusedImport),
|
||||
@@ -689,7 +689,7 @@ mod tests {
|
||||
messages,
|
||||
source_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
} = assert_notebook_path(
|
||||
&actual,
|
||||
expected,
|
||||
&settings::LinterSettings::for_rule(Rule::UnusedVariable),
|
||||
@@ -706,7 +706,7 @@ mod tests {
|
||||
let TestedNotebook {
|
||||
linted_notebook: fixed_notebook,
|
||||
..
|
||||
} = test_notebook_path(
|
||||
} = assert_notebook_path(
|
||||
actual_path,
|
||||
&expected_path,
|
||||
&settings::LinterSettings::for_rule(Rule::UnusedImport),
|
||||
|
||||
@@ -297,6 +297,7 @@ impl Rule {
|
||||
| Rule::TabIndentation
|
||||
| Rule::TrailingCommaOnBareTuple
|
||||
| Rule::TypeCommentInStub
|
||||
| Rule::UnicodeKindPrefix
|
||||
| Rule::UselessSemicolon
|
||||
| Rule::UTF8EncodingDeclaration => LintSource::Tokens,
|
||||
Rule::IOError => LintSource::Io,
|
||||
|
||||
@@ -13,7 +13,15 @@ use crate::checkers::ast::Checker;
|
||||
use super::super::helpers::string_literal;
|
||||
|
||||
static SQL_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)\b(select\s.+\sfrom\s|delete\s+from\s|(insert|replace)\s.+\svalues\s|update\s.+\sset\s)")
|
||||
// We pass this generated expression strings like:
|
||||
// "SELECT " + val + " FROM " + table
|
||||
// f'delete from table where var = {var}'
|
||||
// "\n SELECT *\n FROM table\n WHERE var = {}\n ".format(var)
|
||||
//
|
||||
// To avoid false positives, we:
|
||||
// - Require the SQL to be at the start of the expression, allowing for tokens that are not a part of the string
|
||||
// - Require whole-word matches for SQL keywords
|
||||
Regex::new(r#"(?i)\A(\"|f\"|\'|f\'|\\|\\n|\s)*\b(select\s.+\sfrom\s|delete\s+from\s|(insert|replace)\s.+\svalues\s|update\s.+\sset\s)"#)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
@@ -51,7 +59,7 @@ fn has_string_literal(expr: &Expr) -> bool {
|
||||
}
|
||||
|
||||
fn matches_sql_statement(string: &str) -> bool {
|
||||
SQL_REGEX.is_match(string)
|
||||
SQL_REGEX.is_match(string.trim_start())
|
||||
}
|
||||
|
||||
fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
|
||||
@@ -476,7 +476,7 @@ S608.py:102:9: S608 Possible SQL injection vector through string-based query con
|
||||
102 | query = "REPLACE table VALUES (%s)" % (var,)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608
|
||||
103 |
|
||||
104 | query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||
104 | not_a_query = "Deselect something that is not SQL even though it has a ' from ' somewhere in %s." % "there"
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use ruff_python_stdlib::builtins::is_builtin;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_stdlib::builtins::{is_ipython_builtin, is_python_builtin};
|
||||
|
||||
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
|
||||
is_builtin(name) && ignorelist.iter().all(|ignore| ignore != name)
|
||||
pub(super) fn shadows_builtin(
|
||||
name: &str,
|
||||
ignorelist: &[String],
|
||||
source_type: PySourceType,
|
||||
) -> bool {
|
||||
if is_python_builtin(name) || source_type.is_ipynb() && is_ipython_builtin(name) {
|
||||
ignorelist.iter().all(|ignore| ignore != name)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, parameter: &Para
|
||||
if shadows_builtin(
|
||||
parameter.name.as_str(),
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinArgumentShadowing {
|
||||
|
||||
@@ -74,7 +74,11 @@ pub(crate) fn builtin_attribute_shadowing(
|
||||
name: &str,
|
||||
range: TextRange,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
// Ignore shadowing within `TypedDict` definitions, since these are only accessible through
|
||||
// subscripting and not through attribute access.
|
||||
if class_def
|
||||
@@ -102,7 +106,11 @@ pub(crate) fn builtin_method_shadowing(
|
||||
decorator_list: &[Decorator],
|
||||
range: TextRange,
|
||||
) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
// Ignore some standard-library methods. Ideally, we'd ignore all overridden methods, since
|
||||
// those should be flagged on the superclass, but that's more difficult.
|
||||
if is_standard_library_override(name, class_def, checker.semantic()) {
|
||||
|
||||
@@ -60,7 +60,11 @@ impl Violation for BuiltinVariableShadowing {
|
||||
|
||||
/// A001
|
||||
pub(crate) fn builtin_variable_shadowing(checker: &mut Checker, name: &str, range: TextRange) {
|
||||
if shadows_builtin(name, &checker.settings.flake8_builtins.builtins_ignorelist) {
|
||||
if shadows_builtin(
|
||||
name,
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.source_type,
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BuiltinVariableShadowing {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use ruff_python_ast::Expr;
|
||||
use std::fmt;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_ast::imports::{AnyImport, ImportFrom};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::Importer;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of PEP 585- and PEP 604-style type annotations in Python
|
||||
@@ -42,6 +44,10 @@ use crate::checkers::ast::Checker;
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as adding `from __future__ import annotations`
|
||||
/// may change the semantics of the program.
|
||||
///
|
||||
/// ## Options
|
||||
/// - `target-version`
|
||||
#[violation]
|
||||
@@ -66,18 +72,28 @@ impl fmt::Display for Reason {
|
||||
}
|
||||
}
|
||||
|
||||
impl Violation for FutureRequiredTypeAnnotation {
|
||||
impl AlwaysFixableViolation for FutureRequiredTypeAnnotation {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let FutureRequiredTypeAnnotation { reason } = self;
|
||||
format!("Missing `from __future__ import annotations`, but uses {reason}")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Add `from __future__ import annotations`")
|
||||
}
|
||||
}
|
||||
|
||||
/// FA102
|
||||
pub(crate) fn future_required_type_annotation(checker: &mut Checker, expr: &Expr, reason: Reason) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
FutureRequiredTypeAnnotation { reason },
|
||||
expr.range(),
|
||||
));
|
||||
let mut diagnostic = Diagnostic::new(FutureRequiredTypeAnnotation { reason }, expr.range());
|
||||
if let Some(python_ast) = checker.semantic().definitions.python_ast() {
|
||||
let required_import =
|
||||
AnyImport::ImportFrom(ImportFrom::member("__future__", "annotations"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(
|
||||
Importer::new(python_ast, checker.locator(), checker.stylist())
|
||||
.add_import(&required_import, TextSize::default()),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_lowercase.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
no_future_import_uses_lowercase.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_lowercase.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_lowercase.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int]) -> None:
|
||||
| ^^^^^^^^^^^^^^ FA102
|
||||
7 | del y
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,34 +1,62 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str] | None = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
| ^^^^^^^^^^^^^^ FA102
|
||||
7 | del y
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str, int] | None) -> None:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
7 | del y
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str] | None = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -1,52 +1,94 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_future_annotations/mod.rs
|
||||
---
|
||||
no_future_import_uses_union_inner.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
no_future_import_uses_union_inner.py:2:13: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union_inner.py:2:18: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:2:18: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
1 | def main() -> None:
|
||||
2 | a_list: list[str | None] = []
|
||||
| ^^^^^^^^^^ FA102
|
||||
3 | a_list.append("hello")
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union_inner.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:6:14: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union_inner.py:6:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:6:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
| ^^^^^^^^^^ FA102
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union_inner.py:7:8: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:8: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 585 collection
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
no_future_import_uses_union_inner.py:7:19: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
no_future_import_uses_union_inner.py:7:19: FA102 [*] Missing `from __future__ import annotations`, but uses PEP 604 union
|
||||
|
|
||||
6 | def hello(y: dict[str | None, int]) -> None:
|
||||
7 | z: tuple[str, str | None, str] = tuple(y)
|
||||
| ^^^^^^^^^^ FA102
|
||||
8 | del z
|
||||
|
|
||||
= help: Add `from __future__ import annotations`
|
||||
|
||||
ℹ Unsafe fix
|
||||
1 |+from __future__ import annotations
|
||||
1 2 | def main() -> None:
|
||||
2 3 | a_list: list[str | None] = []
|
||||
3 4 | a_list.append("hello")
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnnecessaryPlaceholder, Path::new("PIE790.py"))]
|
||||
#[test_case(Rule::UnnecessarySpread, Path::new("PIE800.py"))]
|
||||
#[test_case(Rule::ReimplementedContainerBuiltin, Path::new("PIE807.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -32,22 +33,76 @@ use crate::checkers::ast::Checker;
|
||||
pub struct UnnecessarySpread;
|
||||
|
||||
impl Violation for UnnecessarySpread {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Unnecessary spread `**`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Remove unnecessary dict"))
|
||||
}
|
||||
}
|
||||
|
||||
/// PIE800
|
||||
pub(crate) fn unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
|
||||
for item in keys.iter().zip(values.iter()) {
|
||||
pub(crate) fn unnecessary_spread(checker: &mut Checker, dict: &ast::ExprDict) {
|
||||
// The first "end" is the start of the dictionary, immediately following the open bracket.
|
||||
let mut prev_end = dict.start() + TextSize::from(1);
|
||||
for item in dict.keys.iter().zip(dict.values.iter()) {
|
||||
if let (None, value) = item {
|
||||
// We only care about when the key is None which indicates a spread `**`
|
||||
// inside a dict.
|
||||
if let Expr::Dict(_) = value {
|
||||
let diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
|
||||
if let Expr::Dict(inner) = value {
|
||||
let mut diagnostic = Diagnostic::new(UnnecessarySpread, value.range());
|
||||
if checker.settings.preview.is_enabled() {
|
||||
if let Some(fix) = unnecessary_spread_fix(inner, prev_end, checker.locator()) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
prev_end = item.1.end();
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to remove an unnecessary dictionary spread.
|
||||
fn unnecessary_spread_fix(
|
||||
dict: &ast::ExprDict,
|
||||
prev_end: TextSize,
|
||||
locator: &Locator,
|
||||
) -> Option<Fix> {
|
||||
// Find the `**` token preceding the spread.
|
||||
let doublestar = SimpleTokenizer::starts_at(prev_end, locator.contents())
|
||||
.find(|tok| matches!(tok.kind(), SimpleTokenKind::DoubleStar))?;
|
||||
|
||||
if let Some(last) = dict.values.last() {
|
||||
// Ex) `**{a: 1, b: 2}`
|
||||
let mut edits = vec![];
|
||||
for tok in SimpleTokenizer::starts_at(last.end(), locator.contents()).skip_trivia() {
|
||||
match tok.kind() {
|
||||
SimpleTokenKind::Comma => {
|
||||
edits.push(Edit::range_deletion(tok.range()));
|
||||
}
|
||||
SimpleTokenKind::RBrace => {
|
||||
edits.push(Edit::range_deletion(tok.range()));
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(Fix::safe_edits(
|
||||
// Delete the first `**{`
|
||||
Edit::deletion(doublestar.start(), dict.start() + TextSize::from(1)),
|
||||
// Delete the trailing `}`
|
||||
edits,
|
||||
))
|
||||
} else {
|
||||
// Ex) `**{}`
|
||||
Some(Fix::safe_edit(Edit::deletion(
|
||||
doublestar.start(),
|
||||
dict.end(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,37 +6,67 @@ PIE800.py:1:14: PIE800 Unnecessary spread `**`
|
||||
1 | {"foo": 1, **{"bar": 1}} # PIE800
|
||||
| ^^^^^^^^^^ PIE800
|
||||
2 |
|
||||
3 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
PIE800.py:3:15: PIE800 Unnecessary spread `**`
|
||||
PIE800.py:3:4: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
1 | {"foo": 1, **{"bar": 1}} # PIE800
|
||||
2 |
|
||||
3 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
| ^^^^^^^^^^^ PIE800
|
||||
4 |
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
PIE800.py:5:15: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
4 |
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
| ^^^^^^^^^^^^^ PIE800
|
||||
4 |
|
||||
5 | {**foo, **{"bar": 10}} # PIE800
|
||||
6 |
|
||||
7 | {**foo, **{"bar": 10}} # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
PIE800.py:5:11: PIE800 Unnecessary spread `**`
|
||||
PIE800.py:7:11: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
3 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
4 |
|
||||
5 | {**foo, **{"bar": 10}} # PIE800
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
6 |
|
||||
7 | {**foo, **{"bar": 10}} # PIE800
|
||||
| ^^^^^^^^^^^ PIE800
|
||||
6 |
|
||||
7 | {**foo, **buzz, **{bar: 10}} # PIE800
|
||||
|
|
||||
|
||||
PIE800.py:7:19: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
5 | {**foo, **{"bar": 10}} # PIE800
|
||||
6 |
|
||||
7 | {**foo, **buzz, **{bar: 10}} # PIE800
|
||||
| ^^^^^^^^^ PIE800
|
||||
8 |
|
||||
9 | {**foo, "bar": True } # OK
|
||||
9 | { # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
PIE800.py:12:7: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
10 | "a": "b",
|
||||
11 | # Preserve
|
||||
12 | **{
|
||||
| _______^
|
||||
13 | | # all
|
||||
14 | | "bar": 10, # the
|
||||
15 | | # comments
|
||||
16 | | },
|
||||
| |_____^ PIE800
|
||||
17 | }
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
PIE800.py:19:19: PIE800 Unnecessary spread `**`
|
||||
|
|
||||
17 | }
|
||||
18 |
|
||||
19 | {**foo, **buzz, **{bar: 10}} # PIE800
|
||||
| ^^^^^^^^^ PIE800
|
||||
20 |
|
||||
21 | {**foo, "bar": True } # OK
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pie/mod.rs
|
||||
---
|
||||
PIE800.py:1:14: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
1 | {"foo": 1, **{"bar": 1}} # PIE800
|
||||
| ^^^^^^^^^^ PIE800
|
||||
2 |
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-{"foo": 1, **{"bar": 1}} # PIE800
|
||||
1 |+{"foo": 1, "bar": 1} # PIE800
|
||||
2 2 |
|
||||
3 3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
4 4 |
|
||||
|
||||
PIE800.py:3:4: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
1 | {"foo": 1, **{"bar": 1}} # PIE800
|
||||
2 |
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
| ^^^^^^^^^^^ PIE800
|
||||
4 |
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | {"foo": 1, **{"bar": 1}} # PIE800
|
||||
2 2 |
|
||||
3 |-{**{"bar": 10}, "a": "b"} # PIE800
|
||||
3 |+{"bar": 10, "a": "b"} # PIE800
|
||||
4 4 |
|
||||
5 5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
6 6 |
|
||||
|
||||
PIE800.py:5:15: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
4 |
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
| ^^^^^^^^^^^^^ PIE800
|
||||
6 |
|
||||
7 | {**foo, **{"bar": 10}} # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | {**{"bar": 10}, "a": "b"} # PIE800
|
||||
4 4 |
|
||||
5 |-foo({**foo, **{"bar": True}}) # PIE800
|
||||
5 |+foo({**foo, "bar": True}) # PIE800
|
||||
6 6 |
|
||||
7 7 | {**foo, **{"bar": 10}} # PIE800
|
||||
8 8 |
|
||||
|
||||
PIE800.py:7:11: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
6 |
|
||||
7 | {**foo, **{"bar": 10}} # PIE800
|
||||
| ^^^^^^^^^^^ PIE800
|
||||
8 |
|
||||
9 | { # PIE800
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | foo({**foo, **{"bar": True}}) # PIE800
|
||||
6 6 |
|
||||
7 |-{**foo, **{"bar": 10}} # PIE800
|
||||
7 |+{**foo, "bar": 10} # PIE800
|
||||
8 8 |
|
||||
9 9 | { # PIE800
|
||||
10 10 | "a": "b",
|
||||
|
||||
PIE800.py:12:7: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
10 | "a": "b",
|
||||
11 | # Preserve
|
||||
12 | **{
|
||||
| _______^
|
||||
13 | | # all
|
||||
14 | | "bar": 10, # the
|
||||
15 | | # comments
|
||||
16 | | },
|
||||
| |_____^ PIE800
|
||||
17 | }
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 | { # PIE800
|
||||
10 10 | "a": "b",
|
||||
11 11 | # Preserve
|
||||
12 |- **{
|
||||
12 |+
|
||||
13 13 | # all
|
||||
14 |- "bar": 10, # the
|
||||
14 |+ "bar": 10 # the
|
||||
15 15 | # comments
|
||||
16 |- },
|
||||
16 |+ ,
|
||||
17 17 | }
|
||||
18 18 |
|
||||
19 19 | {**foo, **buzz, **{bar: 10}} # PIE800
|
||||
|
||||
PIE800.py:19:19: PIE800 [*] Unnecessary spread `**`
|
||||
|
|
||||
17 | }
|
||||
18 |
|
||||
19 | {**foo, **buzz, **{bar: 10}} # PIE800
|
||||
| ^^^^^^^^^ PIE800
|
||||
20 |
|
||||
21 | {**foo, "bar": True } # OK
|
||||
|
|
||||
= help: Remove unnecessary dict
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | },
|
||||
17 17 | }
|
||||
18 18 |
|
||||
19 |-{**foo, **buzz, **{bar: 10}} # PIE800
|
||||
19 |+{**foo, **buzz, bar: 10} # PIE800
|
||||
20 20 |
|
||||
21 21 | {**foo, "bar": True } # OK
|
||||
22 22 |
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ mod tests {
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_18.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_19.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_20.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_21.py"))]
|
||||
#[test_case(Rule::UndefinedName, Path::new("F821_22.ipynb"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
|
||||
#[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
F821_21.py:4:1: F821 Undefined name `display`
|
||||
|
|
||||
3 | x = 1
|
||||
4 | display(x)
|
||||
| ^^^^^^^ F821
|
||||
|
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
|
||||
@@ -384,7 +384,21 @@ pub(crate) fn f_strings(
|
||||
contents.push_str(&fstring);
|
||||
prev_end = range.end();
|
||||
}
|
||||
contents.push_str(checker.locator().slice(TextRange::new(prev_end, end)));
|
||||
|
||||
// If the remainder is non-empty, add it to the contents.
|
||||
let rest = checker.locator().slice(TextRange::new(prev_end, end));
|
||||
if !lexer::lex_starts_at(rest, Mode::Expression, prev_end)
|
||||
.flatten()
|
||||
.all(|(token, _)| match token {
|
||||
Tok::Comment(_) | Tok::Newline | Tok::NonLogicalNewline | Tok::Indent | Tok::Dedent => {
|
||||
true
|
||||
}
|
||||
Tok::String { value, .. } => value.is_empty(),
|
||||
_ => false,
|
||||
})
|
||||
{
|
||||
contents.push_str(rest);
|
||||
}
|
||||
|
||||
// If necessary, add a space between any leading keyword (`return`, `yield`, `assert`, etc.)
|
||||
// and the string. For example, `return"foo"` is valid, but `returnf"foo"` is not.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::ExprStringLiteral;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::{StringKind, Tok};
|
||||
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of the Unicode kind prefix (`u`) in strings.
|
||||
@@ -39,13 +40,19 @@ impl AlwaysFixableViolation for UnicodeKindPrefix {
|
||||
}
|
||||
|
||||
/// UP025
|
||||
pub(crate) fn unicode_kind_prefix(checker: &mut Checker, string: &ExprStringLiteral) {
|
||||
if string.unicode {
|
||||
let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, string.range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at(
|
||||
string.start(),
|
||||
TextSize::from(1),
|
||||
))));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
pub(crate) fn unicode_kind_prefix(diagnostics: &mut Vec<Diagnostic>, tokens: &[LexResult]) {
|
||||
for (token, range) in tokens.iter().flatten() {
|
||||
if let Tok::String {
|
||||
kind: StringKind::Unicode,
|
||||
..
|
||||
} = token
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(UnicodeKindPrefix, *range);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::at(
|
||||
range.start(),
|
||||
TextSize::from(1),
|
||||
))));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,4 +248,37 @@ UP025.py:19:5: UP025 [*] Remove unicode literals from strings
|
||||
21 21 | # These should not change
|
||||
22 22 | u = "Hello"
|
||||
|
||||
UP025.py:29:7: UP025 [*] Remove unicode literals from strings
|
||||
|
|
||||
27 | return"Hello"
|
||||
28 |
|
||||
29 | f"foo"u"bar"
|
||||
| ^^^^^^ UP025
|
||||
30 | f"foo" u"bar"
|
||||
|
|
||||
= help: Remove unicode prefix
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | def hello():
|
||||
27 27 | return"Hello"
|
||||
28 28 |
|
||||
29 |-f"foo"u"bar"
|
||||
29 |+f"foo""bar"
|
||||
30 30 | f"foo" u"bar"
|
||||
|
||||
UP025.py:30:8: UP025 [*] Remove unicode literals from strings
|
||||
|
|
||||
29 | f"foo"u"bar"
|
||||
30 | f"foo" u"bar"
|
||||
| ^^^^^^ UP025
|
||||
|
|
||||
= help: Remove unicode prefix
|
||||
|
||||
ℹ Safe fix
|
||||
27 27 | return"Hello"
|
||||
28 28 |
|
||||
29 29 | f"foo"u"bar"
|
||||
30 |-f"foo" u"bar"
|
||||
30 |+f"foo" "bar"
|
||||
|
||||
|
||||
|
||||
@@ -962,6 +962,8 @@ UP032_0.py:209:1: UP032 [*] Use f-string instead of `format` call
|
||||
208 | # existing line length, so it's fine.
|
||||
209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP032
|
||||
210 |
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
@@ -971,5 +973,98 @@ UP032_0.py:209:1: UP032 [*] Use f-string instead of `format` call
|
||||
208 208 | # existing line length, so it's fine.
|
||||
209 |-"<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
209 |+f"<Customer: {self.internal_ids}, {self.external_ids}, {self.properties}, {self.tags}, {self.others}>"
|
||||
210 210 |
|
||||
211 211 | # When fixing, trim the trailing empty string.
|
||||
212 212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032_0.py:212:18: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
211 | # When fixing, trim the trailing empty string.
|
||||
212 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
213 | | "".format(new_dict, d))
|
||||
| |_______________________________________^ UP032
|
||||
214 |
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Safe fix
|
||||
209 209 | "<Customer: {}, {}, {}, {}, {}>".format(self.internal_ids, self.external_ids, self.properties, self.tags, self.others)
|
||||
210 210 |
|
||||
211 211 | # When fixing, trim the trailing empty string.
|
||||
212 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
213 |- "".format(new_dict, d))
|
||||
212 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
214 213 |
|
||||
215 214 | # When fixing, trim the trailing empty string.
|
||||
216 215 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032_0.py:216:18: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
215 | # When fixing, trim the trailing empty string.
|
||||
216 | raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
| __________________^
|
||||
217 | | .format(new_dict, d))
|
||||
| |_____________________________________^ UP032
|
||||
218 |
|
||||
219 | raise ValueError(
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Safe fix
|
||||
213 213 | "".format(new_dict, d))
|
||||
214 214 |
|
||||
215 215 | # When fixing, trim the trailing empty string.
|
||||
216 |-raise ValueError("Conflicting configuration dicts: {!r} {!r}"
|
||||
217 |- .format(new_dict, d))
|
||||
216 |+raise ValueError(f"Conflicting configuration dicts: {new_dict!r} {d!r}")
|
||||
218 217 |
|
||||
219 218 | raise ValueError(
|
||||
220 219 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
|
||||
UP032_0.py:220:5: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
219 | raise ValueError(
|
||||
220 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
| _____^
|
||||
221 | | "".format(new_dict, d)
|
||||
| |__________________________^ UP032
|
||||
222 | )
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Safe fix
|
||||
217 217 | .format(new_dict, d))
|
||||
218 218 |
|
||||
219 219 | raise ValueError(
|
||||
220 |- "Conflicting configuration dicts: {!r} {!r}"
|
||||
221 |- "".format(new_dict, d)
|
||||
220 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
222 221 | )
|
||||
223 222 |
|
||||
224 223 | raise ValueError(
|
||||
|
||||
UP032_0.py:225:5: UP032 [*] Use f-string instead of `format` call
|
||||
|
|
||||
224 | raise ValueError(
|
||||
225 | "Conflicting configuration dicts: {!r} {!r}"
|
||||
| _____^
|
||||
226 | | "".format(new_dict, d)
|
||||
| |__________________________^ UP032
|
||||
227 |
|
||||
228 | )
|
||||
|
|
||||
= help: Convert to f-string
|
||||
|
||||
ℹ Safe fix
|
||||
222 222 | )
|
||||
223 223 |
|
||||
224 224 | raise ValueError(
|
||||
225 |- "Conflicting configuration dicts: {!r} {!r}"
|
||||
226 |- "".format(new_dict, d)
|
||||
225 |+ f"Conflicting configuration dicts: {new_dict!r} {d!r}"
|
||||
227 226 |
|
||||
228 227 | )
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ mod tests {
|
||||
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
|
||||
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
|
||||
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
|
||||
#[test_case(Rule::IfExprMinMax, Path::new("FURB136.py"))]
|
||||
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
|
||||
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
|
||||
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
|
||||
|
||||
178
crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs
Normal file
178
crates/ruff_linter/src/rules/refurb/rules/if_expr_min_max.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `if` expressions that can be replaced with `min()` or `max()`
|
||||
/// calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// An `if` expression that selects the lesser or greater of two
|
||||
/// sub-expressions can be replaced with a `min()` or `max()` call
|
||||
/// respectively. When possible, prefer `min()` and `max()`, as they're more
|
||||
/// concise and readable than the equivalent `if` expression.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// highest_score = score1 if score1 > score2 else score2
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// highest_score = max(score2, score1)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `min`](https://docs.python.org/3.11/library/functions.html#min)
|
||||
/// - [Python documentation: `max`](https://docs.python.org/3.11/library/functions.html#max)
|
||||
#[violation]
|
||||
pub struct IfExprMinMax {
|
||||
min_max: MinMax,
|
||||
expression: SourceCodeSnippet,
|
||||
replacement: SourceCodeSnippet,
|
||||
}
|
||||
|
||||
impl Violation for IfExprMinMax {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self {
|
||||
min_max,
|
||||
expression,
|
||||
replacement,
|
||||
} = self;
|
||||
|
||||
match (expression.full_display(), replacement.full_display()) {
|
||||
(_, None) => {
|
||||
format!("Replace `if` expression with `{min_max}` call")
|
||||
}
|
||||
(None, Some(replacement)) => {
|
||||
format!("Replace `if` expression with `{replacement}`")
|
||||
}
|
||||
(Some(expression), Some(replacement)) => {
|
||||
format!("Replace `{expression}` with `{replacement}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self {
|
||||
replacement,
|
||||
min_max,
|
||||
..
|
||||
} = self;
|
||||
if let Some(replacement) = replacement.full_display() {
|
||||
Some(format!("Replace with `{replacement}`"))
|
||||
} else {
|
||||
Some(format!("Replace with `{min_max}` call"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB136
|
||||
pub(crate) fn if_expr_min_max(checker: &mut Checker, if_exp: &ast::ExprIfExp) {
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
..
|
||||
}) = if_exp.test.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Ignore, e.g., `foo < bar < baz`.
|
||||
let [op] = ops.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Determine whether to use `min()` or `max()`, and whether to flip the
|
||||
// order of the arguments, which is relevant for breaking ties.
|
||||
let (mut min_max, mut flip_args) = match op {
|
||||
CmpOp::Gt => (MinMax::Max, true),
|
||||
CmpOp::GtE => (MinMax::Max, false),
|
||||
CmpOp::Lt => (MinMax::Min, true),
|
||||
CmpOp::LtE => (MinMax::Min, false),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let [right] = comparators.as_slice() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let body_cmp = ComparableExpr::from(if_exp.body.as_ref());
|
||||
let orelse_cmp = ComparableExpr::from(if_exp.orelse.as_ref());
|
||||
let left_cmp = ComparableExpr::from(left);
|
||||
let right_cmp = ComparableExpr::from(right);
|
||||
|
||||
if body_cmp == right_cmp && orelse_cmp == left_cmp {
|
||||
min_max = min_max.reverse();
|
||||
flip_args = !flip_args;
|
||||
} else if body_cmp != left_cmp || orelse_cmp != right_cmp {
|
||||
return;
|
||||
}
|
||||
|
||||
let (arg1, arg2) = if flip_args {
|
||||
(right, left.as_ref())
|
||||
} else {
|
||||
(left.as_ref(), right)
|
||||
};
|
||||
|
||||
let replacement = format!(
|
||||
"{min_max}({}, {})",
|
||||
checker.generator().expr(arg1),
|
||||
checker.generator().expr(arg2),
|
||||
);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
IfExprMinMax {
|
||||
min_max,
|
||||
expression: SourceCodeSnippet::from_str(checker.locator().slice(if_exp)),
|
||||
replacement: SourceCodeSnippet::from_str(replacement.as_str()),
|
||||
},
|
||||
if_exp.range(),
|
||||
);
|
||||
|
||||
if checker.semantic().is_builtin(min_max.as_str()) {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
replacement,
|
||||
if_exp.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum MinMax {
|
||||
Min,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl MinMax {
|
||||
fn reverse(self) -> Self {
|
||||
match self {
|
||||
Self::Min => Self::Max,
|
||||
Self::Max => Self::Min,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Min => "min",
|
||||
Self::Max => "max",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MinMax {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) use check_and_remove_from_set::*;
|
||||
pub(crate) use delete_full_slice::*;
|
||||
pub(crate) use if_expr_min_max::*;
|
||||
pub(crate) use implicit_cwd::*;
|
||||
pub(crate) use isinstance_type_none::*;
|
||||
pub(crate) use print_empty_string::*;
|
||||
@@ -13,6 +14,7 @@ pub(crate) use unnecessary_enumerate::*;
|
||||
|
||||
mod check_and_remove_from_set;
|
||||
mod delete_full_slice;
|
||||
mod if_expr_min_max;
|
||||
mod implicit_cwd;
|
||||
mod isinstance_type_none;
|
||||
mod print_empty_string;
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_python_semantic::analyze::typing::is_list;
|
||||
use ruff_python_semantic::{Binding, BindingId, DefinitionId, SemanticModel};
|
||||
use ruff_python_semantic::{Binding, BindingId, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -183,8 +183,7 @@ fn match_consecutive_appends<'a>(
|
||||
let siblings: &[Stmt] = if semantic.at_top_level() {
|
||||
// If the statement is at the top level, we should go to the parent module.
|
||||
// Module is available in the definitions list.
|
||||
let module = semantic.definitions[DefinitionId::module()].as_module()?;
|
||||
module.python_ast
|
||||
semantic.definitions.python_ast()?
|
||||
} else {
|
||||
// Otherwise, go to the parent, and take its body as a sequence of siblings.
|
||||
semantic
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB136.py:4:1: FURB136 [*] Replace `x if x > y else y` with `max(y, x)`
|
||||
|
|
||||
2 | y = 2
|
||||
3 |
|
||||
4 | x if x > y else y # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^ FURB136
|
||||
5 |
|
||||
6 | x if x >= y else y # FURB136
|
||||
|
|
||||
= help: Replace with `max(y, x)`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | x = 1
|
||||
2 2 | y = 2
|
||||
3 3 |
|
||||
4 |-x if x > y else y # FURB136
|
||||
4 |+max(y, x) # FURB136
|
||||
5 5 |
|
||||
6 6 | x if x >= y else y # FURB136
|
||||
7 7 |
|
||||
|
||||
FURB136.py:6:1: FURB136 [*] Replace `x if x >= y else y` with `max(x, y)`
|
||||
|
|
||||
4 | x if x > y else y # FURB136
|
||||
5 |
|
||||
6 | x if x >= y else y # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB136
|
||||
7 |
|
||||
8 | x if x < y else y # FURB136
|
||||
|
|
||||
= help: Replace with `max(x, y)`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 |
|
||||
4 4 | x if x > y else y # FURB136
|
||||
5 5 |
|
||||
6 |-x if x >= y else y # FURB136
|
||||
6 |+max(x, y) # FURB136
|
||||
7 7 |
|
||||
8 8 | x if x < y else y # FURB136
|
||||
9 9 |
|
||||
|
||||
FURB136.py:8:1: FURB136 [*] Replace `x if x < y else y` with `min(y, x)`
|
||||
|
|
||||
6 | x if x >= y else y # FURB136
|
||||
7 |
|
||||
8 | x if x < y else y # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^ FURB136
|
||||
9 |
|
||||
10 | x if x <= y else y # FURB136
|
||||
|
|
||||
= help: Replace with `min(y, x)`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 |
|
||||
6 6 | x if x >= y else y # FURB136
|
||||
7 7 |
|
||||
8 |-x if x < y else y # FURB136
|
||||
8 |+min(y, x) # FURB136
|
||||
9 9 |
|
||||
10 10 | x if x <= y else y # FURB136
|
||||
11 11 |
|
||||
|
||||
FURB136.py:10:1: FURB136 [*] Replace `x if x <= y else y` with `min(x, y)`
|
||||
|
|
||||
8 | x if x < y else y # FURB136
|
||||
9 |
|
||||
10 | x if x <= y else y # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB136
|
||||
11 |
|
||||
12 | y if x > y else x # FURB136
|
||||
|
|
||||
= help: Replace with `min(x, y)`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 |
|
||||
8 8 | x if x < y else y # FURB136
|
||||
9 9 |
|
||||
10 |-x if x <= y else y # FURB136
|
||||
10 |+min(x, y) # FURB136
|
||||
11 11 |
|
||||
12 12 | y if x > y else x # FURB136
|
||||
13 13 |
|
||||
|
||||
FURB136.py:12:1: FURB136 [*] Replace `y if x > y else x` with `min(x, y)`
|
||||
|
|
||||
10 | x if x <= y else y # FURB136
|
||||
11 |
|
||||
12 | y if x > y else x # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^ FURB136
|
||||
13 |
|
||||
14 | y if x >= y else x # FURB136
|
||||
|
|
||||
= help: Replace with `min(x, y)`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 |
|
||||
10 10 | x if x <= y else y # FURB136
|
||||
11 11 |
|
||||
12 |-y if x > y else x # FURB136
|
||||
12 |+min(x, y) # FURB136
|
||||
13 13 |
|
||||
14 14 | y if x >= y else x # FURB136
|
||||
15 15 |
|
||||
|
||||
FURB136.py:14:1: FURB136 [*] Replace `y if x >= y else x` with `min(y, x)`
|
||||
|
|
||||
12 | y if x > y else x # FURB136
|
||||
13 |
|
||||
14 | y if x >= y else x # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB136
|
||||
15 |
|
||||
16 | y if x < y else x # FURB136
|
||||
|
|
||||
= help: Replace with `min(y, x)`
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 |
|
||||
12 12 | y if x > y else x # FURB136
|
||||
13 13 |
|
||||
14 |-y if x >= y else x # FURB136
|
||||
14 |+min(y, x) # FURB136
|
||||
15 15 |
|
||||
16 16 | y if x < y else x # FURB136
|
||||
17 17 |
|
||||
|
||||
FURB136.py:16:1: FURB136 [*] Replace `y if x < y else x` with `max(x, y)`
|
||||
|
|
||||
14 | y if x >= y else x # FURB136
|
||||
15 |
|
||||
16 | y if x < y else x # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^ FURB136
|
||||
17 |
|
||||
18 | y if x <= y else x # FURB136
|
||||
|
|
||||
= help: Replace with `max(x, y)`
|
||||
|
||||
ℹ Safe fix
|
||||
13 13 |
|
||||
14 14 | y if x >= y else x # FURB136
|
||||
15 15 |
|
||||
16 |-y if x < y else x # FURB136
|
||||
16 |+max(x, y) # FURB136
|
||||
17 17 |
|
||||
18 18 | y if x <= y else x # FURB136
|
||||
19 19 |
|
||||
|
||||
FURB136.py:18:1: FURB136 [*] Replace `y if x <= y else x` with `max(y, x)`
|
||||
|
|
||||
16 | y if x < y else x # FURB136
|
||||
17 |
|
||||
18 | y if x <= y else x # FURB136
|
||||
| ^^^^^^^^^^^^^^^^^^ FURB136
|
||||
19 |
|
||||
20 | x + y if x > y else y # OK
|
||||
|
|
||||
= help: Replace with `max(y, x)`
|
||||
|
||||
ℹ Safe fix
|
||||
15 15 |
|
||||
16 16 | y if x < y else x # FURB136
|
||||
17 17 |
|
||||
18 |-y if x <= y else x # FURB136
|
||||
18 |+max(y, x) # FURB136
|
||||
19 19 |
|
||||
20 20 | x + y if x > y else y # OK
|
||||
21 21 |
|
||||
|
||||
FURB136.py:22:1: FURB136 [*] Replace `if` expression with `max(y, x)`
|
||||
|
|
||||
20 | x + y if x > y else y # OK
|
||||
21 |
|
||||
22 | / x if (
|
||||
23 | | x
|
||||
24 | | > y
|
||||
25 | | ) else y # FURB136
|
||||
| |________^ FURB136
|
||||
|
|
||||
= help: Replace with `max(y, x)`
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 |
|
||||
20 20 | x + y if x > y else y # OK
|
||||
21 21 |
|
||||
22 |-x if (
|
||||
23 |- x
|
||||
24 |- > y
|
||||
25 |-) else y # FURB136
|
||||
22 |+max(y, x) # FURB136
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ pub(crate) fn test_resource_path(path: impl AsRef<Path>) -> std::path::PathBuf {
|
||||
Path::new("./resources/test/").join(path)
|
||||
}
|
||||
|
||||
/// Run [`check_path`] on a file in the `resources/test/fixtures` directory.
|
||||
/// Run [`check_path`] on a Python file in the `resources/test/fixtures` directory.
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn test_path(path: impl AsRef<Path>, settings: &LinterSettings) -> Result<Vec<Message>> {
|
||||
let path = test_resource_path("fixtures").join(path);
|
||||
let contents = std::fs::read_to_string(&path)?;
|
||||
Ok(test_contents(&SourceKind::Python(contents), &path, settings).0)
|
||||
let source_type = PySourceType::from(&path);
|
||||
let source_kind = SourceKind::from_path(path.as_ref(), source_type)?.expect("valid source");
|
||||
Ok(test_contents(&source_kind, &path, settings).0)
|
||||
}
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
@@ -54,7 +55,7 @@ pub(crate) struct TestedNotebook {
|
||||
}
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
pub(crate) fn test_notebook_path(
|
||||
pub(crate) fn assert_notebook_path(
|
||||
path: impl AsRef<Path>,
|
||||
expected: impl AsRef<Path>,
|
||||
settings: &LinterSettings,
|
||||
|
||||
@@ -248,6 +248,12 @@ impl<'a> Definitions<'a> {
|
||||
|
||||
ContextualizedDefinitions(definitions.raw)
|
||||
}
|
||||
|
||||
/// Returns a reference to the Python AST.
|
||||
pub fn python_ast(&self) -> Option<&'a [Stmt]> {
|
||||
let module = self[DefinitionId::module()].as_module()?;
|
||||
Some(module.python_ast)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Definitions<'a> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// A list of all Python builtins.
|
||||
///
|
||||
/// Intended to be kept in sync with [`is_builtin`].
|
||||
pub const BUILTINS: &[&str] = &[
|
||||
/// Intended to be kept in sync with [`is_python_builtin`].
|
||||
pub const PYTHON_BUILTINS: &[&str] = &[
|
||||
"ArithmeticError",
|
||||
"AssertionError",
|
||||
"AttributeError",
|
||||
@@ -161,6 +161,11 @@ pub const BUILTINS: &[&str] = &[
|
||||
"zip",
|
||||
];
|
||||
|
||||
/// A list of all builtins that are available in IPython.
|
||||
///
|
||||
/// Intended to be kept in sync with [`is_ipython_builtin`].
|
||||
pub const IPYTHON_BUILTINS: &[&str] = &["display"];
|
||||
|
||||
/// Globally defined names which are not attributes of the builtins module, or
|
||||
/// are only present on some platforms.
|
||||
pub const MAGIC_GLOBALS: &[&str] = &[
|
||||
@@ -173,9 +178,9 @@ pub const MAGIC_GLOBALS: &[&str] = &[
|
||||
|
||||
/// Returns `true` if the given name is that of a Python builtin.
|
||||
///
|
||||
/// Intended to be kept in sync with [`BUILTINS`].
|
||||
pub fn is_builtin(name: &str) -> bool {
|
||||
// Constructed by converting the `BUILTINS` slice to a `match` expression.
|
||||
/// Intended to be kept in sync with [`PYTHON_BUILTINS`].
|
||||
pub fn is_python_builtin(name: &str) -> bool {
|
||||
// Constructed by converting the `PYTHON_BUILTINS` slice to a `match` expression.
|
||||
matches!(
|
||||
name,
|
||||
"ArithmeticError"
|
||||
@@ -345,3 +350,11 @@ pub fn is_iterator(name: &str) -> bool {
|
||||
"enumerate" | "filter" | "map" | "reversed" | "zip" | "iter"
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given name is that of an IPython builtin.
|
||||
///
|
||||
/// Intended to be kept in sync with [`IPYTHON_BUILTINS`].
|
||||
pub fn is_ipython_builtin(name: &str) -> bool {
|
||||
// Constructed by converting the `IPYTHON_BUILTINS` slice to a `match` expression.
|
||||
matches!(name, "display")
|
||||
}
|
||||
|
||||
@@ -1151,6 +1151,7 @@ mod tests {
|
||||
Rule::DirectLoggerInstantiation,
|
||||
Rule::InvalidGetLoggerArgument,
|
||||
Rule::IsinstanceTypeNone,
|
||||
Rule::IfExprMinMax,
|
||||
Rule::ManualDictComprehension,
|
||||
Rule::ReimplementedStarmap,
|
||||
Rule::SliceCopy,
|
||||
|
||||
1
ruff.schema.json
generated
1
ruff.schema.json
generated
@@ -2823,6 +2823,7 @@
|
||||
"FURB13",
|
||||
"FURB131",
|
||||
"FURB132",
|
||||
"FURB136",
|
||||
"FURB14",
|
||||
"FURB140",
|
||||
"FURB145",
|
||||
|
||||
Reference in New Issue
Block a user