Compare commits

...

12 Commits

Author SHA1 Message Date
Charlie Marsh
0152814a00 Bump version to 0.0.215 2023-01-07 22:17:29 -05:00
Harutaka Kawamura
0b3fab256b Remove assertNotContains (#1729)
`unittest.TestCase` doens't have a method named `assertNotContains`.
2023-01-07 22:15:48 -05:00
Chammika Mannakkara
212ce4d331 buf-fix: flake8_simplify SIM212 (#1732)
bug-fix in #1717

Use the correct `IfExprWithTwistedArms` struct.
2023-01-07 22:03:48 -05:00
Charlie Marsh
491b1e4968 Move RUFF_CACHE_DIR to Clap's env support (#1733) 2023-01-07 22:01:27 -05:00
Charlie Marsh
8b01b53d89 Move RUFF_CACHE_DIR to Clap's env support (#1733) 2023-01-07 22:01:20 -05:00
messense
f9a5867d3e Add RUFF_FORMAT environment variable support (#1731)
Resolves #1716
2023-01-07 21:54:19 -05:00
Harutaka Kawamura
4149627f19 Add more unittest assert methods to PT009 (#1730) 2023-01-07 21:52:48 -05:00
Charlie Marsh
7d24146df7 Implement --isolated CLI flag (#1727)
Closes #1724.
2023-01-07 18:43:58 -05:00
Charlie Marsh
1c6ef3666c Treat failures to fix TypedDict conversions as debug logs (#1723)
This also allows us to flag the error, even if we can't fix it.

Closes #1212.
2023-01-07 17:51:45 -05:00
Charlie Marsh
16d933fcf5 Respect isort:skip action comment (#1722)
Resolves: #1718.
2023-01-07 17:30:18 -05:00
Charlie Marsh
a9cc56b2ac Add ComparableExpr hierarchy for comparing expressions (#1721) 2023-01-07 17:29:21 -05:00
Charlie Marsh
4de6c26ff9 Automatically remove duplicate dictionary keys (#1710)
For now, to be safe, we're only removing keys with duplicate _values_.

See: #1647.
2023-01-07 16:16:42 -05:00
35 changed files with 1473 additions and 323 deletions

View File

@@ -4,7 +4,7 @@ Thank you for taking the time to report an issue! We're glad to have you involve
If you're filing a bug report, please consider including the following information:
- A minimal code snippet that reproduces the bug.
- The command you invoked (e.g., `ruff /path/to/file.py --fix`).
- The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
- The current Ruff settings (any relevant sections from your `pyproject.toml`).
- The current Ruff version (`ruff --version`).
-->

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.214
rev: v0.0.215
hooks:
- id: ruff

8
Cargo.lock generated
View File

@@ -735,7 +735,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.214-dev.0"
version = "0.0.215-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1873,7 +1873,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.214"
version = "0.0.215"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1941,7 +1941,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.214"
version = "0.0.215"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1961,7 +1961,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.214"
version = "0.0.215"
dependencies = [
"once_cell",
"proc-macro2",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.214"
version = "0.0.215"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -29,7 +29,7 @@ bitflags = { version = "1.3.2" }
cachedir = { version = "0.3.0" }
cfg-if = { version = "1.0.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
clap = { version = "4.0.1", features = ["derive"] }
clap = { version = "4.0.1", features = ["derive", "env"] }
clap_complete_command = { version = "0.4.0" }
colored = { version = "2.0.0" }
dirs = { version = "4.0.0" }
@@ -51,7 +51,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
quick-junit = { version = "0.3.2" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.214", path = "ruff_macros" }
ruff_macros = { version = "0.0.215", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }

View File

@@ -180,7 +180,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.214'
rev: 'v0.0.215'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -341,6 +341,8 @@ Options:
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-n, --no-cache
Disable cache reads
--isolated
Ignore all configuration files
--select <SELECT>
Comma-separated list of error codes to enable (or ALL, to enable all checks)
--extend-select <EXTEND_SELECT>
@@ -360,11 +362,11 @@ Options:
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [possible values: text, json, junit, grouped, github, gitlab]
Output serialization format for error messages [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab]
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
--cache-dir <CACHE_DIR>
Path to the cache directory
Path to the cache directory [env: RUFF_CACHE_DIR=]
--show-source
Show violations with source code
--respect-gitignore
@@ -551,8 +553,8 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F524 | StringDotFormatMissingArguments | '...'.format(...) is missing argument(s) for placeholder(s): ... | |
| F525 | StringDotFormatMixingAutomatic | '...'.format(...) mixes automatic and manual numbering | |
| F541 | FStringMissingPlaceholders | f-string without any placeholders | 🛠 |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal `...` repeated | 🛠 |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | 🛠 |
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.214"
version = "0.0.215"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.214"
version = "0.0.215"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.214-dev.0"
version = "0.0.215-dev.0"
edition = "2021"
[lib]

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.214"
version = "0.0.215"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -1,10 +1,20 @@
# isort: off
import sys
import os
import collections
# isort: on
def f():
# isort: off
import sys
import os
import collections
# isort: on
import sys
import os # isort: skip
import collections
import abc
def f():
import sys
import os # isort: skip
import collections
import abc
def f():
import sys
import os # isort:skip
import collections
import abc

View File

@@ -10,3 +10,41 @@ x = {
b"123": 1,
b"123": 4,
}
x = {
"a": 1,
"a": 2,
"a": 3,
"a": 3,
}
x = {
"a": 1,
"a": 2,
"a": 3,
"a": 3,
"a": 4,
}
x = {
"a": 1,
"a": 1,
"a": 2,
"a": 3,
"a": 4,
}
x = {
a: 1,
"a": 1,
a: 1,
"a": 2,
a: 2,
"a": 3,
a: 3,
"a": 3,
a: 4,
}
x = {"a": 1, "a": 1}
x = {"a": 1, "b": 2, "a": 1}

View File

@@ -5,3 +5,41 @@ x = {
a: 2,
b: 3,
}
x = {
a: 1,
a: 2,
a: 3,
a: 3,
}
x = {
a: 1,
a: 2,
a: 3,
a: 3,
a: 4,
}
x = {
a: 1,
a: 1,
a: 2,
a: 3,
a: 4,
}
x = {
a: 1,
"a": 1,
a: 1,
"a": 2,
a: 2,
"a": 3,
a: 3,
"a": 3,
a: 4,
}
x = {a: 1, a: 1}
x = {a: 1, b: 2, a: 1}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.214"
version = "0.0.215"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.214"
version = "0.0.215"
edition = "2021"
[lib]

524
src/ast/comparable.rs Normal file
View File

@@ -0,0 +1,524 @@
//! An equivalent object hierarchy to the `Expr` hierarchy, but with the ability
//! to compare expressions for equality (via `Eq` and `Hash`).
use num_bigint::BigInt;
use rustpython_ast::{
Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Keyword,
Operator, Unaryop,
};
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableExprContext {
Load,
Store,
Del,
}
impl From<&ExprContext> for ComparableExprContext {
fn from(ctx: &ExprContext) -> Self {
match ctx {
ExprContext::Load => Self::Load,
ExprContext::Store => Self::Store,
ExprContext::Del => Self::Del,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableBoolop {
And,
Or,
}
impl From<&Boolop> for ComparableBoolop {
fn from(op: &Boolop) -> Self {
match op {
Boolop::And => Self::And,
Boolop::Or => Self::Or,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableOperator {
Add,
Sub,
Mult,
MatMult,
Div,
Mod,
Pow,
LShift,
RShift,
BitOr,
BitXor,
BitAnd,
FloorDiv,
}
impl From<&Operator> for ComparableOperator {
fn from(op: &Operator) -> Self {
match op {
Operator::Add => Self::Add,
Operator::Sub => Self::Sub,
Operator::Mult => Self::Mult,
Operator::MatMult => Self::MatMult,
Operator::Div => Self::Div,
Operator::Mod => Self::Mod,
Operator::Pow => Self::Pow,
Operator::LShift => Self::LShift,
Operator::RShift => Self::RShift,
Operator::BitOr => Self::BitOr,
Operator::BitXor => Self::BitXor,
Operator::BitAnd => Self::BitAnd,
Operator::FloorDiv => Self::FloorDiv,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableUnaryop {
Invert,
Not,
UAdd,
USub,
}
impl From<&Unaryop> for ComparableUnaryop {
fn from(op: &Unaryop) -> Self {
match op {
Unaryop::Invert => Self::Invert,
Unaryop::Not => Self::Not,
Unaryop::UAdd => Self::UAdd,
Unaryop::USub => Self::USub,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableCmpop {
Eq,
NotEq,
Lt,
LtE,
Gt,
GtE,
Is,
IsNot,
In,
NotIn,
}
impl From<&Cmpop> for ComparableCmpop {
fn from(op: &Cmpop) -> Self {
match op {
Cmpop::Eq => Self::Eq,
Cmpop::NotEq => Self::NotEq,
Cmpop::Lt => Self::Lt,
Cmpop::LtE => Self::LtE,
Cmpop::Gt => Self::Gt,
Cmpop::GtE => Self::GtE,
Cmpop::Is => Self::Is,
Cmpop::IsNot => Self::IsNot,
Cmpop::In => Self::In,
Cmpop::NotIn => Self::NotIn,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableConstant<'a> {
None,
Bool(&'a bool),
Str(&'a str),
Bytes(&'a [u8]),
Int(&'a BigInt),
Tuple(Vec<ComparableConstant<'a>>),
Float(u64),
Complex { real: u64, imag: u64 },
Ellipsis,
}
impl<'a> From<&'a Constant> for ComparableConstant<'a> {
fn from(constant: &'a Constant) -> Self {
match constant {
Constant::None => Self::None,
Constant::Bool(value) => Self::Bool(value),
Constant::Str(value) => Self::Str(value),
Constant::Bytes(value) => Self::Bytes(value),
Constant::Int(value) => Self::Int(value),
Constant::Tuple(value) => {
Self::Tuple(value.iter().map(std::convert::Into::into).collect())
}
Constant::Float(value) => Self::Float(value.to_bits()),
Constant::Complex { real, imag } => Self::Complex {
real: real.to_bits(),
imag: imag.to_bits(),
},
Constant::Ellipsis => Self::Ellipsis,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableArguments<'a> {
pub posonlyargs: Vec<ComparableArg<'a>>,
pub args: Vec<ComparableArg<'a>>,
pub vararg: Option<ComparableArg<'a>>,
pub kwonlyargs: Vec<ComparableArg<'a>>,
pub kw_defaults: Vec<ComparableExpr<'a>>,
pub kwarg: Option<ComparableArg<'a>>,
pub defaults: Vec<ComparableExpr<'a>>,
}
impl<'a> From<&'a Arguments> for ComparableArguments<'a> {
fn from(arguments: &'a Arguments) -> Self {
Self {
posonlyargs: arguments
.posonlyargs
.iter()
.map(std::convert::Into::into)
.collect(),
args: arguments
.args
.iter()
.map(std::convert::Into::into)
.collect(),
vararg: arguments.vararg.as_ref().map(std::convert::Into::into),
kwonlyargs: arguments
.kwonlyargs
.iter()
.map(std::convert::Into::into)
.collect(),
kw_defaults: arguments
.kw_defaults
.iter()
.map(std::convert::Into::into)
.collect(),
kwarg: arguments.vararg.as_ref().map(std::convert::Into::into),
defaults: arguments
.defaults
.iter()
.map(std::convert::Into::into)
.collect(),
}
}
}
impl<'a> From<&'a Box<Arg>> for ComparableArg<'a> {
fn from(arg: &'a Box<Arg>) -> Self {
(&**arg).into()
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableArg<'a> {
pub arg: &'a str,
pub annotation: Option<Box<ComparableExpr<'a>>>,
pub type_comment: Option<&'a str>,
}
impl<'a> From<&'a Arg> for ComparableArg<'a> {
fn from(arg: &'a Arg) -> Self {
Self {
arg: &arg.node.arg,
annotation: arg.node.annotation.as_ref().map(std::convert::Into::into),
type_comment: arg.node.type_comment.as_deref(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableKeyword<'a> {
pub arg: Option<&'a str>,
pub value: ComparableExpr<'a>,
}
impl<'a> From<&'a Keyword> for ComparableKeyword<'a> {
fn from(keyword: &'a Keyword) -> Self {
Self {
arg: keyword.node.arg.as_deref(),
value: (&keyword.node.value).into(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableComprehension<'a> {
pub target: ComparableExpr<'a>,
pub iter: ComparableExpr<'a>,
pub ifs: Vec<ComparableExpr<'a>>,
pub is_async: &'a usize,
}
impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> {
fn from(comprehension: &'a Comprehension) -> Self {
Self {
target: (&comprehension.target).into(),
iter: (&comprehension.iter).into(),
ifs: comprehension
.ifs
.iter()
.map(std::convert::Into::into)
.collect(),
is_async: &comprehension.is_async,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableExpr<'a> {
BoolOp {
op: ComparableBoolop,
values: Vec<ComparableExpr<'a>>,
},
NamedExpr {
target: Box<ComparableExpr<'a>>,
value: Box<ComparableExpr<'a>>,
},
BinOp {
left: Box<ComparableExpr<'a>>,
op: ComparableOperator,
right: Box<ComparableExpr<'a>>,
},
UnaryOp {
op: ComparableUnaryop,
operand: Box<ComparableExpr<'a>>,
},
Lambda {
args: ComparableArguments<'a>,
body: Box<ComparableExpr<'a>>,
},
IfExp {
test: Box<ComparableExpr<'a>>,
body: Box<ComparableExpr<'a>>,
orelse: Box<ComparableExpr<'a>>,
},
Dict {
keys: Vec<ComparableExpr<'a>>,
values: Vec<ComparableExpr<'a>>,
},
Set {
elts: Vec<ComparableExpr<'a>>,
},
ListComp {
elt: Box<ComparableExpr<'a>>,
generators: Vec<ComparableComprehension<'a>>,
},
SetComp {
elt: Box<ComparableExpr<'a>>,
generators: Vec<ComparableComprehension<'a>>,
},
DictComp {
key: Box<ComparableExpr<'a>>,
value: Box<ComparableExpr<'a>>,
generators: Vec<ComparableComprehension<'a>>,
},
GeneratorExp {
elt: Box<ComparableExpr<'a>>,
generators: Vec<ComparableComprehension<'a>>,
},
Await {
value: Box<ComparableExpr<'a>>,
},
Yield {
value: Option<Box<ComparableExpr<'a>>>,
},
YieldFrom {
value: Box<ComparableExpr<'a>>,
},
Compare {
left: Box<ComparableExpr<'a>>,
ops: Vec<ComparableCmpop>,
comparators: Vec<ComparableExpr<'a>>,
},
Call {
func: Box<ComparableExpr<'a>>,
args: Vec<ComparableExpr<'a>>,
keywords: Vec<ComparableKeyword<'a>>,
},
FormattedValue {
value: Box<ComparableExpr<'a>>,
conversion: &'a usize,
format_spec: Option<Box<ComparableExpr<'a>>>,
},
JoinedStr {
values: Vec<ComparableExpr<'a>>,
},
Constant {
value: ComparableConstant<'a>,
kind: Option<&'a str>,
},
Attribute {
value: Box<ComparableExpr<'a>>,
attr: &'a str,
ctx: ComparableExprContext,
},
Subscript {
value: Box<ComparableExpr<'a>>,
slice: Box<ComparableExpr<'a>>,
ctx: ComparableExprContext,
},
Starred {
value: Box<ComparableExpr<'a>>,
ctx: ComparableExprContext,
},
Name {
id: &'a str,
ctx: ComparableExprContext,
},
List {
elts: Vec<ComparableExpr<'a>>,
ctx: ComparableExprContext,
},
Tuple {
elts: Vec<ComparableExpr<'a>>,
ctx: ComparableExprContext,
},
Slice {
lower: Option<Box<ComparableExpr<'a>>>,
upper: Option<Box<ComparableExpr<'a>>>,
step: Option<Box<ComparableExpr<'a>>>,
},
}
impl<'a> From<&'a Box<Expr>> for Box<ComparableExpr<'a>> {
fn from(expr: &'a Box<Expr>) -> Self {
Box::new((&**expr).into())
}
}
impl<'a> From<&'a Expr> for ComparableExpr<'a> {
fn from(expr: &'a Expr) -> Self {
match &expr.node {
ExprKind::BoolOp { op, values } => Self::BoolOp {
op: op.into(),
values: values.iter().map(std::convert::Into::into).collect(),
},
ExprKind::NamedExpr { target, value } => Self::NamedExpr {
target: target.into(),
value: value.into(),
},
ExprKind::BinOp { left, op, right } => Self::BinOp {
left: left.into(),
op: op.into(),
right: right.into(),
},
ExprKind::UnaryOp { op, operand } => Self::UnaryOp {
op: op.into(),
operand: operand.into(),
},
ExprKind::Lambda { args, body } => Self::Lambda {
args: (&**args).into(),
body: body.into(),
},
ExprKind::IfExp { test, body, orelse } => Self::IfExp {
test: test.into(),
body: body.into(),
orelse: orelse.into(),
},
ExprKind::Dict { keys, values } => Self::Dict {
keys: keys.iter().map(std::convert::Into::into).collect(),
values: values.iter().map(std::convert::Into::into).collect(),
},
ExprKind::Set { elts } => Self::Set {
elts: elts.iter().map(std::convert::Into::into).collect(),
},
ExprKind::ListComp { elt, generators } => Self::ListComp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
},
ExprKind::SetComp { elt, generators } => Self::SetComp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
},
ExprKind::DictComp {
key,
value,
generators,
} => Self::DictComp {
key: key.into(),
value: value.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
},
ExprKind::GeneratorExp { elt, generators } => Self::GeneratorExp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
},
ExprKind::Await { value } => Self::Await {
value: value.into(),
},
ExprKind::Yield { value } => Self::Yield {
value: value.as_ref().map(std::convert::Into::into),
},
ExprKind::YieldFrom { value } => Self::YieldFrom {
value: value.into(),
},
ExprKind::Compare {
left,
ops,
comparators,
} => Self::Compare {
left: left.into(),
ops: ops.iter().map(std::convert::Into::into).collect(),
comparators: comparators.iter().map(std::convert::Into::into).collect(),
},
ExprKind::Call {
func,
args,
keywords,
} => Self::Call {
func: func.into(),
args: args.iter().map(std::convert::Into::into).collect(),
keywords: keywords.iter().map(std::convert::Into::into).collect(),
},
ExprKind::FormattedValue {
value,
conversion,
format_spec,
} => Self::FormattedValue {
value: value.into(),
conversion,
format_spec: format_spec.as_ref().map(std::convert::Into::into),
},
ExprKind::JoinedStr { values } => Self::JoinedStr {
values: values.iter().map(std::convert::Into::into).collect(),
},
ExprKind::Constant { value, kind } => Self::Constant {
value: value.into(),
kind: kind.as_ref().map(String::as_str),
},
ExprKind::Attribute { value, attr, ctx } => Self::Attribute {
value: value.into(),
attr,
ctx: ctx.into(),
},
ExprKind::Subscript { value, slice, ctx } => Self::Subscript {
value: value.into(),
slice: slice.into(),
ctx: ctx.into(),
},
ExprKind::Starred { value, ctx } => Self::Starred {
value: value.into(),
ctx: ctx.into(),
},
ExprKind::Name { id, ctx } => Self::Name {
id,
ctx: ctx.into(),
},
ExprKind::List { elts, ctx } => Self::List {
elts: elts.iter().map(std::convert::Into::into).collect(),
ctx: ctx.into(),
},
ExprKind::Tuple { elts, ctx } => Self::Tuple {
elts: elts.iter().map(std::convert::Into::into).collect(),
ctx: ctx.into(),
},
ExprKind::Slice { lower, upper, step } => Self::Slice {
lower: lower.as_ref().map(std::convert::Into::into),
upper: upper.as_ref().map(std::convert::Into::into),
step: step.as_ref().map(std::convert::Into::into),
},
}
}
}

View File

@@ -1,5 +1,6 @@
pub mod branch_detection;
pub mod cast;
pub mod comparable;
pub mod function_type;
pub mod helpers;
pub mod operations;

View File

@@ -1,6 +1,5 @@
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -8,16 +7,15 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use filetime::FileTime;
use log::error;
use once_cell::sync::Lazy;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::message::Message;
use crate::settings::{flags, Settings};
pub const CACHE_DIR_NAME: &str = ".ruff_cache";
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
static CACHE_DIR: Lazy<Option<String>> = Lazy::new(|| std::env::var("RUFF_CACHE_DIR").ok());
pub const DEFAULT_CACHE_DIR_NAME: &str = ".ruff_cache";
#[derive(Serialize, Deserialize)]
struct CacheMetadata {
@@ -39,9 +37,7 @@ struct CheckResult {
/// Return the cache directory for a given project root. Defers to the
/// `RUFF_CACHE_DIR` environment variable, if set.
pub fn cache_dir(project_root: &Path) -> PathBuf {
CACHE_DIR
.as_ref()
.map_or_else(|| project_root.join(DEFAULT_CACHE_DIR_NAME), PathBuf::from)
project_root.join(CACHE_DIR_NAME)
}
fn content_dir() -> &'static Path {
@@ -60,7 +56,7 @@ fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autof
/// Initialize the cache at the specified `Path`.
pub fn init(path: &Path) -> Result<()> {
// Create the cache directories.
create_dir_all(path.join(content_dir()))?;
fs::create_dir_all(path.join(content_dir()))?;
// Add the CACHEDIR.TAG.
if !cachedir::is_tagged(path)? {
@@ -70,7 +66,7 @@ pub fn init(path: &Path) -> Result<()> {
// Add the .gitignore.
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
let mut file = File::create(gitignore_path)?;
let mut file = fs::File::create(gitignore_path)?;
file.write_all(b"*")?;
}
@@ -91,7 +87,7 @@ fn read_sync(cache_dir: &Path, key: u64) -> Result<Vec<u8>, std::io::Error> {
/// Get a value from the cache.
pub fn get<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
metadata: &fs::Metadata,
settings: &Settings,
autofix: flags::Autofix,
) -> Option<Vec<Message>> {
@@ -115,7 +111,7 @@ pub fn get<P: AsRef<Path>>(
/// Set a value in the cache.
pub fn set<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
metadata: &fs::Metadata,
settings: &Settings,
autofix: flags::Autofix,
messages: &[Message],

View File

@@ -2385,15 +2385,11 @@ where
));
}
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.enabled.contains(&CheckCode::F601);
let check_repeated_variables = self.settings.enabled.contains(&CheckCode::F602);
if check_repeated_literals || check_repeated_variables {
self.checks.extend(pyflakes::checks::repeated_keys(
keys,
check_repeated_literals,
check_repeated_variables,
));
ExprKind::Dict { keys, values } => {
if self.settings.enabled.contains(&CheckCode::F601)
|| self.settings.enabled.contains(&CheckCode::F602)
{
pyflakes::plugins::repeated_keys(self, keys, values);
}
}
ExprKind::Yield { .. } => {

View File

@@ -20,7 +20,7 @@ pub struct Cli {
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
#[arg(long)]
#[arg(long, conflicts_with = "isolated")]
pub config: Option<PathBuf>,
/// Enable verbose logging.
#[arg(short, long, group = "verbosity")]
@@ -56,6 +56,9 @@ pub struct Cli {
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// Ignore all configuration files.
#[arg(long, conflicts_with = "config")]
pub isolated: bool,
/// Comma-separated list of error codes to enable (or ALL, to enable all
/// checks).
#[arg(long, value_delimiter = ',')]
@@ -90,13 +93,13 @@ pub struct Cli {
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Output serialization format for error messages.
#[arg(long, value_enum)]
#[arg(long, value_enum, env = "RUFF_FORMAT")]
pub format: Option<SerializationFormat>,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<PathBuf>,
/// Path to the cache directory.
#[arg(long)]
#[arg(long, env = "RUFF_CACHE_DIR")]
pub cache_dir: Option<PathBuf>,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
@@ -240,6 +243,7 @@ impl Cli {
explain: self.explain,
files: self.files,
generate_shell_completion: self.generate_shell_completion,
isolated: self.isolated,
no_cache: self.no_cache,
quiet: self.quiet,
show_files: self.show_files,
@@ -301,6 +305,7 @@ pub struct Arguments {
pub explain: Option<CheckCode>,
pub files: Vec<PathBuf>,
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub isolated: bool,
pub no_cache: bool,
pub quiet: bool,
pub show_files: bool,

View File

@@ -16,7 +16,7 @@ use serde::Serialize;
use walkdir::WalkDir;
use crate::autofix::fixer;
use crate::cache::DEFAULT_CACHE_DIR_NAME;
use crate::cache::CACHE_DIR_NAME;
use crate::cli::Overrides;
use crate::iterators::par_iter;
use crate::linter::{add_noqa_to_path, lint_path, lint_stdin, Diagnostics};
@@ -340,10 +340,10 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
pub fn clean(level: &LogLevel) -> Result<()> {
for entry in WalkDir::new(&*path_dedot::CWD)
.into_iter()
.filter_map(std::result::Result::ok)
.filter_map(Result::ok)
.filter(|entry| entry.file_type().is_dir())
{
let cache = entry.path().join(DEFAULT_CACHE_DIR_NAME);
let cache = entry.path().join(CACHE_DIR_NAME);
if cache.is_dir() {
if level >= &LogLevel::Default {
eprintln!("Removing cache at: {}", fs::relativize_path(&cache).bold());

View File

@@ -104,10 +104,13 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
continue;
};
// `isort` allows for `# isort: skip` and `# isort: skip_file` to include or
// omit a space after the colon. The remaining action comments are
// required to include the space, and must appear on their own lines.
let comment_text = comment_text.trim_end();
if comment_text == "# isort: split" {
splits.push(start.row());
} else if comment_text == "# isort: skip_file" {
} else if comment_text == "# isort: skip_file" || comment_text == "# isort:skip_file" {
skip_file = true;
} else if off.is_some() {
if comment_text == "# isort: on" {
@@ -119,7 +122,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
off = None;
}
} else {
if comment_text.contains("isort: skip") {
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
exclusions.insert(start.row());
} else if comment_text == "# isort: off" {
off = Some(start);

View File

@@ -26,10 +26,10 @@ pub enum UnittestAssert {
ItemsEqual,
Less,
LessEqual,
ListEqual,
MultiLineEqual,
NotAlmostEqual,
NotAlmostEquals,
NotContains,
NotEqual,
NotEquals,
NotIn,
@@ -41,8 +41,10 @@ pub enum UnittestAssert {
RaisesRegexp,
Regex,
RegexpMatches,
SequenceEqual,
SetEqual,
True,
TupleEqual,
Underscore,
}
@@ -66,10 +68,10 @@ impl std::fmt::Display for UnittestAssert {
UnittestAssert::ItemsEqual => write!(f, "assertItemsEqual"),
UnittestAssert::Less => write!(f, "assertLess"),
UnittestAssert::LessEqual => write!(f, "assertLessEqual"),
UnittestAssert::ListEqual => write!(f, "assertListEqual"),
UnittestAssert::MultiLineEqual => write!(f, "assertMultiLineEqual"),
UnittestAssert::NotAlmostEqual => write!(f, "assertNotAlmostEqual"),
UnittestAssert::NotAlmostEquals => write!(f, "assertNotAlmostEquals"),
UnittestAssert::NotContains => write!(f, "assertNotContains"),
UnittestAssert::NotEqual => write!(f, "assertNotEqual"),
UnittestAssert::NotEquals => write!(f, "assertNotEquals"),
UnittestAssert::NotIn => write!(f, "assertNotIn"),
@@ -81,8 +83,10 @@ impl std::fmt::Display for UnittestAssert {
UnittestAssert::RaisesRegexp => write!(f, "assertRaisesRegexp"),
UnittestAssert::Regex => write!(f, "assertRegex"),
UnittestAssert::RegexpMatches => write!(f, "assertRegexpMatches"),
UnittestAssert::SequenceEqual => write!(f, "assertSequenceEqual"),
UnittestAssert::SetEqual => write!(f, "assertSetEqual"),
UnittestAssert::True => write!(f, "assertTrue"),
UnittestAssert::TupleEqual => write!(f, "assertTupleEqual"),
UnittestAssert::Underscore => write!(f, "assert_"),
}
}
@@ -110,10 +114,10 @@ impl TryFrom<&str> for UnittestAssert {
"assertItemsEqual" => Ok(UnittestAssert::ItemsEqual),
"assertLess" => Ok(UnittestAssert::Less),
"assertLessEqual" => Ok(UnittestAssert::LessEqual),
"assertListEqual" => Ok(UnittestAssert::ListEqual),
"assertMultiLineEqual" => Ok(UnittestAssert::MultiLineEqual),
"assertNotAlmostEqual" => Ok(UnittestAssert::NotAlmostEqual),
"assertNotAlmostEquals" => Ok(UnittestAssert::NotAlmostEquals),
"assertNotContains" => Ok(UnittestAssert::NotContains),
"assertNotEqual" => Ok(UnittestAssert::NotEqual),
"assertNotEquals" => Ok(UnittestAssert::NotEquals),
"assertNotIn" => Ok(UnittestAssert::NotIn),
@@ -125,8 +129,10 @@ impl TryFrom<&str> for UnittestAssert {
"assertRaisesRegexp" => Ok(UnittestAssert::RaisesRegexp),
"assertRegex" => Ok(UnittestAssert::Regex),
"assertRegexpMatches" => Ok(UnittestAssert::RegexpMatches),
"assertSequenceEqual" => Ok(UnittestAssert::SequenceEqual),
"assertSetEqual" => Ok(UnittestAssert::SetEqual),
"assertTrue" => Ok(UnittestAssert::True),
"assertTupleEqual" => Ok(UnittestAssert::TupleEqual),
"assert_" => Ok(UnittestAssert::Underscore),
_ => Err(format!("Unknown unittest assert method: {value}")),
}
@@ -190,10 +196,10 @@ impl UnittestAssert {
UnittestAssert::ItemsEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::Less => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::LessEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::ListEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::MultiLineEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::NotAlmostEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::NotAlmostEquals => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::NotContains => Arguments::new(vec!["container", "member"], vec!["msg"]),
UnittestAssert::NotEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::NotEquals => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::NotIn => Arguments::new(vec!["member", "container"], vec!["msg"]),
@@ -205,8 +211,10 @@ impl UnittestAssert {
UnittestAssert::RaisesRegexp => Arguments::new(vec!["exception", "regex"], vec!["msg"]),
UnittestAssert::Regex => Arguments::new(vec!["text", "regex"], vec!["msg"]),
UnittestAssert::RegexpMatches => Arguments::new(vec!["text", "regex"], vec!["msg"]),
UnittestAssert::SetEqual => Arguments::new(vec!["set1", "set2"], vec!["msg"]),
UnittestAssert::SequenceEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::SetEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::True => Arguments::new(vec!["expr"], vec!["msg"]),
UnittestAssert::TupleEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
UnittestAssert::Underscore => Arguments::new(vec!["expr"], vec!["msg"]),
}
}

View File

@@ -120,7 +120,7 @@ pub fn twisted_arms_in_ifexpr(
}
let mut check = Check::new(
violations::NegateEqualOp(
violations::IfExprWithTwistedArms(
unparse_expr(body, checker.style),
unparse_expr(orelse, checker.style),
),

View File

@@ -3,7 +3,7 @@ source: src/flake8_simplify/mod.rs
expression: checks
---
- kind:
NegateEqualOp:
IfExprWithTwistedArms:
- b
- a
location:
@@ -12,10 +12,17 @@ expression: checks
end_location:
row: 1
column: 21
fix: ~
fix:
content: a if a else b
location:
row: 1
column: 4
end_location:
row: 1
column: 21
parent: ~
- kind:
NegateEqualOp:
IfExprWithTwistedArms:
- b + c
- a
location:
@@ -24,6 +31,13 @@ expression: checks
end_location:
row: 3
column: 25
fix: ~
fix:
content: a if a else b + c
location:
row: 3
column: 4
end_location:
row: 3
column: 25
parent: ~

View File

@@ -5,35 +5,35 @@ expression: checks
- kind:
UnsortedImports: ~
location:
row: 7
row: 12
column: 0
end_location:
row: 8
row: 14
column: 0
fix:
content: "import sys\n\n"
content: " import abc\n import collections\n"
location:
row: 7
row: 12
column: 0
end_location:
row: 8
row: 14
column: 0
parent: ~
- kind:
UnsortedImports: ~
location:
row: 9
row: 19
column: 0
end_location:
row: 11
row: 21
column: 0
fix:
content: "import abc\nimport collections\n"
content: " import abc\n import collections\n"
location:
row: 9
row: 19
column: 0
end_location:
row: 11
row: 21
column: 0
parent: ~

View File

@@ -5,7 +5,6 @@ use std::sync::mpsc::channel;
use ::ruff::autofix::fixer;
use ::ruff::cli::{extract_log_level, Cli, Overrides};
use ::ruff::commands;
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::printer::{Printer, Violations};
use ::ruff::resolver::{resolve_settings, FileDiscovery, PyprojectDiscovery, Relativity};
@@ -14,22 +13,29 @@ use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use ::ruff::{commands, one_time_warning};
use anyhow::Result;
use clap::{CommandFactory, Parser};
use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
use ruff::one_time_warning;
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(
isolated: bool,
config: Option<&Path>,
overrides: &Overrides,
stdin_filename: Option<&Path>,
) -> Result<PyprojectDiscovery> {
if let Some(pyproject) = config {
// First priority: the user specified a `pyproject.toml` file. Use that
if isolated {
// First priority: if we're running in isolated mode, use the default settings.
let mut config = Configuration::default();
config.apply(overrides.clone());
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = config {
// Second priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(pyproject, &Relativity::Cwd, Some(overrides))?;
@@ -39,7 +45,7 @@ fn resolve(
.as_ref()
.unwrap_or(&path_dedot::CWD.as_path()),
)? {
// Second priority: find a `pyproject.toml` file in either an ancestor of
// Third priority: find a `pyproject.toml` file in either an ancestor of
// `stdin_filename` (if set) or the current working path all paths relative to
// that directory. (With `Strategy::Hierarchical`, we'll end up finding
// the "closest" `pyproject.toml` file for every Python file later on,
@@ -47,7 +53,7 @@ fn resolve(
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
Ok(PyprojectDiscovery::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Third priority: find a user-specific `pyproject.toml`, but resolve all paths
// Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
@@ -59,7 +65,6 @@ fn resolve(
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let mut config = Configuration::default();
// Apply command-line options that override defaults.
config.apply(overrides.clone());
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
Ok(PyprojectDiscovery::Hierarchical(settings))
@@ -84,6 +89,7 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let pyproject_strategy = resolve(
cli.isolated,
cli.config.as_deref(),
&overrides,
cli.stdin_filename.as_deref(),

View File

@@ -1,8 +1,6 @@
use std::string::ToString;
use rustpython_parser::ast::{
Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use rustpython_parser::ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::except_range;
use crate::ast::types::{Binding, Range, Scope, ScopeKind};
@@ -70,59 +68,6 @@ pub fn default_except_not_last(
None
}
#[derive(Debug, PartialEq)]
enum DictionaryKey<'a> {
Constant(&'a Constant),
Variable(&'a str),
}
fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
match &expr.node {
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value)),
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
_ => None,
}
}
/// F601, F602
pub fn repeated_keys(
keys: &[Expr],
check_repeated_literals: bool,
check_repeated_variables: bool,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let num_keys = keys.len();
for i in 0..num_keys {
let k1 = &keys[i];
let v1 = convert_to_value(k1);
for k2 in keys.iter().take(num_keys).skip(i + 1) {
let v2 = convert_to_value(k2);
match (&v1, &v2) {
(Some(DictionaryKey::Constant(v1)), Some(DictionaryKey::Constant(v2))) => {
if check_repeated_literals && v1 == v2 {
checks.push(Check::new(
violations::MultiValueRepeatedKeyLiteral,
Range::from_located(k2),
));
}
}
(Some(DictionaryKey::Variable(v1)), Some(DictionaryKey::Variable(v2))) => {
if check_repeated_variables && v1 == v2 {
checks.push(Check::new(
violations::MultiValueRepeatedKeyVariable((*v2).to_string()),
Range::from_located(k2),
));
}
}
_ => {}
}
}
}
checks
}
/// F621, F622
pub fn starred_expressions(
elts: &[Expr],

View File

@@ -4,6 +4,7 @@ pub use if_tuple::if_tuple;
pub use invalid_literal_comparisons::invalid_literal_comparison;
pub use invalid_print_syntax::invalid_print_syntax;
pub use raise_not_implemented::raise_not_implemented;
pub use repeated_keys::repeated_keys;
pub(crate) use strings::{
percent_format_expected_mapping, percent_format_expected_sequence,
percent_format_extra_named_arguments, percent_format_missing_arguments,
@@ -21,6 +22,7 @@ mod if_tuple;
mod invalid_literal_comparisons;
mod invalid_print_syntax;
mod raise_not_implemented;
mod repeated_keys;
mod strings;
mod unused_annotation;
mod unused_variable;

View File

@@ -0,0 +1,93 @@
use std::hash::{BuildHasherDefault, Hash};
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, ExprKind};
use crate::ast::comparable::{ComparableConstant, ComparableExpr};
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::registry::{Check, CheckCode};
use crate::violations;
#[derive(Debug, Eq, PartialEq, Hash)]
enum DictionaryKey<'a> {
Constant(ComparableConstant<'a>),
Variable(&'a str),
}
fn into_dictionary_key(expr: &Expr) -> Option<DictionaryKey> {
match &expr.node {
ExprKind::Constant { value, .. } => Some(DictionaryKey::Constant(value.into())),
ExprKind::Name { id, .. } => Some(DictionaryKey::Variable(id)),
_ => None,
}
}
/// F601, F602
pub fn repeated_keys(checker: &mut Checker, keys: &[Expr], values: &[Expr]) {
// Generate a map from key to (index, value).
let mut seen: FxHashMap<DictionaryKey, FxHashSet<ComparableExpr>> =
FxHashMap::with_capacity_and_hasher(keys.len(), BuildHasherDefault::default());
// Detect duplicate keys.
for (i, key) in keys.iter().enumerate() {
if let Some(key) = into_dictionary_key(key) {
if let Some(seen_values) = seen.get_mut(&key) {
match key {
DictionaryKey::Constant(..) => {
if checker.settings.enabled.contains(&CheckCode::F601) {
let comparable_value: ComparableExpr = (&values[i]).into();
let is_duplicate_value = seen_values.contains(&comparable_value);
let mut check = Check::new(
violations::MultiValueRepeatedKeyLiteral(
unparse_expr(&keys[i], checker.style),
is_duplicate_value,
),
Range::from_located(&keys[i]),
);
if is_duplicate_value {
if checker.patch(&CheckCode::F601) {
check.amend(Fix::deletion(
values[i - 1].end_location.unwrap(),
values[i].end_location.unwrap(),
));
}
} else {
seen_values.insert(comparable_value);
}
checker.checks.push(check);
}
}
DictionaryKey::Variable(key) => {
if checker.settings.enabled.contains(&CheckCode::F602) {
let comparable_value: ComparableExpr = (&values[i]).into();
let is_duplicate_value = seen_values.contains(&comparable_value);
let mut check = Check::new(
violations::MultiValueRepeatedKeyVariable(
key.to_string(),
is_duplicate_value,
),
Range::from_located(&keys[i]),
);
if is_duplicate_value {
if checker.patch(&CheckCode::F602) {
check.amend(Fix::deletion(
values[i - 1].end_location.unwrap(),
values[i].end_location.unwrap(),
));
}
} else {
seen_values.insert(comparable_value);
}
checker.checks.push(check);
}
}
}
} else {
seen.insert(key, FxHashSet::from_iter([(&values[i]).into()]));
}
}
}
}

View File

@@ -3,7 +3,9 @@ source: src/pyflakes/mod.rs
expression: checks
---
- kind:
MultiValueRepeatedKeyLiteral: ~
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 3
column: 4
@@ -13,7 +15,9 @@ expression: checks
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral: ~
MultiValueRepeatedKeyLiteral:
- "1"
- false
location:
row: 9
column: 4
@@ -23,7 +27,9 @@ expression: checks
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral: ~
MultiValueRepeatedKeyLiteral:
- "b\"123\""
- false
location:
row: 11
column: 4
@@ -32,4 +38,238 @@ expression: checks
column: 10
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 16
column: 4
end_location:
row: 16
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 17
column: 4
end_location:
row: 17
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 18
column: 4
end_location:
row: 18
column: 7
fix:
content: ""
location:
row: 17
column: 10
end_location:
row: 18
column: 10
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 23
column: 4
end_location:
row: 23
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 24
column: 4
end_location:
row: 24
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 25
column: 4
end_location:
row: 25
column: 7
fix:
content: ""
location:
row: 24
column: 10
end_location:
row: 25
column: 10
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 26
column: 4
end_location:
row: 26
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 31
column: 4
end_location:
row: 31
column: 7
fix:
content: ""
location:
row: 30
column: 10
end_location:
row: 31
column: 10
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 32
column: 4
end_location:
row: 32
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 33
column: 4
end_location:
row: 33
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 34
column: 4
end_location:
row: 34
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 41
column: 4
end_location:
row: 41
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- false
location:
row: 43
column: 4
end_location:
row: 43
column: 7
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 45
column: 4
end_location:
row: 45
column: 7
fix:
content: ""
location:
row: 44
column: 8
end_location:
row: 45
column: 10
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 49
column: 13
end_location:
row: 49
column: 16
fix:
content: ""
location:
row: 49
column: 11
end_location:
row: 49
column: 19
parent: ~
- kind:
MultiValueRepeatedKeyLiteral:
- "\"a\""
- true
location:
row: 50
column: 21
end_location:
row: 50
column: 24
fix:
content: ""
location:
row: 50
column: 19
end_location:
row: 50
column: 27
parent: ~

View File

@@ -3,7 +3,9 @@ source: src/pyflakes/mod.rs
expression: checks
---
- kind:
MultiValueRepeatedKeyVariable: a
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 5
column: 4
@@ -12,4 +14,250 @@ expression: checks
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 11
column: 4
end_location:
row: 11
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 12
column: 4
end_location:
row: 12
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 13
column: 4
end_location:
row: 13
column: 5
fix:
content: ""
location:
row: 12
column: 8
end_location:
row: 13
column: 8
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 18
column: 4
end_location:
row: 18
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 19
column: 4
end_location:
row: 19
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 20
column: 4
end_location:
row: 20
column: 5
fix:
content: ""
location:
row: 19
column: 8
end_location:
row: 20
column: 8
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 21
column: 4
end_location:
row: 21
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 26
column: 4
end_location:
row: 26
column: 5
fix:
content: ""
location:
row: 25
column: 8
end_location:
row: 26
column: 8
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 27
column: 4
end_location:
row: 27
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 28
column: 4
end_location:
row: 28
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 29
column: 4
end_location:
row: 29
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 35
column: 4
end_location:
row: 35
column: 5
fix:
content: ""
location:
row: 34
column: 10
end_location:
row: 35
column: 8
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 37
column: 4
end_location:
row: 37
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 39
column: 4
end_location:
row: 39
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- false
location:
row: 41
column: 4
end_location:
row: 41
column: 5
fix: ~
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 44
column: 11
end_location:
row: 44
column: 12
fix:
content: ""
location:
row: 44
column: 9
end_location:
row: 44
column: 15
parent: ~
- kind:
MultiValueRepeatedKeyVariable:
- a
- true
location:
row: 45
column: 17
end_location:
row: 45
column: 18
fix:
content: ""
location:
row: 45
column: 15
end_location:
row: 45
column: 21
parent: ~

View File

@@ -1,24 +1,23 @@
use anyhow::{bail, Result};
use log::error;
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Location, Stmt, StmtKind};
use log::debug;
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::match_module_member;
use crate::ast::helpers::{create_expr, create_stmt, match_module_member, unparse_stmt};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::registry::Check;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::violations;
/// Return the typename, args, keywords and mother class
/// Return the typename, args, keywords, and base class.
fn match_named_tuple_assign<'a>(
checker: &Checker,
targets: &'a [Expr],
value: &'a Expr,
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a ExprKind)> {
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> {
let target = targets.get(0)?;
let ExprKind::Name { id: typename, .. } = &target.node else {
return None;
@@ -39,43 +38,25 @@ fn match_named_tuple_assign<'a>(
) {
return None;
}
Some((typename, args, keywords, &func.node))
Some((typename, args, keywords, func))
}
/// Generate a `StmtKind::AnnAssign` representing the provided property
/// definition.
fn create_property_assignment_stmt(
property: &str,
annotation: &ExprKind,
value: Option<&ExprKind>,
annotation: &Expr,
value: Option<&Expr>,
) -> Stmt {
Stmt::new(
Location::default(),
Location::default(),
StmtKind::AnnAssign {
target: Box::new(Expr::new(
Location::default(),
Location::default(),
ExprKind::Name {
id: property.to_string(),
ctx: ExprContext::Load,
},
)),
annotation: Box::new(Expr::new(
Location::default(),
Location::default(),
annotation.clone(),
)),
value: value.map(|v| {
Box::new(Expr::new(
Location::default(),
Location::default(),
v.clone(),
))
}),
simple: 1,
},
)
create_stmt(StmtKind::AnnAssign {
target: Box::new(create_expr(ExprKind::Name {
id: property.to_string(),
ctx: ExprContext::Load,
})),
annotation: Box::new(annotation.clone()),
value: value.map(|value| Box::new(value.clone())),
simple: 1,
})
}
/// Match the `defaults` keyword in a `NamedTuple(...)` call.
@@ -105,7 +86,6 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
let ExprKind::List { elts, .. } = &fields.node else {
bail!("Expected argument to be `ExprKind::List`");
};
let padded_defaults = if elts.len() >= defaults.len() {
std::iter::repeat(None)
.take(elts.len() - defaults.len())
@@ -132,9 +112,7 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
bail!("Invalid property name: {}", property)
}
Ok(create_property_assignment_stmt(
property,
&annotation.node,
default.map(|d| &d.node),
property, annotation, default,
))
})
.collect()
@@ -142,35 +120,26 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result<Vec<S
/// Generate a `StmtKind:ClassDef` statement based on the provided body and
/// keywords.
fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &ExprKind) -> Stmt {
Stmt::new(
Location::default(),
Location::default(),
StmtKind::ClassDef {
name: typename.to_string(),
bases: vec![Expr::new(
Location::default(),
Location::default(),
base_class.clone(),
)],
keywords: vec![],
body,
decorator_list: vec![],
},
)
fn create_class_def_stmt(typename: &str, body: Vec<Stmt>, base_class: &Expr) -> Stmt {
create_stmt(StmtKind::ClassDef {
name: typename.to_string(),
bases: vec![base_class.clone()],
keywords: vec![],
body,
decorator_list: vec![],
})
}
/// Generate a `Fix` to convert a `NamedTuple` assignment to a class definition.
fn convert_to_class(
stmt: &Stmt,
typename: &str,
body: Vec<Stmt>,
base_class: &ExprKind,
base_class: &Expr,
stylist: &SourceCodeStyleDetector,
) -> Fix {
let mut generator: SourceCodeGenerator = stylist.into();
generator.unparse_stmt(&create_class_def_stmt(typename, body, base_class));
Fix::replacement(
generator.generate(),
unparse_stmt(&create_class_def_stmt(typename, body, base_class), stylist),
stmt.location,
stmt.end_location.unwrap(),
)
@@ -188,26 +157,25 @@ pub fn convert_named_tuple_functional_to_class(
{
return;
};
match match_defaults(keywords) {
Ok(defaults) => match create_properties_from_args(args, defaults) {
let mut check = Check::new(
violations::ConvertNamedTupleFunctionalToClass(typename.to_string()),
Range::from_located(stmt),
);
if checker.patch(check.kind.code()) {
match match_defaults(keywords)
.and_then(|defaults| create_properties_from_args(args, defaults))
{
Ok(properties) => {
let mut check = Check::new(
violations::ConvertNamedTupleFunctionalToClass(typename.to_string()),
Range::from_located(stmt),
);
if checker.patch(check.kind.code()) {
check.amend(convert_to_class(
stmt,
typename,
properties,
base_class,
checker.style,
));
}
checker.checks.push(check);
check.amend(convert_to_class(
stmt,
typename,
properties,
base_class,
checker.style,
));
}
Err(err) => error!("Failed to create properties: {err}"),
},
Err(err) => error!("Failed to parse defaults: {err}"),
Err(err) => debug!("Skipping ineligible `NamedTuple` \"{typename}\": {err}"),
};
}
checker.checks.push(check);
}

View File

@@ -1,17 +1,14 @@
use anyhow::{bail, Result};
use log::error;
use rustpython_ast::{
Constant, Expr, ExprContext, ExprKind, Keyword, KeywordData, Location, Stmt, StmtKind,
};
use log::debug;
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::helpers::match_module_member;
use crate::ast::helpers::{create_expr, create_stmt, match_module_member, unparse_stmt};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::registry::Check;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::violations;
@@ -21,7 +18,7 @@ fn match_typed_dict_assign<'a>(
checker: &Checker,
targets: &'a [Expr],
value: &'a Expr,
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a ExprKind)> {
) -> Option<(&'a str, &'a [Expr], &'a [Keyword], &'a Expr)> {
let target = targets.get(0)?;
let ExprKind::Name { id: class_name, .. } = &target.node else {
return None;
@@ -42,38 +39,26 @@ fn match_typed_dict_assign<'a>(
) {
return None;
}
Some((class_name, args, keywords, &func.node))
Some((class_name, args, keywords, func))
}
/// Generate a `StmtKind::AnnAssign` representing the provided property
/// definition.
fn create_property_assignment_stmt(property: &str, annotation: &ExprKind) -> Stmt {
Stmt::new(
Location::default(),
Location::default(),
StmtKind::AnnAssign {
target: Box::new(Expr::new(
Location::default(),
Location::default(),
ExprKind::Name {
id: property.to_string(),
ctx: ExprContext::Load,
},
)),
annotation: Box::new(Expr::new(
Location::default(),
Location::default(),
annotation.clone(),
)),
value: None,
simple: 1,
},
)
create_stmt(StmtKind::AnnAssign {
target: Box::new(create_expr(ExprKind::Name {
id: property.to_string(),
ctx: ExprContext::Load,
})),
annotation: Box::new(create_expr(annotation.clone())),
value: None,
simple: 1,
})
}
/// Generate a `StmtKind::Pass` statement.
fn create_pass_stmt() -> Stmt {
Stmt::new(Location::default(), Location::default(), StmtKind::Pass)
create_stmt(StmtKind::Pass)
}
/// Generate a `StmtKind:ClassDef` statement based on the provided body,
@@ -81,35 +66,23 @@ fn create_pass_stmt() -> Stmt {
fn create_class_def_stmt(
class_name: &str,
body: Vec<Stmt>,
total_keyword: Option<KeywordData>,
base_class: &ExprKind,
total_keyword: Option<&Keyword>,
base_class: &Expr,
) -> Stmt {
let keywords = match total_keyword {
Some(keyword) => vec![Keyword::new(
Location::default(),
Location::default(),
keyword,
)],
Some(keyword) => vec![keyword.clone()],
None => vec![],
};
Stmt::new(
Location::default(),
Location::default(),
StmtKind::ClassDef {
name: class_name.to_string(),
bases: vec![Expr::new(
Location::default(),
Location::default(),
base_class.clone(),
)],
keywords,
body,
decorator_list: vec![],
},
)
create_stmt(StmtKind::ClassDef {
name: class_name.to_string(),
bases: vec![base_class.clone()],
keywords,
body,
decorator_list: vec![],
})
}
fn get_properties_from_dict_literal(keys: &[Expr], values: &[Expr]) -> Result<Vec<Stmt>> {
fn properties_from_dict_literal(keys: &[Expr], values: &[Expr]) -> Result<Vec<Stmt>> {
keys.iter()
.zip(values.iter())
.map(|(key, value)| match &key.node {
@@ -120,7 +93,7 @@ fn get_properties_from_dict_literal(keys: &[Expr], values: &[Expr]) -> Result<Ve
if IDENTIFIER_REGEX.is_match(property) && !KWLIST.contains(&property.as_str()) {
Ok(create_property_assignment_stmt(property, &value.node))
} else {
bail!("Invalid property name: {}", property)
bail!("Property name is not valid identifier: {}", property)
}
}
_ => bail!("Expected `key` to be `Constant::Str`"),
@@ -128,18 +101,18 @@ fn get_properties_from_dict_literal(keys: &[Expr], values: &[Expr]) -> Result<Ve
.collect()
}
fn get_properties_from_dict_call(func: &Expr, keywords: &[Keyword]) -> Result<Vec<Stmt>> {
fn properties_from_dict_call(func: &Expr, keywords: &[Keyword]) -> Result<Vec<Stmt>> {
let ExprKind::Name { id, .. } = &func.node else {
bail!("Expected `func` to be `ExprKind::Name`")
};
if id != "dict" {
bail!("Expected `id` to be `\"dict\"`")
}
get_properties_from_keywords(keywords)
properties_from_keywords(keywords)
}
// Deprecated in Python 3.11, removed in Python 3.13.
fn get_properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
fn properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
keywords
.iter()
.map(|keyword| {
@@ -156,36 +129,40 @@ fn get_properties_from_keywords(keywords: &[Keyword]) -> Result<Vec<Stmt>> {
}
// The only way to have the `total` keyword is to use the args version, like:
// (`TypedDict('name', {'a': int}, total=True)`)
fn get_total_from_only_keyword(keywords: &[Keyword]) -> Option<&KeywordData> {
// ```
// TypedDict('name', {'a': int}, total=True)
// ```
fn match_total_from_only_keyword(keywords: &[Keyword]) -> Option<&Keyword> {
let keyword = keywords.get(0)?;
let arg = &keyword.node.arg.as_ref()?;
match arg.as_str() {
"total" => Some(&keyword.node),
"total" => Some(keyword),
_ => None,
}
}
fn get_properties_and_total(
args: &[Expr],
keywords: &[Keyword],
) -> Result<(Vec<Stmt>, Option<KeywordData>)> {
fn match_properties_and_total<'a>(
args: &'a [Expr],
keywords: &'a [Keyword],
) -> Result<(Vec<Stmt>, Option<&'a Keyword>)> {
// We don't have to manage the hybrid case because it's not possible to have a
// dict and keywords. For example, the following is illegal:
// MyType = TypedDict('MyType', {'a': int, 'b': str}, a=int, b=str)
// ```
// MyType = TypedDict('MyType', {'a': int, 'b': str}, a=int, b=str)
// ```
if let Some(dict) = args.get(1) {
let total = get_total_from_only_keyword(keywords).cloned();
let total = match_total_from_only_keyword(keywords);
match &dict.node {
ExprKind::Dict { keys, values } => {
Ok((get_properties_from_dict_literal(keys, values)?, total))
Ok((properties_from_dict_literal(keys, values)?, total))
}
ExprKind::Call { func, keywords, .. } => {
Ok((get_properties_from_dict_call(func, keywords)?, total))
Ok((properties_from_dict_call(func, keywords)?, total))
}
_ => Ok((vec![create_pass_stmt()], total)),
}
} else if !keywords.is_empty() {
Ok((get_properties_from_keywords(keywords)?, None))
Ok((properties_from_keywords(keywords)?, None))
} else {
Ok((vec![create_pass_stmt()], None))
}
@@ -196,19 +173,15 @@ fn convert_to_class(
stmt: &Stmt,
class_name: &str,
body: Vec<Stmt>,
total_keyword: Option<KeywordData>,
base_class: &ExprKind,
total_keyword: Option<&Keyword>,
base_class: &Expr,
stylist: &SourceCodeStyleDetector,
) -> Fix {
let mut generator: SourceCodeGenerator = stylist.into();
generator.unparse_stmt(&create_class_def_stmt(
class_name,
body,
total_keyword,
base_class,
));
Fix::replacement(
generator.generate(),
unparse_stmt(
&create_class_def_stmt(class_name, body, total_keyword, base_class),
stylist,
),
stmt.location,
stmt.end_location.unwrap(),
)
@@ -226,26 +199,25 @@ pub fn convert_typed_dict_functional_to_class(
{
return;
};
let (body, total_keyword) = match get_properties_and_total(args, keywords) {
Err(err) => {
error!("Failed to parse TypedDict: {err}");
return;
}
Ok(args) => args,
};
let mut check = Check::new(
violations::ConvertTypedDictFunctionalToClass(class_name.to_string()),
Range::from_located(stmt),
);
if checker.patch(check.kind.code()) {
check.amend(convert_to_class(
stmt,
class_name,
body,
total_keyword,
base_class,
checker.style,
));
match match_properties_and_total(args, keywords) {
Ok((body, total_keyword)) => {
check.amend(convert_to_class(
stmt,
class_name,
body,
total_keyword,
base_class,
checker.style,
));
}
Err(err) => debug!("Skipping ineligible `TypedDict` \"{class_name}\": {err}"),
};
}
checker.checks.push(check);
}

View File

@@ -138,6 +138,16 @@ expression: checks
row: 24
column: 65
parent: ~
- kind:
ConvertTypedDictFunctionalToClass: MyType9
location:
row: 27
column: 0
end_location:
row: 27
column: 55
fix: ~
parent: ~
- kind:
ConvertTypedDictFunctionalToClass: MyType10
location:

View File

@@ -53,4 +53,14 @@ expression: checks
row: 15
column: 56
parent: ~
- kind:
ConvertNamedTupleFunctionalToClass: NT4
location:
row: 18
column: 0
end_location:
row: 22
column: 1
fix: ~
parent: ~

View File

@@ -651,29 +651,50 @@ impl AlwaysAutofixableViolation for FStringMissingPlaceholders {
}
define_violation!(
pub struct MultiValueRepeatedKeyLiteral;
pub struct MultiValueRepeatedKeyLiteral(pub String, pub bool);
);
impl Violation for MultiValueRepeatedKeyLiteral {
fn message(&self) -> String {
"Dictionary key literal repeated".to_string()
let MultiValueRepeatedKeyLiteral(name, ..) = self;
format!("Dictionary key literal `{name}` repeated")
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let MultiValueRepeatedKeyLiteral(.., repeated_value) = self;
if *repeated_value {
Some(|MultiValueRepeatedKeyLiteral(name, ..)| {
format!("Remove repeated key literal `{name}`")
})
} else {
None
}
}
fn placeholder() -> Self {
MultiValueRepeatedKeyLiteral
MultiValueRepeatedKeyLiteral("...".to_string(), true)
}
}
define_violation!(
pub struct MultiValueRepeatedKeyVariable(pub String);
pub struct MultiValueRepeatedKeyVariable(pub String, pub bool);
);
impl Violation for MultiValueRepeatedKeyVariable {
fn message(&self) -> String {
let MultiValueRepeatedKeyVariable(name) = self;
let MultiValueRepeatedKeyVariable(name, ..) = self;
format!("Dictionary key `{name}` repeated")
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let MultiValueRepeatedKeyVariable(.., repeated_value) = self;
if *repeated_value {
Some(|MultiValueRepeatedKeyVariable(name, ..)| format!("Remove repeated key `{name}`"))
} else {
None
}
}
fn placeholder() -> Self {
MultiValueRepeatedKeyVariable("...".to_string())
MultiValueRepeatedKeyVariable("...".to_string(), true)
}
}