Compare commits

...

38 Commits

Author SHA1 Message Date
Charlie Marsh
a710e35ebc Bump version to 0.0.170 2022-12-08 11:36:24 -05:00
Charlie Marsh
49df43bb78 Use single newlines in .pyi import sorting (#1142) 2022-12-08 11:34:41 -05:00
Charlie Marsh
e338d9acbe Remove 'consider' language from check messages (#1135) 2022-12-07 20:10:36 -05:00
Charlie Marsh
5c8655f479 Bump ruff_macros to 0.0.169 2022-12-07 19:10:16 -05:00
Charlie Marsh
60987888a2 Re-increase max iterations to 100 2022-12-07 19:10:03 -05:00
Charlie Marsh
a81581c781 Bump ruff_macros to 0.0.168 2022-12-07 19:08:18 -05:00
Charlie Marsh
3152dd7a8e Don't prompt users to --fix if they ran with --fix (#1133) 2022-12-07 19:07:51 -05:00
Charlie Marsh
528416f07a Rename I252 to TID252; add redirects for all renamed codes (#1129) 2022-12-07 15:12:22 -05:00
Charlie Marsh
4405a6a903 Bump version to 0.0.168 2022-12-07 13:18:40 -05:00
Charlie Marsh
35fa2a3c32 Convert more BTree usages to Fx (#1112) 2022-12-07 12:21:12 -05:00
Charlie Marsh
bb67fbb73a Implement unused argument detection (ARG) (#1126)
Detect unused arguments
2022-12-07 12:15:41 -05:00
Charlie Marsh
d698c6123e Bump version to 0.0.167 2022-12-07 10:37:31 -05:00
Charlie Marsh
9579faffa8 Avoid flagging bare exception issues when exception is re-raised (#1124) 2022-12-07 10:37:08 -05:00
Phillip Verheyden
9c6e8c7644 Auto-generate the rules table of contents (#1121) 2022-12-07 10:03:42 -05:00
Charlie Marsh
7abecd4f0e Implement B905 (#1122) 2022-12-07 10:01:24 -05:00
Phillip Verheyden
b8ff209af8 Encode prefixes in README headings not just in TOC (#1109) 2022-12-07 09:24:49 -05:00
Jeong YunWon
c5451cd8ad Reduce indents (#1116) 2022-12-07 09:20:33 -05:00
Jonathan Plasse
92b9ab3010 Add aiter() and anext() to BUILTINS (#1118) 2022-12-07 09:20:06 -05:00
Edgar R. M
f2ac8c4ec2 Add flake8-import-conventions to TOC in readme (#1114) 2022-12-06 21:27:04 -05:00
Charlie Marsh
80e2f0c92e Bump version to 0.0.166 2022-12-06 16:06:19 -05:00
Edgar R. M
ea550abd3c Implement flake8-import-conventions (#1098) 2022-12-06 16:01:17 -05:00
Charlie Marsh
5c26777e4c Avoid flagging ANN errors in @overload implementations (#1110) 2022-12-06 12:46:38 -05:00
Laurent Baillet
6eb6b6eede Update readme in order to match pylint prefixes (#1105) 2022-12-06 08:59:45 -05:00
Charlie Marsh
f1d3e3698a Bump version to 0.0.165 2022-12-06 00:03:30 -05:00
Charlie Marsh
080411bc89 Re-create ruff snapshots 2022-12-06 00:03:14 -05:00
Charlie Marsh
f2ad915224 Bump version to 0.0.164 2022-12-05 23:37:22 -05:00
Charlie Marsh
71543eeabc Rename rules mod to ruff (#1104) 2022-12-05 23:35:36 -05:00
Charlie Marsh
44025f1c92 Improve F841's Flake8 parity for unpacking assignments (#1103) 2022-12-05 23:34:40 -05:00
Charlie Marsh
971bf6d232 Run cargo fmt 2022-12-05 23:01:47 -05:00
Charlie Marsh
0acc47386a Use pyproject.toml parent as project root when explicitly provided (#1101) 2022-12-05 23:00:59 -05:00
Reiner Gerecke
982ac6b0ad Auto-generate options in README from field attributes (#1015) 2022-12-05 22:34:40 -05:00
Charlie Marsh
541440f7a8 Re-support F841 detection for single context managers (#1099) 2022-12-05 22:09:45 -05:00
Charlie Marsh
66dde46e03 Track nested imports without column number detection (#1097) 2022-12-05 21:16:44 -05:00
Charlie Marsh
1339e2a002 Bump version to 0.0.163 2022-12-05 20:45:24 -05:00
Charlie Marsh
38ad10f60d Treat nested classes and functions as "standard" siblings (#1095) 2022-12-05 20:45:12 -05:00
Charlie Marsh
88e78c5cde Add missing D415 fixture 2022-12-05 20:42:21 -05:00
Charlie Marsh
436aeed20a Implement autofix for D400 and D415 (#1094) 2022-12-05 20:24:56 -05:00
Charlie Marsh
b94169a8bb Don't autofix D210 by introducing a syntax error (#1093) 2022-12-05 19:13:22 -05:00
160 changed files with 6051 additions and 1669 deletions

View File

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

View File

@@ -105,6 +105,8 @@ You may also want to add the new configuration option to the `flake8-to-ruff` to
responsible for converting `flake8` configuration files to Ruff's TOML format. This logic
lives in `flake8_to_ruff/src/converter.rs`.
To update the documentation for supported configuration options, run `cargo dev generate-options`.
## Release process
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub

17
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.162-dev.0"
version = "0.0.170-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.162"
version = "0.0.170"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1855,6 +1855,7 @@ dependencies = [
"rayon",
"regex",
"ropey",
"ruff_macros",
"rustc-hash",
"rustpython-ast",
"rustpython-common",
@@ -1873,7 +1874,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.162"
version = "0.0.170"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1889,6 +1890,16 @@ dependencies = [
"strum_macros",
]
[[package]]
name = "ruff_macros"
version = "0.0.170"
dependencies = [
"proc-macro2",
"quote",
"syn",
"textwrap",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.162"
version = "0.0.170"
edition = "2021"
rust-version = "1.65.0"
@@ -41,6 +41,7 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.170", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }

50
LICENSE
View File

@@ -388,6 +388,56 @@ are:
SOFTWARE.
"""
- flake8-import-conventions, licensed as follows:
"""
MIT License
Copyright (c) 2021 João Palmeiro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-unused-arguments, licensed as follows:
"""
MIT License
Copyright (c) 2019 Nathan Hoad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- isort, licensed as follows:
"""
The MIT License (MIT)

779
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -270,6 +270,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -314,6 +315,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -358,6 +360,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -402,6 +405,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -451,6 +455,7 @@ mod tests {
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -533,6 +538,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -583,6 +589,7 @@ mod tests {
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,

View File

@@ -0,0 +1,49 @@
from typing import overload
@overload
def foo(i: int) -> "int":
...
@overload
def foo(i: "str") -> "str":
...
def foo(i):
return i
@overload
def bar(i: int) -> "int":
...
@overload
def bar(i: "str") -> "str":
...
class X:
def bar(i):
return i
# TODO(charlie): This third case should raise an error (as in Mypy), because we have a
# statement between the interfaces and implementation.
@overload
def baz(i: int) -> "int":
...
@overload
def baz(i: "str") -> "str":
...
x = 1
def baz(i):
return i

View File

@@ -53,3 +53,11 @@ try:
raise e
except Exception:
pass
try:
pass
except Exception as e:
raise bad
except BaseException:
pass

View File

@@ -0,0 +1,10 @@
zip()
zip(range(3))
zip("a", "b")
zip("a", "b", *zip("c"))
zip(zip("a"), strict=False)
zip(zip("a", strict=True))
zip(range(3), strict=True)
zip("a", "b", strict=False)
zip("a", "b", "c", strict=True)

View File

@@ -0,0 +1,25 @@
import math # not checked
import altair # unconventional
import dask.array # unconventional
import dask.dataframe # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import dask.array as darray # unconventional
import dask.dataframe as ddf # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import dask.array as da # conventional
import dask.dataframe as dd # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as np # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as nmp # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # not checked
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # not checked
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # not checked
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,137 @@
from abc import abstractmethod
from typing_extensions import override
###
# Unused arguments on functions.
###
def f(self, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
def f(self, x):
...
def f(cls, x):
...
###
# Unused arguments on lambdas.
###
lambda x: print("Hello, world!")
class X:
###
# Unused arguments.
###
def f(self, x):
print("Hello, world!")
def f(self, /, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
@classmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(x):
print("Hello, world!")
###
# Unused arguments attached to empty functions (OK).
###
def f(self, x):
...
def f(self, /, x):
...
def f(cls, x):
...
@classmethod
def f(cls, x):
...
@staticmethod
def f(cls, x):
...
@staticmethod
def f(x):
...
###
# Unused functions attached to abstract methods (OK).
###
@abstractmethod
def f(self, x):
print("Hello, world!")
@abstractmethod
def f(self, /, x):
print("Hello, world!")
@abstractmethod
def f(cls, x):
print("Hello, world!")
@classmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(x):
print("Hello, world!")
###
# Unused functions attached to overrides (OK).
###
@override
def f(self, x):
print("Hello, world!")
@override
def f(self, /, x):
print("Hello, world!")
@override
def f(cls, x):
print("Hello, world!")
@classmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(x):
print("Hello, world!")

View File

@@ -27,3 +27,15 @@ if True:
import os
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
if True:
x = 1
import collections
import typing
def f(): pass

View File

@@ -0,0 +1,41 @@
import a
import b
x = 1
import os
import sys
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
y = 1
import os
import sys
"""Docstring"""
if True:
import os
def f():
pass
if True:
import os
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
if True:
x = 1
import collections
import typing
def f(): pass

View File

@@ -571,3 +571,11 @@ def multiline_trailing_and_leading_space():
"""
pass
@expect('D210: No whitespaces allowed surrounding docstring text')
@expect("D400: First line should end with a period (not '\"')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not '\"')")
def endswith_quote():
"""Whitespace at the end, but also a quote" """

View File

@@ -0,0 +1,73 @@
def f():
"Here's a line without a period"
...
def f():
"""Here's a line without a period"""
...
def f():
"""
Here's a line without a period,
but here's the next line
"""
...
def f():
"""Here's a line without a period"""
...
def f():
"""
Here's a line without a period,
but here's the next line"""
...
def f():
"""
Here's a line without a period,
but here's the next line with trailing space """
...
def f():
r"Here's a line without a period"
...
def f():
r"""Here's a line without a period"""
...
def f():
r"""
Here's a line without a period,
but here's the next line
"""
...
def f():
r"""Here's a line without a period"""
...
def f():
r"""
Here's a line without a period,
but here's the next line"""
...
def f():
r"""
Here's a line without a period,
but here's the next line with trailing space """
...

View File

@@ -65,3 +65,8 @@ def f7():
with connect() as (connection, cursor):
cursor.execute("SELECT * FROM users")
def f8():
with open("file") as f, open("") as ((a, b)):
print("hello")

View File

@@ -0,0 +1,24 @@
def f(tup):
x, y = tup # this does NOT trigger F841
def f():
x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
def f():
(x, y) = coords = 1, 2 # this does NOT trigger F841
if x > 1:
print(coords)
def f():
(x, y) = coords = 1, 2 # this triggers F841 on coords
def f():
coords = (x, y) = 1, 2 # this triggers F841 on coords
def f():
(a, b) = (x, y) = 1, 2 # this triggers F841 on everything

View File

@@ -41,3 +41,9 @@ staticmethod-decorators = ["staticmethod"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"
[tool.ruff.flake8-import-conventions.aliases]
pandas = "pd"
[tool.ruff.flake8-import-conventions.extend-aliases]
"dask.dataframe" = "dd"

View File

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

View File

@@ -8,7 +8,7 @@ use anyhow::Result;
use clap::Parser;
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use ruff::checks::{CheckCode, REDIRECTS};
use ruff::checks::{CheckCode, CODE_REDIRECTS, PREFIX_REDIRECTS};
use strum::IntoEnumIterator;
const FILE: &str = "src/checks_gen.rs";
@@ -39,34 +39,26 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
// Add any aliases (e.g., "U001" to "UP001").
for (alias, check_code) in REDIRECTS.iter() {
// Compute the length of the prefix and suffix for both codes.
let code_str: String = check_code.as_ref().to_string();
let code_prefix_len = code_str
.chars()
.take_while(|char| char.is_alphabetic())
.count();
let code_suffix_len = code_str.len() - code_prefix_len;
let alias_prefix_len = alias
.chars()
.take_while(|char| char.is_alphabetic())
.count();
let alias_suffix_len = alias.len() - alias_prefix_len;
assert_eq!(code_suffix_len, alias_suffix_len);
for i in 0..=code_suffix_len {
let source = code_str[..code_prefix_len + i].to_string();
let destination = alias[..alias_prefix_len + i].to_string();
if source != destination {
prefix_to_codes.insert(
destination,
prefix_to_codes
.get(&source)
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
}
// Add any prefix aliases (e.g., "U" to "UP").
for (alias, source) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&(*source).to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
// Add any check code aliases (e.g., "U001" to "UP001").
for (alias, check_code) in CODE_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&check_code.as_ref().to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {alias:?}"))
.clone(),
);
}
let mut scope = Scope::new();
@@ -76,6 +68,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("AsRefStr")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
@@ -112,10 +105,10 @@ pub fn main(cli: &Cli) -> Result<()> {
.line("#[allow(clippy::match_same_arms)]")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
if let Some(target) = REDIRECTS.get(&prefix.as_str()) {
if let Some(target) = CODE_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been renamed to \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target.as_ref(),
@@ -124,6 +117,18 @@ pub fn main(cli: &Cli) -> Result<()> {
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target,
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
@@ -170,9 +175,9 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push('\n');
output.push_str("use colored::Colorize;");
output.push('\n');
output.push_str("use serde::{{Serialize, Deserialize}};");
output.push_str("use serde::{Deserialize, Serialize};");
output.push('\n');
output.push_str("use strum_macros::EnumString;");
output.push_str("use strum_macros::{AsRefStr, EnumString};");
output.push('\n');
output.push('\n');
output.push_str("use crate::checks::CheckCode;");
@@ -186,7 +191,9 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push_str("pub const CATEGORIES: &[CheckCodePrefix] = &[");
output.push('\n');
for prefix in prefix_to_codes.keys() {
if prefix.chars().all(char::is_alphabetic) {
if prefix.chars().all(char::is_alphabetic)
&& !PREFIX_REDIRECTS.contains_key(&prefix.as_str())
{
output.push_str(&format!("CheckCodePrefix::{prefix},"));
output.push('\n');
}

View File

@@ -0,0 +1,125 @@
//! Generate a Markdown-compatible listing of configuration options.
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated options sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated options sections. -->";
#[derive(Args)]
pub struct Cli {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
dry_run: bool,
}
fn emit_field(output: &mut String, field: &OptionField, group_name: Option<&str>) {
output.push_str(&format!("#### [`{0}`](#{0})\n", field.name));
output.push('\n');
output.push_str(field.doc);
output.push_str("\n\n");
output.push_str(&format!("**Default value**: `{}`\n", field.default));
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if group_name.is_some() {
format!(".{}", group_name.unwrap())
} else {
String::new()
},
field.example
));
output.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
let mut output = String::new();
// Generate all the top-level fields.
for field in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, &field, None);
output.push_str("---\n\n");
}
// Generate all the sub-groups.
for group in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Group(group) = entry {
Some(group)
} else {
None
}
})
.sorted_by_key(|group| group.name)
{
output.push_str(&format!("### `{}`\n", group.name));
output.push('\n');
for field in group
.fields
.iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, field, Some(group.name));
output.push_str("---\n\n");
}
}
if cli.dry_run {
print!("{output}");
} else {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(BEGIN_PRAGMA)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + BEGIN_PRAGMA.len()];
// Extract the suffix.
let index = existing
.find(END_PRAGMA)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{prefix}\n\n")?;
write!(f, "{output}")?;
write!(f, "{suffix}")?;
}
Ok(())
}

View File

@@ -7,11 +7,15 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::checks::{CheckCategory, CheckCode};
use strum::IntoEnumIterator;
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TABLE_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const TABLE_END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TOC_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated table of contents. -->";
const TOC_END_PRAGMA: &str = "<!-- End auto-generated table of contents. -->";
#[derive(Args)]
pub struct Cli {
@@ -22,73 +26,91 @@ pub struct Cli {
pub fn main(cli: &Cli) -> Result<()> {
// Generate the table string.
let mut output = String::new();
let mut table_out = String::new();
let mut toc_out = String::new();
for check_category in CheckCategory::iter() {
output.push_str(&format!("### {}", check_category.title()));
output.push('\n');
output.push('\n');
let codes_csv: String = check_category.codes().iter().map(AsRef::as_ref).join(", ");
table_out.push_str(&format!("### {} ({codes_csv})", check_category.title()));
table_out.push('\n');
table_out.push('\n');
toc_out.push_str(&format!(
" 1. [{} ({})](#{}-{})\n",
check_category.title(),
codes_csv,
check_category.title().to_lowercase().replace(' ', "-"),
codes_csv.to_lowercase().replace(',', "-").replace(' ', "")
));
if let Some((url, platform)) = check_category.url() {
output.push_str(&format!(
table_out.push_str(&format!(
"For more, see [{}]({}) on {}.",
check_category.title(),
url,
platform
));
output.push('\n');
output.push('\n');
table_out.push('\n');
table_out.push('\n');
}
output.push_str("| Code | Name | Message | Fix |");
output.push('\n');
output.push_str("| ---- | ---- | ------- | --- |");
output.push('\n');
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for check_code in CheckCode::iter() {
if check_code.category() == check_category {
let check_kind = check_code.kind();
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
output.push_str(&format!(
table_out.push_str(&format!(
"| {} | {} | {} | {} |",
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.summary().replace('|', r"\|"),
fix_token
));
output.push('\n');
table_out.push('\n');
}
}
output.push('\n');
table_out.push('\n');
}
if cli.dry_run {
print!("{output}");
print!("Table of Contents: {toc_out}\n Rules Tables: {table_out}");
} else {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(BEGIN_PRAGMA)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + BEGIN_PRAGMA.len()];
// Extract the suffix.
let index = existing
.find(END_PRAGMA)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{prefix}\n\n")?;
write!(f, "{output}")?;
write!(f, "{suffix}")?;
// Extra newline in the markdown numbered list looks weird
replace_readme_section(toc_out.trim_end(), TOC_BEGIN_PRAGMA, TOC_END_PRAGMA)?;
replace_readme_section(&table_out, TABLE_BEGIN_PRAGMA, TABLE_END_PRAGMA)?;
}
Ok(())
}
fn replace_readme_section(content: &str, begin_pragma: &str, end_pragma: &str) -> Result<()> {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(begin_pragma)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + begin_pragma.len()];
// Extract the suffix.
let index = existing
.find(end_pragma)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
writeln!(f, "{prefix}")?;
write!(f, "{content}")?;
write!(f, "{suffix}")?;
Ok(())
}

View File

@@ -12,6 +12,7 @@
)]
pub mod generate_check_code_prefix;
pub mod generate_options;
pub mod generate_rules_table;
pub mod generate_source_code;
pub mod print_ast;

View File

@@ -14,8 +14,8 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff_dev::{
generate_check_code_prefix, generate_rules_table, generate_source_code, print_ast, print_cst,
print_tokens,
generate_check_code_prefix, generate_options, generate_rules_table, generate_source_code,
print_ast, print_cst, print_tokens,
};
#[derive(Parser)]
@@ -32,6 +32,8 @@ enum Commands {
GenerateCheckCodePrefix(generate_check_code_prefix::Cli),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable(generate_rules_table::Cli),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions(generate_options::Cli),
/// Run round-trip source code generation on a given Python file.
GenerateSourceCode(generate_source_code::Cli),
/// Print the AST for a given Python file.
@@ -48,6 +50,7 @@ fn main() -> Result<()> {
Commands::GenerateCheckCodePrefix(args) => generate_check_code_prefix::main(args)?,
Commands::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Commands::GenerateSourceCode(args) => generate_source_code::main(args)?,
Commands::GenerateOptions(args) => generate_options::main(args)?,
Commands::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,

13
ruff_macros/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "ruff_macros"
version = "0.0.170"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0.47" }
quote = { version = "1.0.21" }
syn = { version = "1.0.103", features = ["derive", "parsing"] }
textwrap = { version = "0.16.0" }

177
ruff_macros/src/lib.rs Normal file
View File

@@ -0,0 +1,177 @@
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
clippy::implicit_hasher,
clippy::match_same_arms,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::similar_names,
clippy::too_many_lines
)]
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream};
use syn::token::Comma;
use syn::{
parse_macro_input, AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput,
Field, Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath,
};
#[proc_macro_derive(ConfigurationOptions, attributes(option, option_group))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
derive_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data, .. } = input;
match data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => {
let mut output = vec![];
for field in fields.named.iter() {
if let Some(attr) = field.attrs.iter().find(|a| a.path.is_ident("option")) {
output.push(handle_option(field, attr)?);
};
if field.attrs.iter().any(|a| a.path.is_ident("option_group")) {
output.push(handle_option_group(field)?);
};
}
Ok(quote! {
use crate::settings::options_base::{OptionEntry, OptionField, OptionGroup, ConfigurationOptions};
#[automatically_derived]
impl ConfigurationOptions for #ident {
fn get_available_options() -> Vec<OptionEntry> {
vec![#(#output),*]
}
}
})
}
_ => Err(syn::Error::new(
ident.span(),
"Can only derive ConfigurationOptions from structs with named fields.",
)),
}
}
/// For a field with type `Option<Foobar>` where `Foobar` itself is a struct
/// deriving `ConfigurationOptions`, create code that calls retrieves options
/// from that group: `Foobar::get_available_options()`
fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
// unwrap is safe because we're only going over named fields
let ident = field.ident.as_ref().unwrap();
match &field.ty {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => match segments.first() {
Some(PathSegment {
ident: type_ident,
arguments:
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
..
}) if type_ident == "Option" => {
let path = &args[0];
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => OptionEntry::Group(OptionGroup {
name: #kebab_name,
fields: #path::get_available_options(),
})
))
}
_ => Err(syn::Error::new(
ident.span(),
"Expected `Option<_>` as type.",
)),
},
_ => Err(syn::Error::new(ident.span(), "Expected type.")),
}
}
/// Parse an `#[option(doc="...", default="...", value_type="...",
/// example="...")]` attribute and return data in the form of an `OptionField`.
fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::TokenStream> {
// unwrap is safe because we're only going over named fields
let ident = field.ident.as_ref().unwrap();
let FieldAttributes {
doc,
default,
value_type,
example,
} = attr.parse_args::<FieldAttributes>()?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => OptionEntry::Field(OptionField {
name: #kebab_name,
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
})
))
}
#[derive(Debug)]
struct FieldAttributes {
doc: String,
default: String,
value_type: String,
example: String,
}
impl Parse for FieldAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let doc = _parse_key_value(input, "doc")?;
input.parse::<Comma>()?;
let default = _parse_key_value(input, "default")?;
input.parse::<Comma>()?;
let value_type = _parse_key_value(input, "value_type")?;
input.parse::<Comma>()?;
let example = _parse_key_value(input, "example")?;
if !input.is_empty() {
input.parse::<Comma>()?;
}
Ok(FieldAttributes {
doc: textwrap::dedent(&doc).trim_matches('\n').to_string(),
default,
value_type,
example: textwrap::dedent(&example).trim_matches('\n').to_string(),
})
}
}
fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
let ident: proc_macro2::Ident = input.parse()?;
if ident != name {
return Err(syn::Error::new(
ident.span(),
format!("Expected `{name}` name"),
));
}
input.parse::<Token![=]>()?;
let value: Lit = input.parse()?;
match &value {
Lit::Str(v) => Ok(v.value()),
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
}
}

9
src/ast/cast.rs Normal file
View File

@@ -0,0 +1,9 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. }
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list,
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
}

65
src/ast/function_type.rs Normal file
View File

@@ -0,0 +1,65 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Expr;
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn classify(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
classmethod_decorators: &[String],
staticmethod_decorators: &[String],
) -> FunctionType {
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
}

View File

@@ -1,3 +1,5 @@
pub mod cast;
pub mod function_type;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -94,23 +94,132 @@ pub fn in_nested_block<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool
})
}
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
let StmtKind::Assign { targets, value, .. } = &stmt.node else {
return false;
};
if !targets.iter().any(|child| {
matches!(
child.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}) {
return false;
/// Returns `true` if `parent` contains `child`.
fn contains(parent: &Expr, child: &Expr) -> bool {
match &parent.node {
ExprKind::BoolOp { values, .. } => values.iter().any(|parent| contains(parent, child)),
ExprKind::NamedExpr { target, value } => contains(target, child) || contains(value, child),
ExprKind::BinOp { left, right, .. } => contains(left, child) || contains(right, child),
ExprKind::UnaryOp { operand, .. } => contains(operand, child),
ExprKind::Lambda { body, .. } => contains(body, child),
ExprKind::IfExp { test, body, orelse } => {
contains(test, child) || contains(body, child) || contains(orelse, child)
}
ExprKind::Dict { keys, values } => keys
.iter()
.chain(values.iter())
.any(|parent| contains(parent, child)),
ExprKind::Set { elts } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::ListComp { elt, .. } => contains(elt, child),
ExprKind::SetComp { elt, .. } => contains(elt, child),
ExprKind::DictComp { key, value, .. } => contains(key, child) || contains(value, child),
ExprKind::GeneratorExp { elt, .. } => contains(elt, child),
ExprKind::Await { value } => contains(value, child),
ExprKind::Yield { value } => value.as_ref().map_or(false, |value| contains(value, child)),
ExprKind::YieldFrom { value } => contains(value, child),
ExprKind::Compare {
left, comparators, ..
} => contains(left, child) || comparators.iter().any(|parent| contains(parent, child)),
ExprKind::Call {
func,
args,
keywords,
} => {
contains(func, child)
|| args.iter().any(|parent| contains(parent, child))
|| keywords
.iter()
.any(|keyword| contains(&keyword.node.value, child))
}
ExprKind::FormattedValue {
value, format_spec, ..
} => {
contains(value, child)
|| format_spec
.as_ref()
.map_or(false, |value| contains(value, child))
}
ExprKind::JoinedStr { values } => values.iter().any(|parent| contains(parent, child)),
ExprKind::Constant { .. } => false,
ExprKind::Attribute { value, .. } => contains(value, child),
ExprKind::Subscript { value, slice, .. } => {
contains(value, child) || contains(slice, child)
}
ExprKind::Starred { value, .. } => contains(value, child),
ExprKind::Name { .. } => parent == child,
ExprKind::List { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::Tuple { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::Slice { lower, upper, step } => {
lower.as_ref().map_or(false, |value| contains(value, child))
|| upper.as_ref().map_or(false, |value| contains(value, child))
|| step.as_ref().map_or(false, |value| contains(value, child))
}
}
}
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
match &parent.node {
StmtKind::With { items, .. } => items.iter().any(|item| {
if let Some(optional_vars) = &item.optional_vars {
if matches!(optional_vars.node, ExprKind::Tuple { .. }) {
if contains(optional_vars, child) {
return true;
}
}
}
false
}),
StmtKind::Assign { targets, value, .. } => {
// In `(a, b) = (1, 2)`, `(1, 2)` is the target, and it is a tuple.
let value_is_tuple = matches!(
&value.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
);
// In `(a, b) = coords = (1, 2)`, `(a, b)` and `coords` are the targets, and
// `(a, b`) is a tuple. (We use "tuple" as a placeholder for any
// unpackable expression.)
let targets_are_tuples = targets.iter().all(|item| {
matches!(
item.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
});
// If we're looking at `a` in `(a, b) = coords = (1, 2)`, then we should
// identify that the current expression is in a tuple.
let child_in_tuple = targets_are_tuples
|| targets.iter().any(|item| {
matches!(
item.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
) && contains(item, child)
});
// If our child is a tuple, and value is not, it's always an unpacking
// expression. Ex) `x, y = tup`
if child_in_tuple && !value_is_tuple {
return true;
}
// If our child isn't a tuple, but value is, it's never an unpacking expression.
// Ex) `coords = (1, 2)`
if !child_in_tuple && value_is_tuple {
return false;
}
// If our target and the value are both tuples, then it's an unpacking
// expression assuming there's at least one non-tuple child.
// Ex) Given `(x, y) = coords = 1, 2`, `(x, y)` is considered an unpacking
// expression. Ex) Given `(x, y) = (a, b) = 1, 2`, `(x, y)` isn't
// considered an unpacking expression.
if child_in_tuple && value_is_tuple {
return !targets_are_tuples;
}
false
}
_ => false,
}
!matches!(
&value.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}
pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;

View File

@@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use rustc_hash::FxHashMap;
use rustpython_ast::{Expr, Keyword, Stmt};
use rustpython_ast::{Arguments, Expr, Keyword, Stmt};
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
@@ -30,35 +30,49 @@ impl Range {
}
}
#[derive(Clone, Debug)]
pub struct FunctionScope {
#[derive(Debug)]
pub struct FunctionDef<'a> {
pub name: &'a str,
pub args: &'a Arguments,
pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
// pub returns: Option<&'a Expr>,
// pub type_comment: Option<&'a str>,
// TODO(charlie): Create AsyncFunctionDef to mirror the AST.
pub async_: bool,
pub uses_locals: bool,
}
#[derive(Clone, Debug)]
pub struct ClassScope<'a> {
#[derive(Debug)]
pub struct ClassDef<'a> {
pub name: &'a str,
pub bases: &'a [Expr],
pub keywords: &'a [Keyword],
// pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Lambda<'a> {
pub args: &'a Arguments,
pub body: &'a Expr,
}
#[derive(Debug)]
pub enum ScopeKind<'a> {
Class(ClassScope<'a>),
Function(FunctionScope),
Class(ClassDef<'a>),
Function(FunctionDef<'a>),
Generator,
Module,
Arg,
Lambda,
Lambda(Lambda<'a>),
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub uses_locals: bool,
pub values: FxHashMap<&'a str, Binding>,
}
@@ -68,6 +82,7 @@ impl<'a> Scope<'a> {
id: id(),
kind,
import_starred: false,
uses_locals: false,
values: FxHashMap::default(),
}
}

View File

@@ -139,20 +139,21 @@ pub fn get(
return None;
};
if let Ok(encoded) = read_sync(cache_key(path, settings, autofix)) {
match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => {
if FileTime::from_last_modification_time(metadata).unix_seconds() == mtime {
return Some(messages);
}
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
let encoded = read_sync(cache_key(path, settings, autofix)).ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => (mtime, messages),
Err(e) => {
error!("Failed to deserialize encoded cache entry: {e:?}");
return None;
}
};
if FileTime::from_last_modification_time(metadata).unix_seconds() != mtime {
return None;
}
None
Some(messages)
}
/// Set a value in the cache.

View File

@@ -5,7 +5,6 @@ use std::path::Path;
use log::error;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Withitem;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@@ -18,9 +17,10 @@ use crate::ast::helpers::{
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, Node, Range, Scope, ScopeKind,
Binding, BindingContext, BindingKind, ClassDef, FunctionDef, Lambda, Node, Range, Scope,
ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, walk_withitem, Visitor};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{helpers, operations, visitor};
use crate::checks::{Check, CheckCode, CheckKind, DeferralKeyword};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
@@ -36,8 +36,9 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_print, flake8_return, flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle,
pyflakes, pygrep_hooks, pylint, pyupgrade,
flake8_import_conventions, flake8_print, flake8_return, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
pylint, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -72,7 +73,7 @@ pub struct Checker<'a> {
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext)>,
deferred_assignments: Vec<usize>,
deferred_assignments: Vec<(usize, DeferralContext)>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
@@ -82,7 +83,6 @@ pub struct Checker<'a> {
in_deferred_type_definition: bool,
in_literal: bool,
in_subscript: bool,
in_withitem: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
@@ -130,7 +130,6 @@ impl<'a> Checker<'a> {
in_deferred_type_definition: false,
in_literal: false,
in_subscript: false,
in_withitem: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: false,
@@ -145,15 +144,16 @@ impl<'a> Checker<'a> {
// If we're in an f-string, override the location. RustPython doesn't produce
// reliable locations for expressions within f-strings, so we use the
// span of the f-string itself as a best-effort default.
if let Some(range) = self.in_f_string {
self.checks.push(Check {
let check = if let Some(range) = self.in_f_string {
Check {
location: range.location,
end_location: range.end_location,
..check
});
}
} else {
self.checks.push(check);
}
check
};
self.checks.push(check);
}
/// Add multiple `Check` items to the `Checker`.
@@ -582,7 +582,7 @@ where
for expr in decorator_list {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Class(ClassScope {
self.push_scope(Scope::new(ScopeKind::Class(ClassDef {
name,
bases,
keywords,
@@ -673,7 +673,7 @@ where
pylint::plugins::useless_import_alias(self, alias);
}
if self.settings.enabled.contains(&CheckCode::PLR0402) {
pylint::plugins::consider_using_from_import(self, alias);
pylint::plugins::use_from_import(self, alias);
}
if let Some(asname) = &alias.node.asname {
@@ -730,6 +730,19 @@ where
}
}
}
if self.settings.enabled.contains(&CheckCode::ICN001) {
if let Some(check) =
flake8_import_conventions::checks::check_conventional_import(
stmt,
&alias.node.name,
alias.node.asname.as_deref(),
&self.settings.flake8_import_conventions.aliases,
)
{
self.add_check(check);
}
}
}
}
StmtKind::ImportFrom {
@@ -891,7 +904,7 @@ where
);
}
if self.settings.enabled.contains(&CheckCode::I252) {
if self.settings.enabled.contains(&CheckCode::TID252) {
if let Some(check) = flake8_tidy_imports::checks::banned_relative_import(
stmt,
level.as_ref(),
@@ -1066,9 +1079,6 @@ where
if self.settings.enabled.contains(&CheckCode::B013) {
flake8_bugbear::plugins::redundant_tuple_in_exception_handler(self, handlers);
}
if self.settings.enabled.contains(&CheckCode::BLE001) {
flake8_blind_except::plugins::blind_except(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
@@ -1398,20 +1408,18 @@ where
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_named_arguments(
&summary, keywords, location,
)
if let Some(check) = pyflakes::checks::string_dot_format_extra_named_arguments(
&summary, keywords, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F523) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_positional_arguments(
&summary, args, location,
)
if let Some(check) = pyflakes::checks::string_dot_format_extra_positional_arguments(
&summary, args, location,
)
{
self.add_check(check);
}
@@ -1447,16 +1455,15 @@ where
if self.settings.enabled.contains(&CheckCode::UP005) {
pyupgrade::plugins::deprecated_unittest_alias(self, func);
}
if self.settings.enabled.contains(&CheckCode::UP012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
// flake8-super
if self.settings.enabled.contains(&CheckCode::UP008) {
pyupgrade::plugins::super_call_with_parameters(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::UP012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
@@ -1464,6 +1471,7 @@ where
flake8_print::plugins::print_call(self, expr, func);
}
// flake8-bugbear
if self.settings.enabled.contains(&CheckCode::B004) {
flake8_bugbear::plugins::unreliable_callable_check(self, expr, func, args);
}
@@ -1478,7 +1486,7 @@ where
.scope_stack
.iter()
.rev()
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda))
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda(..)))
{
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
@@ -1491,6 +1499,15 @@ where
self, args, keywords,
);
}
if self.settings.enabled.contains(&CheckCode::B905)
&& self.settings.target_version >= PythonVersion::Py310
{
flake8_bugbear::plugins::zip_without_explicit_strict(
self, expr, func, keywords,
);
}
// flake8-bandit
if self.settings.enabled.contains(&CheckCode::S102) {
if let Some(check) = flake8_bandit::plugins::exec_used(expr, func) {
self.add_check(check);
@@ -1730,9 +1747,7 @@ where
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found"))];
if let ScopeKind::Function(inner) = &mut scope.kind {
inner.uses_locals = true;
}
scope.uses_locals = true;
}
}
@@ -1758,7 +1773,7 @@ where
pylint::plugins::unnecessary_direct_lambda_call(self, expr, func);
}
if self.settings.enabled.contains(&CheckCode::PLR1722) {
pylint::plugins::consider_using_sys_exit(self, func);
pylint::plugins::use_sys_exit(self, func);
}
}
ExprKind::Dict { keys, .. } => {
@@ -2053,7 +2068,7 @@ where
}
}
}
ExprKind::Lambda { args, .. } => {
ExprKind::Lambda { args, body, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
for arg in &args.posonlyargs {
if let Some(expr) = &arg.node.annotation {
@@ -2086,7 +2101,7 @@ where
for expr in &args.defaults {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Lambda));
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
}
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
if self.settings.enabled.contains(&CheckCode::C416) {
@@ -2114,7 +2129,7 @@ where
}
ExprKind::BoolOp { op, values } => {
if self.settings.enabled.contains(&CheckCode::PLR1701) {
pylint::plugins::consider_merging_isinstance(self, expr, op, values);
pylint::plugins::merge_isinstance(self, expr, op, values);
}
}
_ => {}
@@ -2327,16 +2342,25 @@ where
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
self.add_check(Check::new(
CheckKind::DoNotUseBareExcept,
if self.settings.enabled.contains(&CheckCode::E722) {
if let Some(check) = pycodestyle::checks::do_not_use_bare_except(
type_.as_deref(),
body,
Range::from_located(excepthandler),
));
) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::B904) {
{
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
if self.settings.enabled.contains(&CheckCode::BLE001) {
flake8_blind_except::plugins::blind_except(
self,
type_.as_deref(),
name.as_ref().map(String::as_str),
body,
);
}
match name {
Some(name) => {
@@ -2485,13 +2509,6 @@ where
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
}
fn visit_withitem(&mut self, withitem: &'b Withitem) {
let prev_in_withitem = self.in_withitem;
self.in_withitem = true;
walk_withitem(self, withitem);
self.in_withitem = prev_in_withitem;
}
}
fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> bool {
@@ -2798,7 +2815,7 @@ impl<'a> Checker<'a> {
return;
}
if self.in_withitem || operations::is_unpacking_assignment(parent) {
if operations::is_unpacking_assignment(parent, expr) {
self.add_binding(
id,
Binding {
@@ -2939,27 +2956,44 @@ impl<'a> Checker<'a> {
fn check_deferred_functions(&mut self) {
while let Some((stmt, (scopes, parents), visibility)) = self.deferred_functions.pop() {
self.scope_stack = scopes;
self.parent_stack = parents;
self.scope_stack = scopes.clone();
self.parent_stack = parents.clone();
self.visible_scope = visibility;
self.push_scope(Scope::new(ScopeKind::Function(FunctionScope {
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
uses_locals: false,
})));
match &stmt.node {
StmtKind::FunctionDef { body, args, .. }
| StmtKind::AsyncFunctionDef { body, args, .. } => {
StmtKind::FunctionDef {
name,
body,
args,
decorator_list,
..
}
| StmtKind::AsyncFunctionDef {
name,
body,
args,
decorator_list,
..
} => {
self.push_scope(Scope::new(ScopeKind::Function(FunctionDef {
name,
body,
args,
decorator_list,
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
})));
self.visit_arguments(args);
for stmt in body {
self.visit_stmt(stmt);
}
}
_ => {}
_ => unreachable!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
self.deferred_assignments
.push(*self.scope_stack.last().expect("No current scope found"));
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
}
@@ -2967,25 +3001,29 @@ impl<'a> Checker<'a> {
fn check_deferred_lambdas(&mut self) {
while let Some((expr, (scopes, parents))) = self.deferred_lambdas.pop() {
self.scope_stack = scopes;
self.parent_stack = parents;
self.push_scope(Scope::new(ScopeKind::Lambda));
self.scope_stack = scopes.clone();
self.parent_stack = parents.clone();
if let ExprKind::Lambda { args, body } = &expr.node {
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
self.visit_arguments(args);
self.visit_expr(body);
} else {
unreachable!("Expected ExprKind::Lambda");
}
self.deferred_assignments
.push(*self.scope_stack.last().expect("No current scope found"));
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
}
}
fn check_deferred_assignments(&mut self) {
if self.settings.enabled.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
while let Some((index, (scopes, _parents))) = self.deferred_assignments.pop() {
if self.settings.enabled.contains(&CheckCode::F841) {
self.add_checks(
pyflakes::checks::unused_variables(
&self.scopes[index],
@@ -2994,6 +3032,23 @@ impl<'a> Checker<'a> {
.into_iter(),
);
}
if self.settings.enabled.contains(&CheckCode::ARG001)
|| self.settings.enabled.contains(&CheckCode::ARG002)
|| self.settings.enabled.contains(&CheckCode::ARG003)
|| self.settings.enabled.contains(&CheckCode::ARG004)
|| self.settings.enabled.contains(&CheckCode::ARG005)
{
self.add_checks(
flake8_unused_arguments::plugins::unused_arguments(
self,
&self.scopes[*scopes
.last()
.expect("Expected parent scope above function scope")],
&self.scopes[index],
)
.into_iter(),
);
}
}
}
@@ -3073,7 +3128,7 @@ impl<'a> Checker<'a> {
for (name, binding) in &scope.values {
let (BindingKind::Importation(_, full_name, context)
| BindingKind::SubmoduleImportation(_, full_name, context)
| BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue };
| BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue; };
// Skip used exports from `__all__`
if binding.used.is_some()
@@ -3142,6 +3197,8 @@ impl<'a> Checker<'a> {
}
fn check_definitions(&mut self) {
let mut overloaded_name: Option<String> = None;
self.definitions.reverse();
while let Some((definition, visibility)) = self.definitions.pop() {
// flake8-annotations
if self.settings.enabled.contains(&CheckCode::ANN001)
@@ -3156,7 +3213,21 @@ impl<'a> Checker<'a> {
|| self.settings.enabled.contains(&CheckCode::ANN206)
|| self.settings.enabled.contains(&CheckCode::ANN401)
{
flake8_annotations::plugins::definition(self, &definition, &visibility);
// TODO(charlie): This should be even stricter, in that an overload
// implementation should come immediately after the overloaded
// interfaces, without any AST nodes in between. Right now, we
// only error when traversing definition boundaries (functions,
// classes, etc.).
if !overloaded_name.map_or(false, |overloaded_name| {
flake8_annotations::helpers::is_overload_impl(
self,
&definition,
&overloaded_name,
)
}) {
flake8_annotations::plugins::definition(self, &definition, &visibility);
}
overloaded_name = flake8_annotations::helpers::overloaded_name(self, &definition);
}
// pydocstyle

View File

@@ -1,5 +1,7 @@
//! Lint rules based on import analysis.
use std::path::Path;
use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
@@ -33,8 +35,9 @@ pub fn check_imports(
directives: &IsortDirectives,
settings: &Settings,
autofix: bool,
path: &Path,
) -> Vec<Check> {
let mut tracker = ImportTracker::new(directives);
let mut tracker = ImportTracker::new(directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind, REDIRECTS};
use crate::checks::{Check, CheckCode, CheckKind, CODE_REDIRECTS};
use crate::noqa;
use crate::noqa::{is_file_exempt, Directive};
use crate::settings::Settings;
@@ -20,19 +20,18 @@ static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").unwra
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
let mut chunks = line.split_whitespace();
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(true, |c| URL_REGEX.is_match(c)))
} else {
// Single word / no printable chars - no way to make the line shorter
false
}
} else {
false
if length <= limit {
return false;
}
let mut chunks = line.split_whitespace();
let (Some(first), Some(_)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return false;
};
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(true, |c| URL_REGEX.is_match(c)))
}
pub fn check_lines(
@@ -210,7 +209,7 @@ pub fn check_lines(
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
let code = REDIRECTS.get(code).map_or(code, AsRef::as_ref);
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code.to_string());
} else {

View File

@@ -4,9 +4,9 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
use crate::lex::docstring_detection::StateMachine;
use crate::rules::checks::Context;
use crate::ruff::checks::Context;
use crate::source_code_locator::SourceCodeLocator;
use crate::{eradicate, flake8_quotes, pycodestyle, rules, Settings};
use crate::{eradicate, flake8_quotes, pycodestyle, ruff, Settings};
pub fn check_tokens(
locator: &SourceCodeLocator,
@@ -37,7 +37,7 @@ pub fn check_tokens(
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment) {
checks.extend(rules::checks::ambiguous_unicode_character(
checks.extend(ruff::checks::ambiguous_unicode_character(
locator,
start,
end,

View File

@@ -10,6 +10,7 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_debugger::types::DebuggerUsingType;
use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness;
@@ -134,6 +135,7 @@ pub enum CheckCode {
B026,
B027,
B904,
B905,
// flake8-blind-except
BLE001,
// flake8-comprehensions
@@ -158,7 +160,7 @@ pub enum CheckCode {
// mccabe
C901,
// flake8-tidy-imports
I252,
TID252,
// flake8-return
RET501,
RET502,
@@ -290,6 +292,14 @@ pub enum CheckCode {
FBT001,
FBT002,
FBT003,
// flake8-unused-arguments
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
// flake8-import-conventions
ICN001,
// Ruff
RUF001,
RUF002,
@@ -317,10 +327,12 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8TidyImports,
Flake8UnusedArguments,
Eradicate,
PygrepHooks,
Pylint,
@@ -354,10 +366,12 @@ impl CheckCategory {
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Debugger => "flake8-debugger",
CheckCategory::Flake8ImportConventions => "flake8-import-conventions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Return => "flake8-return",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -371,6 +385,42 @@ impl CheckCategory {
}
}
pub fn codes(&self) -> Vec<CheckCodePrefix> {
match self {
CheckCategory::Eradicate => vec![CheckCodePrefix::ERA],
CheckCategory::Flake82020 => vec![CheckCodePrefix::YTT],
CheckCategory::Flake8Annotations => vec![CheckCodePrefix::ANN],
CheckCategory::Flake8Bandit => vec![CheckCodePrefix::S],
CheckCategory::Flake8BlindExcept => vec![CheckCodePrefix::BLE],
CheckCategory::Flake8BooleanTrap => vec![CheckCodePrefix::FBT],
CheckCategory::Flake8Bugbear => vec![CheckCodePrefix::B],
CheckCategory::Flake8Builtins => vec![CheckCodePrefix::A],
CheckCategory::Flake8Comprehensions => vec![CheckCodePrefix::C4],
CheckCategory::Flake8Debugger => vec![CheckCodePrefix::T10],
CheckCategory::Flake8Print => vec![CheckCodePrefix::T20],
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::TID],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Isort => vec![CheckCodePrefix::I],
CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
CheckCategory::Pycodestyle => vec![CheckCodePrefix::E, CheckCodePrefix::W],
CheckCategory::Pydocstyle => vec![CheckCodePrefix::D],
CheckCategory::Pyflakes => vec![CheckCodePrefix::F],
CheckCategory::PygrepHooks => vec![CheckCodePrefix::PGH],
CheckCategory::Pylint => vec![
CheckCodePrefix::PLC,
CheckCodePrefix::PLE,
CheckCodePrefix::PLR,
CheckCodePrefix::PLW,
],
CheckCategory::Pyupgrade => vec![CheckCodePrefix::UP],
CheckCategory::Flake8ImportConventions => vec![CheckCodePrefix::ICN],
CheckCategory::Ruff => vec![CheckCodePrefix::RUF],
}
}
pub fn url(&self) -> Option<(&'static str, &'static Platform)> {
match self {
CheckCategory::Eradicate => {
@@ -412,6 +462,7 @@ impl CheckCategory {
"https://pypi.org/project/flake8-debugger/4.1.2/",
&Platform::PyPI,
)),
CheckCategory::Flake8ImportConventions => None,
CheckCategory::Flake8Print => Some((
"https://pypi.org/project/flake8-print/5.0.0/",
&Platform::PyPI,
@@ -428,6 +479,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-tidy-imports/4.8.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8UnusedArguments => Some((
"https://pypi.org/project/flake8-unused-arguments/0.0.12/",
&Platform::PyPI,
)),
CheckCategory::Isort => {
Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI))
}
@@ -583,13 +638,13 @@ pub enum CheckKind {
ConsiderUsingFromImport(String, String),
AwaitOutsideAsync,
UselessElseOnLoop,
ConsiderUsingSysExit,
UseSysExit(String),
// flake8-builtins
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-blind-except
BlindExcept,
BlindExcept(String),
// flake8-bugbear
AbstractBaseClassWithoutAbstractMethod(String),
AssignmentToOsEnviron,
@@ -618,6 +673,7 @@ pub enum CheckKind {
UselessComparison,
UselessContextlibSuppress,
UselessExpression,
ZipWithoutExplicitStrict,
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -774,6 +830,14 @@ pub enum CheckKind {
BooleanPositionalValueInFunctionCall,
// pygrep-hooks
NoEval,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
UnusedClassMethodArgument(String),
UnusedStaticMethodArgument(String),
UnusedLambdaArgument(String),
// flake8-import-conventions
ImportAliasIsNotConventional(String, String),
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -886,7 +950,7 @@ impl CheckCode {
CheckCode::PLR1701 => {
CheckKind::ConsiderMergingIsinstance("...".to_string(), vec!["...".to_string()])
}
CheckCode::PLR1722 => CheckKind::ConsiderUsingSysExit,
CheckCode::PLR1722 => CheckKind::UseSysExit("exit".to_string()),
CheckCode::PLW0120 => CheckKind::UselessElseOnLoop,
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
@@ -924,6 +988,7 @@ impl CheckCode {
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
CheckCode::B904 => CheckKind::RaiseWithoutFromInsideExcept,
CheckCode::B905 => CheckKind::ZipWithoutExplicitStrict,
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -957,7 +1022,7 @@ impl CheckCode {
// flake8-debugger
CheckCode::T100 => CheckKind::Debugger(DebuggerUsingType::Import("...".to_string())),
// flake8-tidy-imports
CheckCode::I252 => CheckKind::BannedRelativeImport(Strictness::All),
CheckCode::TID252 => CheckKind::BannedRelativeImport(Strictness::All),
// flake8-return
CheckCode::RET501 => CheckKind::UnnecessaryReturnNone,
CheckCode::RET502 => CheckKind::ImplicitReturnValue,
@@ -999,7 +1064,7 @@ impl CheckCode {
CheckCode::YTT302 => CheckKind::SysVersionCmpStr10,
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// flake8-blind-except
CheckCode::BLE001 => CheckKind::BlindExcept,
CheckCode::BLE001 => CheckKind::BlindExcept("Exception".to_string()),
// pyupgrade
CheckCode::UP001 => CheckKind::UselessMetaclassType,
CheckCode::UP003 => CheckKind::TypeOfPrimitive(Primitive::Str),
@@ -1113,6 +1178,16 @@ impl CheckCode {
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// pygrep-hooks
CheckCode::PGH001 => CheckKind::NoEval,
// flake8-unused-arguments
CheckCode::ARG001 => CheckKind::UnusedFunctionArgument("...".to_string()),
CheckCode::ARG002 => CheckKind::UnusedMethodArgument("...".to_string()),
CheckCode::ARG003 => CheckKind::UnusedClassMethodArgument("...".to_string()),
CheckCode::ARG004 => CheckKind::UnusedStaticMethodArgument("...".to_string()),
CheckCode::ARG005 => CheckKind::UnusedLambdaArgument("...".to_string()),
// flake8-import-conventions
CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
}
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -1138,6 +1213,11 @@ impl CheckCode {
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::ARG001 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG002 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG003 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG004 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG005 => CheckCategory::Flake8UnusedArguments,
CheckCode::B002 => CheckCategory::Flake8Bugbear,
CheckCode::B003 => CheckCategory::Flake8Bugbear,
CheckCode::B004 => CheckCategory::Flake8Bugbear,
@@ -1165,6 +1245,7 @@ impl CheckCode {
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::B904 => CheckCategory::Flake8Bugbear,
CheckCode::B905 => CheckCategory::Flake8Bugbear,
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
@@ -1288,7 +1369,8 @@ impl CheckCode {
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::TID252 => CheckCategory::Flake8TidyImports,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::N801 => CheckCategory::PEP8Naming,
CheckCode::N802 => CheckCategory::PEP8Naming,
CheckCode::N803 => CheckCategory::PEP8Naming,
@@ -1441,7 +1523,7 @@ impl CheckKind {
CheckKind::ConsiderMergingIsinstance(..) => &CheckCode::PLR1701,
CheckKind::PropertyWithParameters => &CheckCode::PLR0206,
CheckKind::ConsiderUsingFromImport(..) => &CheckCode::PLR0402,
CheckKind::ConsiderUsingSysExit => &CheckCode::PLR1722,
CheckKind::UseSysExit(_) => &CheckCode::PLR1722,
CheckKind::UselessElseOnLoop => &CheckCode::PLW0120,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
@@ -1465,6 +1547,7 @@ impl CheckKind {
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::RaiseWithoutFromInsideExcept => &CheckCode::B904,
CheckKind::ZipWithoutExplicitStrict => &CheckCode::B905,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
@@ -1476,7 +1559,7 @@ impl CheckKind {
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::UselessExpression => &CheckCode::B018,
// flake8-blind-except
CheckKind::BlindExcept => &CheckCode::BLE001,
CheckKind::BlindExcept(_) => &CheckCode::BLE001,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -1497,7 +1580,7 @@ impl CheckKind {
// flake8-debugger
CheckKind::Debugger(_) => &CheckCode::T100,
// flake8-tidy-imports
CheckKind::BannedRelativeImport(_) => &CheckCode::I252,
CheckKind::BannedRelativeImport(_) => &CheckCode::TID252,
// flake8-return
CheckKind::UnnecessaryReturnNone => &CheckCode::RET501,
CheckKind::ImplicitReturnValue => &CheckCode::RET502,
@@ -1633,6 +1716,14 @@ impl CheckKind {
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// pygrep-hooks
CheckKind::NoEval => &CheckCode::PGH001,
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(..) => &CheckCode::ARG001,
CheckKind::UnusedMethodArgument(..) => &CheckCode::ARG002,
CheckKind::UnusedClassMethodArgument(..) => &CheckCode::ARG003,
CheckKind::UnusedStaticMethodArgument(..) => &CheckCode::ARG004,
CheckKind::UnusedLambdaArgument(..) => &CheckCode::ARG005,
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1830,7 +1921,7 @@ impl CheckKind {
}
CheckKind::ConsiderMergingIsinstance(obj, types) => {
let types = types.join(", ");
format!("Consider merging these isinstance calls: `isinstance({obj}, ({types}))`")
format!("Merge these isinstance calls: `isinstance({obj}, ({types}))`")
}
CheckKind::MisplacedComparisonConstant(comprison) => {
format!("Comparison should be {comprison}")
@@ -1842,7 +1933,7 @@ impl CheckKind {
"Cannot have defined parameters for properties".to_string()
}
CheckKind::ConsiderUsingFromImport(module, name) => {
format!("Consider using `from {module} import {name}`")
format!("Use `from {module} import {name}` in lieu of alias")
}
CheckKind::AwaitOutsideAsync => {
"`await` should be used within an async function".to_string()
@@ -1850,7 +1941,7 @@ impl CheckKind {
CheckKind::UselessElseOnLoop => "Else clause on loop without a break statement, \
remove the else and de-indent all the code inside it"
.to_string(),
CheckKind::ConsiderUsingSysExit => "Consider using `sys.exit()`".to_string(),
CheckKind::UseSysExit(name) => format!("Use `sys.exit()` instead of `{name}`"),
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
@@ -1976,6 +2067,9 @@ impl CheckKind {
from None to distinguish them from errors in exception handling"
.to_string()
}
CheckKind::ZipWithoutExplicitStrict => {
"`zip()` without an explicit `strict=` parameter".to_string()
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
@@ -2402,7 +2496,7 @@ impl CheckKind {
format!("Possible hardcoded password: `\"{string}\"`")
}
// flake8-blind-except
CheckKind::BlindExcept => "Blind except Exception: statement".to_string(),
CheckKind::BlindExcept(name) => format!("Do not catch blind exception: `{name}`"),
// mccabe
CheckKind::FunctionIsTooComplex(name, complexity) => {
format!("`{name}` is too complex ({complexity})")
@@ -2419,6 +2513,22 @@ impl CheckKind {
}
// pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(name) => {
format!("Unused function argument: `{name}`")
}
CheckKind::UnusedMethodArgument(name) => format!("Unused method argument: `{name}`"),
CheckKind::UnusedClassMethodArgument(name) => {
format!("Unused class method argument: `{name}`")
}
CheckKind::UnusedStaticMethodArgument(name) => {
format!("Unused static method argument: `{name}`")
}
CheckKind::UnusedLambdaArgument(name) => format!("Unused lambda argument: `{name}`"),
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(name, asname) => {
format!("`{name}` should be imported as `{asname}`")
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -2489,7 +2599,6 @@ impl CheckKind {
| CheckKind::BlankLineBeforeSection(..)
| CheckKind::CapitalizeSectionName(..)
| CheckKind::CommentedOutCode
| CheckKind::ConsiderUsingSysExit
| CheckKind::ConvertNamedTupleFunctionalToClass(..)
| CheckKind::ConvertTypedDictFunctionalToClass(..)
| CheckKind::DashedUnderlineAfterSection(..)
@@ -2497,6 +2606,8 @@ impl CheckKind {
| CheckKind::DoNotAssertFalse
| CheckKind::DoNotAssignLambda
| CheckKind::DuplicateHandlerException(..)
| CheckKind::EndsInPeriod
| CheckKind::EndsInPunctuation
| CheckKind::GetAttrWithConstant
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue
@@ -2553,6 +2664,7 @@ impl CheckKind {
| CheckKind::UnusedNOQA(..)
| CheckKind::UsePEP585Annotation(..)
| CheckKind::UsePEP604Annotation
| CheckKind::UseSysExit(..)
| CheckKind::UselessImportAlias
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(..)
@@ -2584,7 +2696,7 @@ impl Check {
}
/// A hash map from deprecated to latest `CheckCode`.
pub static REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", CheckCode::UP001),
@@ -2601,6 +2713,25 @@ pub static REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
("U013", CheckCode::UP013),
("U014", CheckCode::UP014),
("U015", CheckCode::UP015),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCode::TID252),
("M001", CheckCode::RUF100),
])
});
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U01", "UP01"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("M", "RUF100"),
("M0", "RUF100"),
])
});

View File

@@ -2,11 +2,13 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use strum_macros::EnumString;
use strum_macros::{AsRefStr, EnumString};
use crate::checks::CheckCode;
#[derive(EnumString, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
#[derive(
EnumString, AsRefStr, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize,
)]
pub enum CheckCodePrefix {
A,
A0,
@@ -34,6 +36,14 @@ pub enum CheckCodePrefix {
ANN4,
ANN40,
ANN401,
ARG,
ARG0,
ARG00,
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
B,
B0,
B00,
@@ -68,6 +78,7 @@ pub enum CheckCodePrefix {
B9,
B90,
B904,
B905,
BLE,
BLE0,
BLE00,
@@ -256,6 +267,13 @@ pub enum CheckCodePrefix {
I2,
I25,
I252,
ICN,
ICN0,
ICN00,
ICN001,
M,
M0,
M001,
N,
N8,
N80,
@@ -360,6 +378,10 @@ pub enum CheckCodePrefix {
T20,
T201,
T203,
TID,
TID2,
TID25,
TID252,
U,
U0,
U00,
@@ -485,6 +507,32 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN40 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN401 => vec![CheckCode::ANN401],
CheckCodePrefix::ARG => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG0 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG00 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG001 => vec![CheckCode::ARG001],
CheckCodePrefix::ARG002 => vec![CheckCode::ARG002],
CheckCodePrefix::ARG003 => vec![CheckCode::ARG003],
CheckCodePrefix::ARG004 => vec![CheckCode::ARG004],
CheckCodePrefix::ARG005 => vec![CheckCode::ARG005],
CheckCodePrefix::B => vec![
CheckCode::B002,
CheckCode::B003,
@@ -513,6 +561,7 @@ impl CheckCodePrefix {
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
CheckCode::B905,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -600,9 +649,10 @@ impl CheckCodePrefix {
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::B9 => vec![CheckCode::B904],
CheckCodePrefix::B90 => vec![CheckCode::B904],
CheckCodePrefix::B9 => vec![CheckCode::B904, CheckCode::B905],
CheckCodePrefix::B90 => vec![CheckCode::B904, CheckCode::B905],
CheckCodePrefix::B904 => vec![CheckCode::B904],
CheckCodePrefix::B905 => vec![CheckCode::B905],
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
@@ -1131,13 +1181,68 @@ impl CheckCodePrefix {
CheckCodePrefix::FBT001 => vec![CheckCode::FBT001],
CheckCodePrefix::FBT002 => vec![CheckCode::FBT002],
CheckCodePrefix::FBT003 => vec![CheckCode::FBT003],
CheckCodePrefix::I => vec![CheckCode::I252, CheckCode::I001],
CheckCodePrefix::I => vec![CheckCode::I001],
CheckCodePrefix::I0 => vec![CheckCode::I001],
CheckCodePrefix::I00 => vec![CheckCode::I001],
CheckCodePrefix::I001 => vec![CheckCode::I001],
CheckCodePrefix::I2 => vec![CheckCode::I252],
CheckCodePrefix::I25 => vec![CheckCode::I252],
CheckCodePrefix::I252 => vec![CheckCode::I252],
CheckCodePrefix::I2 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I2` has been remapped to `TID2`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::I25 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I25` has been remapped to `TID25`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::I252 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I252` has been remapped to `TID252`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::ICN => vec![CheckCode::ICN001],
CheckCodePrefix::ICN0 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN00 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN001 => vec![CheckCode::ICN001],
CheckCodePrefix::M => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::M0 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M0` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::M001 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M001` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::N => vec![
CheckCode::N801,
CheckCode::N802,
@@ -1366,54 +1471,82 @@ impl CheckCodePrefix {
CheckCodePrefix::T20 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T201 => vec![CheckCode::T201],
CheckCodePrefix::T203 => vec![CheckCode::T203],
CheckCodePrefix::U => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U0 => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U00 => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
],
CheckCodePrefix::TID => vec![CheckCode::TID252],
CheckCodePrefix::TID2 => vec![CheckCode::TID252],
CheckCodePrefix::TID25 => vec![CheckCode::TID252],
CheckCodePrefix::TID252 => vec![CheckCode::TID252],
CheckCodePrefix::U => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U` has been remapped to `UP`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U0 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U0` has been remapped to `UP0`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U00 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U00` has been remapped to `UP00`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
]
}
CheckCodePrefix::U001 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U001` has been renamed to `UP001`".bold()
"`U001` has been remapped to `UP001`".bold()
);
vec![CheckCode::UP001]
}
@@ -1422,7 +1555,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U003` has been renamed to `UP003`".bold()
"`U003` has been remapped to `UP003`".bold()
);
vec![CheckCode::UP003]
}
@@ -1431,7 +1564,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U004` has been renamed to `UP004`".bold()
"`U004` has been remapped to `UP004`".bold()
);
vec![CheckCode::UP004]
}
@@ -1440,7 +1573,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U005` has been renamed to `UP005`".bold()
"`U005` has been remapped to `UP005`".bold()
);
vec![CheckCode::UP005]
}
@@ -1449,7 +1582,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U006` has been renamed to `UP006`".bold()
"`U006` has been remapped to `UP006`".bold()
);
vec![CheckCode::UP006]
}
@@ -1458,7 +1591,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U007` has been renamed to `UP007`".bold()
"`U007` has been remapped to `UP007`".bold()
);
vec![CheckCode::UP007]
}
@@ -1467,7 +1600,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U008` has been renamed to `UP008`".bold()
"`U008` has been remapped to `UP008`".bold()
);
vec![CheckCode::UP008]
}
@@ -1476,24 +1609,32 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U009` has been renamed to `UP009`".bold()
"`U009` has been remapped to `UP009`".bold()
);
vec![CheckCode::UP009]
}
CheckCodePrefix::U01 => vec![
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U01 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U01` has been remapped to `UP01`".bold()
);
vec![
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U010 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U010` has been renamed to `UP010`".bold()
"`U010` has been remapped to `UP010`".bold()
);
vec![CheckCode::UP010]
}
@@ -1502,7 +1643,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U011` has been renamed to `UP011`".bold()
"`U011` has been remapped to `UP011`".bold()
);
vec![CheckCode::UP011]
}
@@ -1511,7 +1652,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U012` has been renamed to `UP012`".bold()
"`U012` has been remapped to `UP012`".bold()
);
vec![CheckCode::UP012]
}
@@ -1520,7 +1661,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U013` has been renamed to `UP013`".bold()
"`U013` has been remapped to `UP013`".bold()
);
vec![CheckCode::UP013]
}
@@ -1529,7 +1670,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U014` has been renamed to `UP014`".bold()
"`U014` has been remapped to `UP014`".bold()
);
vec![CheckCode::UP014]
}
@@ -1538,7 +1679,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U015` has been renamed to `UP015`".bold()
"`U015` has been remapped to `UP015`".bold()
);
vec![CheckCode::UP015]
}
@@ -1685,6 +1826,14 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => SuffixLength::One,
CheckCodePrefix::ANN40 => SuffixLength::Two,
CheckCodePrefix::ANN401 => SuffixLength::Three,
CheckCodePrefix::ARG => SuffixLength::Zero,
CheckCodePrefix::ARG0 => SuffixLength::One,
CheckCodePrefix::ARG00 => SuffixLength::Two,
CheckCodePrefix::ARG001 => SuffixLength::Three,
CheckCodePrefix::ARG002 => SuffixLength::Three,
CheckCodePrefix::ARG003 => SuffixLength::Three,
CheckCodePrefix::ARG004 => SuffixLength::Three,
CheckCodePrefix::ARG005 => SuffixLength::Three,
CheckCodePrefix::B => SuffixLength::Zero,
CheckCodePrefix::B0 => SuffixLength::One,
CheckCodePrefix::B00 => SuffixLength::Two,
@@ -1719,6 +1868,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B9 => SuffixLength::One,
CheckCodePrefix::B90 => SuffixLength::Two,
CheckCodePrefix::B904 => SuffixLength::Three,
CheckCodePrefix::B905 => SuffixLength::Three,
CheckCodePrefix::BLE => SuffixLength::Zero,
CheckCodePrefix::BLE0 => SuffixLength::One,
CheckCodePrefix::BLE00 => SuffixLength::Two,
@@ -1907,6 +2057,13 @@ impl CheckCodePrefix {
CheckCodePrefix::I2 => SuffixLength::One,
CheckCodePrefix::I25 => SuffixLength::Two,
CheckCodePrefix::I252 => SuffixLength::Three,
CheckCodePrefix::ICN => SuffixLength::Zero,
CheckCodePrefix::ICN0 => SuffixLength::One,
CheckCodePrefix::ICN00 => SuffixLength::Two,
CheckCodePrefix::ICN001 => SuffixLength::Three,
CheckCodePrefix::M => SuffixLength::Zero,
CheckCodePrefix::M0 => SuffixLength::One,
CheckCodePrefix::M001 => SuffixLength::Three,
CheckCodePrefix::N => SuffixLength::Zero,
CheckCodePrefix::N8 => SuffixLength::One,
CheckCodePrefix::N80 => SuffixLength::Two,
@@ -2011,6 +2168,10 @@ impl CheckCodePrefix {
CheckCodePrefix::T20 => SuffixLength::Two,
CheckCodePrefix::T201 => SuffixLength::Three,
CheckCodePrefix::T203 => SuffixLength::Three,
CheckCodePrefix::TID => SuffixLength::Zero,
CheckCodePrefix::TID2 => SuffixLength::One,
CheckCodePrefix::TID25 => SuffixLength::Two,
CheckCodePrefix::TID252 => SuffixLength::Three,
CheckCodePrefix::U => SuffixLength::Zero,
CheckCodePrefix::U0 => SuffixLength::One,
CheckCodePrefix::U00 => SuffixLength::Two,
@@ -2078,6 +2239,7 @@ impl CheckCodePrefix {
pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::A,
CheckCodePrefix::ANN,
CheckCodePrefix::ARG,
CheckCodePrefix::B,
CheckCodePrefix::BLE,
CheckCodePrefix::C,
@@ -2087,6 +2249,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::F,
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::N,
CheckCodePrefix::PGH,
CheckCodePrefix::PLC,
@@ -2098,7 +2261,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::T,
CheckCodePrefix::U,
CheckCodePrefix::TID,
CheckCodePrefix::UP,
CheckCodePrefix::W,
CheckCodePrefix::YTT,

View File

@@ -93,32 +93,34 @@ pub fn extract_isort_directives(lxr: &[LexResult], locator: &SourceCodeLocator)
continue;
}
if matches!(tok, Tok::Comment) {
// TODO(charlie): Modify RustPython to include the comment text in the token.
let comment_text = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
if !matches!(tok, Tok::Comment) {
continue;
}
if comment_text == "# isort: split" {
splits.push(start.row());
} else if comment_text == "# isort: skip_file" {
skip_file = true;
} else if off.is_some() {
if comment_text == "# isort: on" {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
// TODO(charlie): Modify RustPython to include the comment text in the token.
let comment_text = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
if comment_text == "# isort: split" {
splits.push(start.row());
} else if comment_text == "# isort: skip_file" {
skip_file = true;
} else if off.is_some() {
if comment_text == "# isort: on" {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
off = None;
}
} else {
if comment_text.contains("isort: skip") {
exclusions.insert(start.row());
} else if comment_text == "# isort: off" {
off = Some(start);
}
off = None;
}
} else {
if comment_text.contains("isort: skip") {
exclusions.insert(start.row());
} else if comment_text == "# isort: off" {
off = Some(start);
}
}
}

View File

@@ -0,0 +1,63 @@
use rustpython_ast::{Arguments, Expr, Stmt, StmtKind};
use crate::ast::cast;
use crate::check_ast::Checker;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::visibility;
pub(super) fn match_function_def(
stmt: &Stmt,
) -> (&str, &Arguments, &Option<Box<Expr>>, &Vec<Stmt>) {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
returns,
body,
..
}
| StmtKind::AsyncFunctionDef {
name,
args,
returns,
body,
..
} => (name, args, returns, body),
_ => panic!("Found non-FunctionDef in match_name"),
}
}
/// Return the name of the function, if it's overloaded.
pub fn overloaded_name(checker: &Checker, definition: &Definition) -> Option<String> {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if visibility::is_overload(checker, cast::decorator_list(stmt)) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
None
}
} else {
None
}
}
/// Return `true` if the definition is the implementation for an overloaded
/// function.
pub fn is_overload_impl(checker: &Checker, definition: &Definition, overloaded_name: &str) -> bool {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if visibility::is_overload(checker, cast::decorator_list(stmt)) {
false
} else {
let (name, ..) = match_function_def(stmt);
name == overloaded_name
}
} else {
false
}
}

View File

@@ -1,3 +1,4 @@
pub mod helpers;
pub mod plugins;
pub mod settings;
@@ -134,4 +135,24 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn allow_overload() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_overload.py"),
&Settings {
..Settings::for_rules(vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
])
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,11 +1,12 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::ast::{cast, visitor};
use crate::check_ast::Checker;
use crate::checks::{CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::flake8_annotations::helpers::match_function_def;
use crate::visibility::Visibility;
use crate::{visibility, Check};
@@ -61,26 +62,6 @@ where
};
}
fn match_function_def(stmt: &Stmt) -> (&str, &Arguments, &Option<Box<Expr>>, &Vec<Stmt>) {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
returns,
body,
..
}
| StmtKind::AsyncFunctionDef {
name,
args,
returns,
body,
..
} => (name, args, returns, body),
_ => panic!("Found non-FunctionDef in match_name"),
}
}
/// Generate flake8-annotation checks for a given `Definition`.
pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &Visibility) {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
@@ -211,7 +192,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(!visibility::is_staticmethod(checker, stmt)),
usize::from(!visibility::is_staticmethod(
checker,
cast::decorator_list(stmt),
)),
)
{
// ANN401 for dynamically typed arguments
@@ -283,10 +267,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
// ANN101, ANN102
if !visibility::is_staticmethod(checker, stmt) {
if !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, stmt) {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN102) {
checker.add_check(Check::new(
CheckKind::MissingTypeCls(arg.node.arg.to_string()),
@@ -319,14 +303,14 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
return;
}
if visibility::is_classmethod(checker, stmt) {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN206) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeClassMethod(name.to_string()),
Range::from_located(stmt),
));
}
} else if visibility::is_staticmethod(checker, stmt) {
} else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN205) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeStaticMethod(name.to_string()),

View File

@@ -1,22 +1,51 @@
//! Settings for the `flake-annotations` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
/// Allow omission of a return type hint for `__init__` if at least one
/// argument is annotated.
#[option(
doc = r#"
Whether to allow the omission of a return type hint for `__init__` if at least one
argument is annotated.
"#,
default = "false",
value_type = "bool",
example = "mypy-init-return = true"
)]
pub mypy_init_return: Option<bool>,
/// Suppress ANN000-level errors for dummy arguments, like `_`.
#[option(
doc = r#"
Whether to suppress `ANN000`-level errors for arguments matching the "dummy" variable
regex (like `_`).
"#,
default = "false",
value_type = "bool",
example = "suppress-dummy-args = true"
)]
pub suppress_dummy_args: Option<bool>,
/// Suppress ANN200-level errors for functions that meet one of the
/// following criteria:
/// - Contain no `return` statement
/// - Explicit `return` statement(s) all return `None` (explicitly or
/// implicitly).
#[option(
doc = r#"
Whether to suppress `ANN200`-level errors for functions that meet either of the
following criteria:
- Contain no `return` statement.
- Explicit `return` statement(s) all return `None` (explicitly or implicitly).
"#,
default = "false",
value_type = "bool",
example = "suppress-none-returning = true"
)]
pub suppress_none_returning: Option<bool>,
/// Suppress ANN401 for dynamically typed *args and **kwargs.
#[option(
doc = "Whether to suppress `ANN401` for dynamically typed `*args` and `**kwargs` \
arguments.",
default = "false",
value_type = "bool",
example = "allow-star-arg-any = true"
)]
pub allow_star_arg_any: Option<bool>,
}

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind:
MissingReturnTypePublicFunction: bar
location:
row: 29
column: 4
end_location:
row: 35
column: 0
fix: ~

View File

@@ -1,21 +1,42 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn blind_except(checker: &mut Checker, handlers: &[Excepthandler]) {
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_: Some(type_), .. } = &handler.node else {
continue;
};
let ExprKind::Name { id, .. } = &type_.node else {
continue;
};
for exception in ["BaseException", "Exception"] {
if id == exception {
/// BLE001
pub fn blind_except(
checker: &mut Checker,
type_: Option<&Expr>,
name: Option<&str>,
body: &[Stmt],
) {
let Some(type_) = type_ else {
return;
};
let ExprKind::Name { id, .. } = &type_.node else {
return;
};
for exception in ["BaseException", "Exception"] {
if id == exception && checker.is_builtin(exception) {
// If the exception is re-raised, don't flag an error.
if !body.iter().any(|stmt| {
if let StmtKind::Raise { exc, .. } = &stmt.node {
if let Some(exc) = exc {
if let ExprKind::Name { id, .. } = &exc.node {
name.map_or(false, |name| name == id)
} else {
false
}
} else {
true
}
} else {
false
}
}) {
checker.add_check(Check::new(
CheckKind::BlindExcept,
CheckKind::BlindExcept(id.to_string()),
Range::from_located(type_),
));
}

View File

@@ -2,31 +2,8 @@
source: src/flake8_blind_except/mod.rs
expression: checks
---
- kind: BlindExcept
location:
row: 5
column: 7
end_location:
row: 5
column: 16
fix: ~
- kind: BlindExcept
location:
row: 13
column: 7
end_location:
row: 13
column: 20
fix: ~
- kind: BlindExcept
location:
row: 23
column: 7
end_location:
row: 23
column: 16
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: BaseException
location:
row: 25
column: 7
@@ -34,7 +11,8 @@ expression: checks
row: 25
column: 20
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 31
column: 7
@@ -42,15 +20,8 @@ expression: checks
row: 31
column: 16
fix: ~
- kind: BlindExcept
location:
row: 36
column: 11
end_location:
row: 36
column: 24
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 42
column: 7
@@ -58,7 +29,8 @@ expression: checks
row: 42
column: 16
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: BaseException
location:
row: 45
column: 11
@@ -66,15 +38,8 @@ expression: checks
row: 45
column: 24
fix: ~
- kind: BlindExcept
location:
row: 52
column: 11
end_location:
row: 52
column: 24
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 54
column: 7
@@ -82,4 +47,22 @@ expression: checks
row: 54
column: 16
fix: ~
- kind:
BlindExcept: Exception
location:
row: 60
column: 7
end_location:
row: 60
column: 16
fix: ~
- kind:
BlindExcept: BaseException
location:
row: 62
column: 7
end_location:
row: 62
column: 20
fix: ~

View File

@@ -39,6 +39,7 @@ mod tests {
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::B904, Path::new("B904.py"); "B904")]
#[test_case(CheckCode::B905, Path::new("B905.py"); "B905")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -1,6 +1,5 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustc_hash::FxHashSet;
use rustpython_ast::{
Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location, Stmt,
};
@@ -27,9 +26,9 @@ fn duplicate_handler_exceptions<'a>(
checker: &mut Checker,
expr: &'a Expr,
elts: &'a [Expr],
) -> BTreeSet<Vec<&'a str>> {
let mut seen: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut duplicates: BTreeSet<Vec<&str>> = BTreeSet::default();
) -> FxHashSet<Vec<&'a str>> {
let mut seen: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut duplicates: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut unique_elts: Vec<&Expr> = Vec::default();
for type_ in elts {
let call_path = helpers::collect_call_paths(type_);
@@ -76,8 +75,8 @@ fn duplicate_handler_exceptions<'a>(
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut duplicates: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut seen: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut duplicates: FxHashSet<Vec<&str>> = FxHashSet::default();
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_: Some(type_), .. } = &handler.node else {
continue;

View File

@@ -23,6 +23,7 @@ pub use unused_loop_control_variable::unused_loop_control_variable;
pub use useless_comparison::useless_comparison;
pub use useless_contextlib_suppress::useless_contextlib_suppress;
pub use useless_expression::useless_expression;
pub use zip_without_explicit_strict::zip_without_explicit_strict;
mod abstract_base_class;
mod assert_false;
@@ -49,3 +50,4 @@ mod unused_loop_control_variable;
mod useless_comparison;
mod useless_contextlib_suppress;
mod useless_expression;
mod zip_without_explicit_strict;

View File

@@ -0,0 +1,31 @@
use rustpython_ast::{Expr, ExprKind, Keyword};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B905
pub fn zip_without_explicit_strict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
kwargs: &[Keyword],
) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "zip"
&& checker.is_builtin("zip")
&& !kwargs.iter().any(|keyword| {
keyword
.node
.arg
.as_ref()
.map_or(false, |name| name == "strict")
})
{
checker.add_check(Check::new(
CheckKind::ZipWithoutExplicitStrict,
Range::from_located(expr),
));
}
}
}

View File

@@ -1,10 +1,23 @@
//! Settings for the `flake8-bugbear` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Additional callable functions to consider "immutable" when evaluating, e.g.,
`no-mutable-default-argument` checks (`B006`).
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
"#
)]
pub extend_immutable_calls: Option<Vec<String>>,
}

View File

@@ -0,0 +1,61 @@
---
source: src/flake8_bugbear/mod.rs
expression: checks
---
- kind: ZipWithoutExplicitStrict
location:
row: 1
column: 0
end_location:
row: 1
column: 5
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 2
column: 0
end_location:
row: 2
column: 13
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 4
column: 15
end_location:
row: 4
column: 23
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 5
column: 4
end_location:
row: 5
column: 12
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 6
column: 0
end_location:
row: 6
column: 26
fix: ~

View File

@@ -0,0 +1,36 @@
use rustc_hash::FxHashMap;
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// ICN001
pub fn check_conventional_import(
import_from: &Stmt,
name: &str,
asname: Option<&str>,
conventions: &FxHashMap<String, String>,
) -> Option<Check> {
let mut is_valid_import = true;
if let Some(expected_alias) = conventions.get(name) {
if !expected_alias.is_empty() {
if let Some(alias) = asname {
if expected_alias != alias {
is_valid_import = false;
}
} else {
is_valid_import = false;
}
}
if !is_valid_import {
return Some(Check::new(
CheckKind::ImportAliasIsNotConventional(
name.to_string(),
expected_alias.to_string(),
),
Range::from_located(import_from),
));
}
}
None
}

View File

@@ -0,0 +1,101 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustc_hash::FxHashMap;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{flake8_import_conventions, Settings};
#[test]
fn defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/defaults.py"),
&Settings::for_rule(CheckCode::ICN001),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("defaults", checks);
Ok(())
}
#[test]
fn custom() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/custom.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([
("dask.array".to_string(), "da".to_string()),
("dask.dataframe".to_string(), "dd".to_string()),
])),
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("custom", checks);
Ok(())
}
#[test]
fn remove_defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/remove_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: Some(FxHashMap::from_iter([
("altair".to_string(), "alt".to_string()),
("matplotlib.pyplot".to_string(), "plt".to_string()),
("pandas".to_string(), "pd".to_string()),
("seaborn".to_string(), "sns".to_string()),
])),
extend_aliases: None,
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("remove_default", checks);
Ok(())
}
#[test]
fn override_defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/override_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([(
"numpy".to_string(),
"nmp".to_string(),
)])),
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("override_default", checks);
Ok(())
}
}

View File

@@ -0,0 +1,94 @@
//! Settings for import conventions.
use std::hash::{Hash, Hasher};
use itertools::Itertools;
use ruff_macros::ConfigurationOptions;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
("altair", "alt"),
("matplotlib.pyplot", "plt"),
("numpy", "np"),
("pandas", "pd"),
("seaborn", "sns"),
];
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = "The conventional aliases for imports. These aliases can be extended by the \
`extend_aliases` option.",
default = r#"{"altair": "alt", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns"}"#,
value_type = "FxHashMap<String, String>",
example = r#"
# Declare the default aliases.
altair = "alt"
matplotlib.pyplot = "plt"
numpy = "np"
pandas = "pd"
seaborn = "sns"
"#
)]
pub aliases: Option<FxHashMap<String, String>>,
#[option(
doc = "A mapping of modules to their conventional import aliases. These aliases will be \
added to the `aliases` mapping.",
default = r#"{}"#,
value_type = "FxHashMap<String, String>",
example = r#"
# Declare a custom alias for the `matplotlib` module.
"dask.dataframe" = "dd"
"#
)]
pub extend_aliases: Option<FxHashMap<String, String>>,
}
#[derive(Debug)]
pub struct Settings {
pub aliases: FxHashMap<String, String>,
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
for value in self.aliases.iter().sorted() {
value.hash(state);
}
}
}
fn default_aliases() -> FxHashMap<String, String> {
CONVENTIONAL_ALIASES
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect::<FxHashMap<_, _>>()
}
fn resolve_aliases(options: Options) -> FxHashMap<String, String> {
let mut aliases = match options.aliases {
Some(options_aliases) => options_aliases,
None => default_aliases(),
};
if let Some(extend_aliases) = options.extend_aliases {
aliases.extend(extend_aliases);
}
aliases
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
aliases: resolve_aliases(options),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
aliases: default_aliases(),
}
}
}

View File

@@ -0,0 +1,159 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.array
- da
location:
row: 4
column: 0
end_location:
row: 4
column: 17
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.dataframe
- dd
location:
row: 5
column: 0
end_location:
row: 5
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 6
column: 0
end_location:
row: 6
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 7
column: 0
end_location:
row: 7
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 8
column: 0
end_location:
row: 8
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 9
column: 0
end_location:
row: 9
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 11
column: 0
end_location:
row: 11
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 12
column: 0
end_location:
row: 12
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.array
- da
location:
row: 13
column: 0
end_location:
row: 13
column: 27
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.dataframe
- dd
location:
row: 14
column: 0
end_location:
row: 14
column: 28
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 15
column: 0
end_location:
row: 15
column: 19
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 16
column: 0
end_location:
row: 16
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 17
column: 0
end_location:
row: 17
column: 22
fix: ~

View File

@@ -0,0 +1,115 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 5
column: 0
end_location:
row: 5
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 11
column: 0
end_location:
row: 11
column: 19
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -0,0 +1,115 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- nmp
location:
row: 5
column: 0
end_location:
row: 5
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- nmp
location:
row: 11
column: 0
end_location:
row: 11
column: 18
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -0,0 +1,93 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -1,5 +1,6 @@
//! Settings for the `flake8-quotes` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -9,12 +10,55 @@ pub enum Quote {
Double,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Quote style to prefer for inline strings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
inline-quotes = "single"
"#
)]
pub inline_quotes: Option<Quote>,
#[option(
doc = r#"
Quote style to prefer for multiline strings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
multiline-quotes = "single"
"#
)]
pub multiline_quotes: Option<Quote>,
#[option(
doc = r#"
Quote style to prefer for docstrings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
docstring-quotes = "single"
"#
)]
pub docstring_quotes: Option<Quote>,
#[option(
doc = r#"
Whether to avoid using single quotes if a string contains single quotes, or vice-versa
with double quotes, as per [PEP8](https://peps.python.org/pep-0008/#string-quotes).
This minimizes the need to escape quotation marks within strings.
"#,
default = r#"true"#,
value_type = "bool",
example = r#"
# Don't bother trying to avoid escapes.
avoid-escape = false
"#
)]
pub avoid_escape: Option<bool>,
}

View File

@@ -15,12 +15,12 @@ mod tests {
#[test]
fn ban_parent_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/I252.py"),
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::Parents,
},
..Settings::for_rules(vec![CheckCode::I252])
..Settings::for_rules(vec![CheckCode::TID252])
},
true,
)?;
@@ -32,12 +32,12 @@ mod tests {
#[test]
fn ban_all_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/I252.py"),
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::All,
},
..Settings::for_rules(vec![CheckCode::I252])
..Settings::for_rules(vec![CheckCode::TID252])
},
true,
)?;

View File

@@ -1,5 +1,6 @@
//! Settings for the `flake8-tidy-imports` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -9,9 +10,21 @@ pub enum Strictness {
All,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Whether to ban all relative imports (`"all"`), or only those imports that extend into
the parent module and beyond (`"parents"`).
"#,
default = r#""parents""#,
value_type = "Strictness",
example = r#"
# Disallow all relative imports.
ban-relative-imports = "all"
"#
)]
pub ban_relative_imports: Option<Strictness>,
}

View File

@@ -0,0 +1,19 @@
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind};
pub fn is_empty(body: &[Stmt]) -> bool {
match &body {
[] => true,
// Also allow: raise NotImplementedError, raise NotImplemented
[stmt] => match &stmt.node {
StmtKind::Pass => true,
StmtKind::Expr { value } => match &value.node {
ExprKind::Constant { value, .. } => {
matches!(value, Constant::Str(_) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
},
_ => false,
}
}

View File

@@ -0,0 +1,35 @@
mod helpers;
pub mod plugins;
mod types;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::ARG001, Path::new("ARG.py"); "ARG001")]
#[test_case(CheckCode::ARG002, Path::new("ARG.py"); "ARG002")]
#[test_case(CheckCode::ARG003, Path::new("ARG.py"); "ARG003")]
#[test_case(CheckCode::ARG004, Path::new("ARG.py"); "ARG004")]
#[test_case(CheckCode::ARG005, Path::new("ARG.py"); "ARG005")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_unused_arguments")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,184 @@
use std::iter;
use regex::Regex;
use rustc_hash::FxHashMap;
use rustpython_ast::{Arg, Arguments};
use crate::ast::function_type;
use crate::ast::function_type::FunctionType;
use crate::ast::helpers::collect_arg_names;
use crate::ast::types::{Binding, BindingKind, FunctionDef, Lambda, Scope, ScopeKind};
use crate::check_ast::Checker;
use crate::flake8_unused_arguments::helpers;
use crate::flake8_unused_arguments::types::Argumentable;
use crate::{visibility, Check};
/// Check a plain function for unused arguments.
fn function(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg_name in collect_arg_names(args) {
if let Some(binding) = bindings.get(arg_name) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg_name)
{
checks.push(Check::new(
argumentable.check_for(arg_name.to_string()),
binding.range,
));
}
}
}
checks
}
/// Check a method for unused arguments.
fn method(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg in args
.posonlyargs
.iter()
.chain(args.args.iter())
.skip(1)
.chain(args.kwonlyargs.iter())
.chain(iter::once::<Option<&Arg>>(args.vararg.as_deref()).flatten())
.chain(iter::once::<Option<&Arg>>(args.kwarg.as_deref()).flatten())
{
if let Some(binding) = bindings.get(&arg.node.arg.as_str()) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{
checks.push(Check::new(
argumentable.check_for(arg.node.arg.to_string()),
binding.range,
));
}
}
}
checks
}
/// ARG001, ARG002, ARG003, ARG004, ARG005
pub fn unused_arguments(checker: &Checker, parent: &Scope, scope: &Scope) -> Vec<Check> {
match &scope.kind {
ScopeKind::Function(FunctionDef {
name,
args,
body,
decorator_list,
..
}) => {
match function_type::classify(
parent,
name,
decorator_list,
&checker.from_imports,
&checker.import_aliases,
&checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
) {
FunctionType::Function => {
if checker
.settings
.enabled
.contains(Argumentable::Function.check_code())
{
function(
&Argumentable::Function,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::Method => {
if checker
.settings
.enabled
.contains(Argumentable::Method.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::Method,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::ClassMethod => {
if checker
.settings
.enabled
.contains(Argumentable::ClassMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::ClassMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::StaticMethod => {
if checker
.settings
.enabled
.contains(Argumentable::StaticMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
function(
&Argumentable::StaticMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
}
}
ScopeKind::Lambda(Lambda { args, .. }) => {
if checker
.settings
.enabled
.contains(Argumentable::Lambda.check_code())
{
function(
&Argumentable::Lambda,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
_ => unreachable!("Expected ScopeKind::Function | ScopeKind::Lambda"),
}
}

View File

@@ -0,0 +1,77 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedFunctionArgument: self
location:
row: 8
column: 6
end_location:
row: 8
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 8
column: 12
end_location:
row: 8
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 12
column: 6
end_location:
row: 12
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 12
column: 11
end_location:
row: 12
column: 12
fix: ~
- kind:
UnusedFunctionArgument: self
location:
row: 16
column: 6
end_location:
row: 16
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 16
column: 12
end_location:
row: 16
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 20
column: 6
end_location:
row: 20
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 20
column: 11
end_location:
row: 20
column: 12
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedMethodArgument: x
location:
row: 34
column: 16
end_location:
row: 34
column: 17
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 37
column: 19
end_location:
row: 37
column: 20
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 40
column: 15
end_location:
row: 40
column: 16
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedClassMethodArgument: x
location:
row: 44
column: 15
end_location:
row: 44
column: 16
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedStaticMethodArgument: cls
location:
row: 48
column: 10
end_location:
row: 48
column: 13
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 48
column: 15
end_location:
row: 48
column: 16
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 52
column: 10
end_location:
row: 52
column: 11
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedLambdaArgument: x
location:
row: 27
column: 7
end_location:
row: 27
column: 8
fix: ~

View File

@@ -0,0 +1,32 @@
use crate::checks::{CheckCode, CheckKind};
/// An AST node that can contain arguments.
pub enum Argumentable {
Function,
Method,
ClassMethod,
StaticMethod,
Lambda,
}
impl Argumentable {
pub fn check_for(&self, name: String) -> CheckKind {
match self {
Argumentable::Function => CheckKind::UnusedFunctionArgument(name),
Argumentable::Method => CheckKind::UnusedMethodArgument(name),
Argumentable::ClassMethod => CheckKind::UnusedClassMethodArgument(name),
Argumentable::StaticMethod => CheckKind::UnusedStaticMethodArgument(name),
Argumentable::Lambda => CheckKind::UnusedLambdaArgument(name),
}
}
pub fn check_code(&self) -> &CheckCode {
match self {
Argumentable::Function => &CheckCode::ARG001,
Argumentable::Method => &CheckCode::ARG002,
Argumentable::ClassMethod => &CheckCode::ARG003,
Argumentable::StaticMethod => &CheckCode::ARG004,
Argumentable::Lambda => &CheckCode::ARG005,
}
}
}

View File

@@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
@@ -8,6 +7,7 @@ use anyhow::{anyhow, Result};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode;
@@ -83,8 +83,8 @@ pub fn iter_python_files<'a>(
/// Create tree set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)],
) -> Result<BTreeSet<&'a CheckCode>> {
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)],
) -> Result<FxHashSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
@@ -97,7 +97,7 @@ pub(crate) fn ignores_from_path<'a>(
/// Convert any path to an absolute path (based on the current working
/// directory).
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
pub fn normalize_path(path: &Path) -> PathBuf {
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
@@ -105,7 +105,7 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf {
}
/// Convert any path to an absolute path (based on the specified project root).
pub(crate) fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}
@@ -113,7 +113,7 @@ pub(crate) fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
}
/// Convert an absolute path to be relative to the current working directory.
pub(crate) fn relativize_path(path: &Path) -> Cow<str> {
pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
return path.to_string_lossy();
}

View File

@@ -559,6 +559,7 @@ mod tests {
#[test_case(Path::new("force_wrap_aliases.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("insert_empty_lines.py"))]
#[test_case(Path::new("insert_empty_lines.pyi"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("no_wrap_star.py"))]

View File

@@ -2,15 +2,79 @@
use std::collections::BTreeSet;
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports)
option.
"#,
default = r#"false"#,
value_type = "bool",
example = r#"
combine-as-imports = true
"#
)]
pub combine_as_imports: Option<bool>,
#[option(
doc = r#"
Force `import from` statements with multiple members and at least one alias (e.g.,
`import A as B`) to wrap such that every line contains exactly one member. For example,
this formatting would be retained, rather than condensing to a single line:
```py
from .utils import (
test_directory as test_directory,
test_id as test_id
)
```
"#,
default = r#"false"#,
value_type = "bool",
example = r#"
force-wrap-aliases = true
"#
)]
pub force_wrap_aliases: Option<bool>,
#[option(
doc = r#"
A list of modules to consider first-party, regardless of whether they can be identified
as such via introspection of the local filesystem.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
known-first-party = ["src"]
"#
)]
pub known_first_party: Option<Vec<String>>,
#[option(
doc = r#"
A list of modules to consider third-party, regardless of whether they can be identified
as such via introspection of the local filesystem.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
known-third-party = ["src"]
"#
)]
pub known_third_party: Option<Vec<String>>,
#[option(
doc = r#"
A list of modules to consider standard-library, in addition to those known to Ruff in
advance.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
extra-standard-library = ["path"]
"#
)]
pub extra_standard_library: Option<Vec<String>>,
}

View File

@@ -47,4 +47,34 @@ expression: checks
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0
- kind: UnsortedImports
location:
row: 39
column: 0
end_location:
row: 41
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 39
column: 0
end_location:
row: 41
column: 0

View File

@@ -0,0 +1,65 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 3
column: 0
fix:
content: "import a\nimport b\n\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
- kind: UnsortedImports
location:
row: 4
column: 0
end_location:
row: 6
column: 0
fix:
content: "import os\nimport sys\n\n"
location:
row: 4
column: 0
end_location:
row: 6
column: 0
- kind: UnsortedImports
location:
row: 14
column: 0
end_location:
row: 16
column: 0
fix:
content: "import os\nimport sys\n\n"
location:
row: 14
column: 0
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0

View File

@@ -1,3 +1,5 @@
use std::path::Path;
use rustpython_ast::{
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
ExcepthandlerKind, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, StmtKind,
@@ -20,17 +22,21 @@ pub struct Block<'a> {
}
pub struct ImportTracker<'a> {
blocks: Vec<Block<'a>>,
directives: &'a IsortDirectives,
pyi: bool,
blocks: Vec<Block<'a>>,
split_index: usize,
nested: bool,
}
impl<'a> ImportTracker<'a> {
pub fn new(directives: &'a IsortDirectives) -> Self {
pub fn new(directives: &'a IsortDirectives, path: &'a Path) -> Self {
Self {
directives,
pyi: path.extension().map_or(false, |ext| ext == "pyi"),
blocks: vec![Block::default()],
split_index: 0,
nested: false,
}
}
@@ -39,6 +45,34 @@ impl<'a> ImportTracker<'a> {
self.blocks[index].imports.push(stmt);
}
fn trailer_for(&self, stmt: &'a Stmt) -> Option<Trailer> {
if self.pyi {
// Black treats interface files differently, limiting to one newline
// (`Trailing::Sibling`), and avoiding inserting any newlines in nested function
// blocks.
if self.nested
&& matches!(
stmt.node,
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. }
)
{
None
} else {
Some(Trailer::Sibling)
}
} else if self.nested {
Some(Trailer::Sibling)
} else {
Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
})
}
}
fn finalize(&mut self, trailer: Option<Trailer>) {
let index = self.blocks.len() - 1;
if !self.blocks[index].imports.is_empty() {
@@ -60,13 +94,7 @@ where
// Track manual splits.
while self.split_index < self.directives.splits.len() {
if stmt.location.row() >= self.directives.splits[self.split_index] {
self.finalize(Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}));
self.finalize(self.trailer_for(stmt));
self.split_index += 1;
} else {
break;
@@ -81,16 +109,12 @@ where
{
self.track_import(stmt);
} else {
self.finalize(Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}));
self.finalize(self.trailer_for(stmt));
}
// Track scope.
let prev_nested = self.nested;
self.nested = true;
match &stmt.node {
StmtKind::FunctionDef { body, .. } => {
for stmt in body {
@@ -198,6 +222,7 @@ where
}
_ => {}
}
self.nested = prev_nested;
}
fn visit_annotation(&mut self, _: &'b Expr) {}
@@ -219,11 +244,16 @@ where
fn visit_comprehension(&mut self, _: &'b Comprehension) {}
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
let prev_nested = self.nested;
self.nested = true;
let ExcepthandlerKind::ExceptHandler { body, .. } = &excepthandler.node;
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize(None);
self.nested = prev_nested;
}
fn visit_arguments(&mut self, _: &'b Arguments) {}

View File

@@ -49,10 +49,12 @@ pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_debugger;
mod flake8_import_conventions;
mod flake8_print;
pub mod flake8_quotes;
mod flake8_return;
pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod fs;
mod isort;
mod lex;
@@ -70,7 +72,7 @@ mod pygrep_hooks;
mod pylint;
mod python;
mod pyupgrade;
mod rules;
mod ruff;
mod rustpython_helpers;
pub mod settings;
pub mod source_code_locator;

View File

@@ -92,6 +92,7 @@ pub(crate) fn check_path(
&directives.isort,
settings,
autofix,
path,
));
}
}

View File

@@ -17,6 +17,7 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use ::ruff::autofix::fixer;
use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, Cli};
use ::ruff::fs::iter_python_files;
@@ -29,7 +30,7 @@ use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use ::ruff::{cache, commands};
use ::ruff::{cache, commands, fs};
use anyhow::Result;
use clap::{CommandFactory, Parser};
use colored::Colorize;
@@ -37,7 +38,6 @@ use log::{debug, error};
use notify::{recommended_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff::autofix::fixer;
use rustpython_ast::Location;
use walkdir::DirEntry;
@@ -210,10 +210,12 @@ fn inner_main() -> Result<ExitCode> {
}
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&cli.files);
let pyproject = cli
.config
.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
let config: Option<PathBuf> = cli.config;
let project_root = config.as_ref().map_or_else(
|| pyproject::find_project_root(&cli.files),
|config| config.parent().map(fs::normalize_path),
);
let pyproject = config.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
// Reconcile configuration from pyproject.toml and command-line arguments.
let mut configuration =
@@ -390,7 +392,7 @@ fn inner_main() -> Result<ExitCode> {
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics)?;
printer.write_once(&diagnostics, &autofix)?;
}
// Check for updates if we're in a non-silent log level.

View File

@@ -1,10 +1,20 @@
//! Settings for the `mccabe` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = "The maximum McCabe complexity to allow before triggering `C901` errors.",
default = "10",
value_type = "usize",
example = r#"
# Flag errors (`C901`) whenever the complexity level exceeds 5.
max-complexity = 5
"#
)]
pub max_complexity: Option<usize>,
}
@@ -14,8 +24,7 @@ pub struct Settings {
}
impl Settings {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: Options) -> Self {
pub fn from_options(options: &Options) -> Self {
Self {
max_complexity: options.max_complexity.unwrap_or_default(),
}

View File

@@ -1,4 +1,3 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
@@ -7,8 +6,9 @@ use itertools::Itertools;
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::checks::{Check, CheckCode, REDIRECTS};
use crate::checks::{Check, CheckCode, CODE_REDIRECTS};
static NO_QA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -70,7 +70,7 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
pub fn includes(needle: &CheckCode, haystack: &[&str]) -> bool {
let needle: &str = needle.as_ref();
haystack.iter().any(|candidate| {
if let Some(candidate) = REDIRECTS.get(candidate) {
if let Some(candidate) = CODE_REDIRECTS.get(candidate) {
needle == candidate.as_ref()
} else {
&needle == candidate
@@ -83,7 +83,7 @@ pub fn add_noqa(
checks: &[Check],
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
external: &BTreeSet<String>,
external: &FxHashSet<String>,
) -> Result<usize> {
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for, external);
fs::write(path, output)?;
@@ -94,16 +94,16 @@ fn add_noqa_inner(
checks: &[Check],
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
external: &BTreeSet<String>,
external: &FxHashSet<String>,
) -> (usize, String) {
let mut matches_by_line: BTreeMap<usize, BTreeSet<&CheckCode>> = BTreeMap::new();
let mut matches_by_line: FxHashMap<usize, FxHashSet<&CheckCode>> = FxHashMap::default();
for (lineno, line) in contents.lines().enumerate() {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(line) {
return (0, contents.to_string());
}
let mut codes: BTreeSet<&CheckCode> = BTreeSet::new();
let mut codes: FxHashSet<&CheckCode> = FxHashSet::default();
for check in checks {
if check.location.row() == lineno + 1 {
codes.insert(check.kind.code());
@@ -117,7 +117,7 @@ fn add_noqa_inner(
if !codes.is_empty() {
let matches = matches_by_line.entry(noqa_lineno).or_default();
matches.append(&mut codes);
matches.extend(codes);
}
}
@@ -199,9 +199,9 @@ fn add_noqa_inner(
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use nohash_hasher::IntMap;
use rustc_hash::FxHashSet;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
@@ -227,7 +227,7 @@ mod tests {
let checks = vec![];
let contents = "x = 1";
let noqa_line_for = IntMap::default();
let external = BTreeSet::default();
let external = FxHashSet::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for, &external);
assert_eq!(count, 0);
assert_eq!(output.trim(), contents.trim());
@@ -241,7 +241,7 @@ mod tests {
)];
let contents = "x = 1";
let noqa_line_for = IntMap::default();
let external = BTreeSet::default();
let external = FxHashSet::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for, &external);
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: F841".trim());
@@ -264,7 +264,7 @@ mod tests {
];
let contents = "x = 1 # noqa: E741";
let noqa_line_for = IntMap::default();
let external = BTreeSet::default();
let external = FxHashSet::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for, &external);
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
@@ -287,7 +287,7 @@ mod tests {
];
let contents = "x = 1 # noqa";
let noqa_line_for = IntMap::default();
let external = BTreeSet::default();
let external = FxHashSet::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for, &external);
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());

View File

@@ -1,10 +1,10 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::function_type;
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
use crate::python::string::{self};
@@ -58,15 +58,16 @@ pub fn invalid_first_argument_name_for_class_method(
settings: &Settings,
) -> Option<Check> {
if !matches!(
helpers::function_type(
function_type::classify(
scope,
name,
decorator_list,
from_imports,
import_aliases,
settings,
&settings.classmethod_decorators,
&settings.staticmethod_decorators,
),
FunctionType::ClassMethod
function_type::FunctionType::ClassMethod
) {
return None;
}
@@ -99,15 +100,16 @@ pub fn invalid_first_argument_name_for_method(
settings: &Settings,
) -> Option<Check> {
if !matches!(
helpers::function_type(
function_type::classify(
scope,
name,
decorator_list,
from_imports,
import_aliases,
settings,
&settings.classmethod_decorators,
&settings.staticmethod_decorators,
),
FunctionType::Method
function_type::FunctionType::Method
) {
return None;
}

View File

@@ -1,71 +1,10 @@
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, Stmt, StmtKind};
use rustpython_ast::{Stmt, StmtKind};
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::ast::helpers::{collect_call_paths, match_call_path};
use crate::python::string::{is_lower, is_upper};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn function_type(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
settings: &Settings,
) -> FunctionType {
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
settings.staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
}
pub fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}

View File

@@ -1,5 +1,6 @@
//! Settings for the `pep8-naming` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
const IGNORE_NAMES: [&str; 12] = [
@@ -21,11 +22,47 @@ const CLASSMETHOD_DECORATORS: [&str; 1] = ["classmethod"];
const STATICMETHOD_DECORATORS: [&str; 1] = ["staticmethod"];
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
A list of names to ignore when considering `pep8-naming` violations.
"#,
default = r#"["setUp", "tearDown", "setUpClass", "tearDownClass", "setUpModule", "tearDownModule", "asyncSetUp", "asyncTearDown", "setUpTestData", "failureException", "longMessage", "maxDiff"]"#,
value_type = "Vec<String>",
example = r#"
ignore-names = ["callMethod"]
"#
)]
pub ignore_names: Option<Vec<String>>,
#[option(
doc = r#"
A list of decorators that, when applied to a method, indicate that the method should be
treated as a class method. For example, Ruff will expect that any method decorated by a
decorator in this list takes a `cls` argument as its first argument.
"#,
default = r#"["classmethod"]"#,
value_type = "Vec<String>",
example = r#"
# Allow Pydantic's `@validator` decorator to trigger class method treatment.
classmethod-decorators = ["classmethod", "pydantic.validator"]
"#
)]
pub classmethod_decorators: Option<Vec<String>>,
#[option(
doc = r#"
A list of decorators that, when applied to a method, indicate that the method should be
treated as a static method. For example, Ruff will expect that any method decorated by a
decorator in this list has no `self` or `cls` argument.
"#,
default = r#"["staticmethod"]"#,
value_type = "Vec<String>",
example = r#"
# Allow a shorthand alias, `@stcmthd`, to trigger static method treatment.
staticmethod-decorators = ["staticmethod", "stcmthd"]
"#
)]
pub staticmethod_decorators: Option<Vec<String>>,
}

View File

@@ -9,7 +9,7 @@ use itertools::iterate;
use rustpython_parser::ast::Location;
use serde::Serialize;
use crate::autofix::Fix;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckCode;
use crate::fs::relativize_path;
use crate::linter::Diagnostics;
@@ -57,15 +57,15 @@ impl<'a> Printer<'a> {
}
}
fn post_text(&self, num_fixable: usize) {
fn post_text(&self, num_fixable: usize, autofix: &fixer::Mode) {
if self.log_level >= &LogLevel::Default {
if num_fixable > 0 {
if num_fixable > 0 && !matches!(autofix, fixer::Mode::Apply) {
println!("{num_fixable} potentially fixable with the --fix option.");
}
}
}
pub fn write_once(&self, diagnostics: &Diagnostics) -> Result<()> {
pub fn write_once(&self, diagnostics: &Diagnostics, autofix: &fixer::Mode) -> Result<()> {
if matches!(self.log_level, LogLevel::Silent) {
return Ok(());
}
@@ -147,7 +147,7 @@ impl<'a> Printer<'a> {
print_message(message);
}
self.post_text(num_fixable);
self.post_text(num_fixable, autofix);
}
SerializationFormat::Grouped => {
self.pre_text(diagnostics);
@@ -190,7 +190,7 @@ impl<'a> Printer<'a> {
println!();
}
self.post_text(num_fixable);
self.post_text(num_fixable, autofix);
}
SerializationFormat::Github => {
self.pre_text(diagnostics);

View File

@@ -1,11 +1,65 @@
use itertools::izip;
use rustpython_ast::Location;
use rustpython_ast::{Location, Stmt, StmtKind};
use rustpython_parser::ast::{Cmpop, Expr, ExprKind};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::source_code_locator::SourceCodeLocator;
/// E721
pub fn type_comparison(ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
if !matches!(op, Cmpop::Is | Cmpop::IsNot | Cmpop::Eq | Cmpop::NotEq) {
continue;
}
match &right.node {
ExprKind::Call { func, args, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
// Ex) type(False)
if id == "type" {
if let Some(arg) = args.first() {
// Allow comparison for types which are not obvious.
if !matches!(arg.node, ExprKind::Name { .. }) {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
}
}
ExprKind::Attribute { value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// Ex) types.IntType
if id == "types" {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
_ => {}
}
}
checks
}
/// E722
pub fn do_not_use_bare_except(
type_: Option<&Expr>,
body: &[Stmt],
location: Range,
) -> Option<Check> {
if type_.is_none()
&& !body
.iter()
.any(|stmt| matches!(stmt.node, StmtKind::Raise { exc: None, .. }))
{
Some(Check::new(CheckKind::DoNotUseBareExcept, location))
} else {
None
}
}
fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
@@ -46,43 +100,6 @@ pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
}
}
/// E721
pub fn type_comparison(ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for (op, right) in izip!(ops, comparators) {
if !matches!(op, Cmpop::Is | Cmpop::IsNot | Cmpop::Eq | Cmpop::NotEq) {
continue;
}
match &right.node {
ExprKind::Call { func, args, .. } => {
if let ExprKind::Name { id, .. } = &func.node {
// Ex) type(False)
if id == "type" {
if let Some(arg) = args.first() {
// Allow comparison for types which are not obvious.
if !matches!(arg.node, ExprKind::Name { .. }) {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
}
}
ExprKind::Attribute { value, .. } => {
if let ExprKind::Name { id, .. } = &value.node {
// Ex) types.IntType
if id == "types" {
checks.push(Check::new(CheckKind::TypeComparison, location));
}
}
}
_ => {}
}
}
checks
}
// See: https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals
const VALID_ESCAPE_SEQUENCES: &[char; 23] = &[
'\n', '\\', '\'', '"', 'a', 'b', 'f', 'n', 'r', 't', 'v', '0', '1', '2', '3', '4', '5', '6',

43
src/pydocstyle/helpers.rs Normal file
View File

@@ -0,0 +1,43 @@
use rustpython_ast::Expr;
use crate::ast::types::Range;
use crate::docstrings::constants;
use crate::SourceCodeLocator;
/// Return the leading quote string for a docstring (e.g., `"""`).
pub fn leading_quote<'a>(docstring: &Expr, locator: &'a SourceCodeLocator) -> Option<&'a str> {
if let Some(first_line) = locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(str::to_lowercase)
{
for pattern in constants::TRIPLE_QUOTE_PREFIXES
.iter()
.chain(constants::SINGLE_QUOTE_PREFIXES)
{
if first_line.starts_with(pattern) {
return Some(pattern);
}
}
}
None
}
/// Return the index of the first logical line in a string.
pub fn logical_line(content: &str) -> Option<usize> {
// Find the first logical line.
let mut logical_line = None;
for (i, line) in content.lines().enumerate() {
if line.trim().is_empty() {
// Empty line. If this is the line _after_ the first logical line, stop.
if logical_line.is_some() {
break;
}
} else {
// Non-empty line. Store the index.
logical_line = Some(i);
}
}
logical_line
}

View File

@@ -1,3 +1,4 @@
mod helpers;
pub mod plugins;
#[cfg(test)]
@@ -36,7 +37,8 @@ mod tests {
#[test_case(CheckCode::D214, Path::new("sections.py"); "D214")]
#[test_case(CheckCode::D215, Path::new("sections.py"); "D215")]
#[test_case(CheckCode::D300, Path::new("D.py"); "D300")]
#[test_case(CheckCode::D400, Path::new("D.py"); "D400")]
#[test_case(CheckCode::D400, Path::new("D.py"); "D400_0")]
#[test_case(CheckCode::D400, Path::new("D400.py"); "D400_1")]
#[test_case(CheckCode::D402, Path::new("D.py"); "D402")]
#[test_case(CheckCode::D403, Path::new("D.py"); "D403")]
#[test_case(CheckCode::D404, Path::new("D.py"); "D404")]

View File

@@ -1,5 +1,3 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -7,8 +5,8 @@ use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range;
use crate::ast::whitespace;
use crate::ast::whitespace::LinesWithTrailingNewline;
use crate::ast::{cast, whitespace};
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
@@ -16,6 +14,7 @@ use crate::docstrings::constants;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::sections::{section_contexts, SectionContext};
use crate::docstrings::styles::SectionStyle;
use crate::pydocstyle::helpers::{leading_quote, logical_line};
use crate::visibility::{is_init, is_magic, is_overload, is_override, is_staticmethod, Visibility};
/// D100, D101, D102, D103, D104, D105, D106, D107
@@ -76,7 +75,7 @@ pub fn not_missing(
false
}
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
if is_overload(checker, stmt) {
if is_overload(checker, cast::decorator_list(stmt)) {
true
} else {
if checker.settings.enabled.contains(&CheckCode::D103) {
@@ -89,7 +88,9 @@ pub fn not_missing(
}
}
DefinitionKind::Method(stmt) => {
if is_overload(checker, stmt) || is_override(checker, stmt) {
if is_overload(checker, cast::decorator_list(stmt))
|| is_override(checker, cast::decorator_list(stmt))
{
true
} else if is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::D105) {
@@ -621,18 +622,11 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition)
Range::from_located(docstring),
);
if checker.patch(check.kind.code()) {
if let Some(first_line) = checker
.locator
.slice_source_code_range(&Range::from_located(docstring))
.lines()
.next()
.map(str::to_lowercase)
{
for pattern in constants::TRIPLE_QUOTE_PREFIXES
.iter()
.chain(constants::SINGLE_QUOTE_PREFIXES)
{
if first_line.starts_with(pattern) {
if let Some(pattern) = leading_quote(docstring, checker.locator) {
if let Some(quote) = pattern.chars().last() {
// If removing whitespace would lead to an invalid string of quote
// characters, avoid applying the fix.
if !trimmed.ends_with(quote) {
check.amend(Fix::replacement(
trimmed.to_string(),
Location::new(
@@ -644,7 +638,6 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition)
docstring.location.column() + pattern.len() + line.chars().count(),
),
));
break;
}
}
}
@@ -743,16 +736,30 @@ pub fn ends_with_period(checker: &mut Checker, definition: &Definition) {
} = &docstring.node else {
return;
};
let Some(string) = string.trim().lines().next() else {
return;
};
if string.ends_with('.') {
return;
};
checker.add_check(Check::new(
CheckKind::EndsInPeriod,
Range::from_located(docstring),
));
if let Some(index) = logical_line(string) {
let line = string.lines().nth(index).unwrap();
let trimmed = line.trim_end();
if !trimmed.ends_with('.') {
let mut check = Check::new(CheckKind::EndsInPeriod, Range::from_located(docstring));
// Best-effort autofix: avoid adding a period after other punctuation marks.
if checker.patch(&CheckCode::D400) && !trimmed.ends_with(':') && !trimmed.ends_with(';')
{
if let Some((row, column)) = if index == 0 {
leading_quote(docstring, checker.locator).map(|pattern| {
(
docstring.location.row(),
docstring.location.column() + pattern.len() + trimmed.chars().count(),
)
})
} else {
Some((docstring.location.row() + index, trimmed.chars().count()))
} {
check.amend(Fix::insertion(".".to_string(), Location::new(row, column)));
}
}
checker.add_check(check);
};
}
}
/// D402
@@ -870,16 +877,31 @@ pub fn ends_with_punctuation(checker: &mut Checker, definition: &Definition) {
} = &docstring.node else {
return
};
let Some(string) = string.trim().lines().next() else {
return
};
if string.ends_with('.') || string.ends_with('!') || string.ends_with('?') {
return;
if let Some(index) = logical_line(string) {
let line = string.lines().nth(index).unwrap();
let trimmed = line.trim_end();
if !(trimmed.ends_with('.') || trimmed.ends_with('!') || trimmed.ends_with('?')) {
let mut check =
Check::new(CheckKind::EndsInPunctuation, Range::from_located(docstring));
// Best-effort autofix: avoid adding a period after other punctuation marks.
if checker.patch(&CheckCode::D415) && !trimmed.ends_with(':') && !trimmed.ends_with(';')
{
if let Some((row, column)) = if index == 0 {
leading_quote(docstring, checker.locator).map(|pattern| {
(
docstring.location.row(),
docstring.location.column() + pattern.len() + trimmed.chars().count(),
)
})
} else {
Some((docstring.location.row() + index, trimmed.chars().count()))
} {
check.amend(Fix::insertion(".".to_string(), Location::new(row, column)));
}
}
checker.add_check(check);
};
}
checker.add_check(Check::new(
CheckKind::EndsInPunctuation,
Range::from_located(docstring),
));
}
/// D418
@@ -894,7 +916,7 @@ pub fn if_needed(checker: &mut Checker, definition: &Definition) {
) = definition.kind else {
return
};
if !is_overload(checker, stmt) {
if !is_overload(checker, cast::decorator_list(stmt)) {
return;
}
checker.add_check(Check::new(
@@ -1378,7 +1400,7 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
};
// Look for arguments that weren't included in the docstring.
let mut missing_arg_names: BTreeSet<String> = BTreeSet::default();
let mut missing_arg_names: FxHashSet<String> = FxHashSet::default();
for arg in arguments
.args
.iter()
@@ -1388,7 +1410,7 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
// If this is a non-static method, skip `cls` or `self`.
usize::from(
matches!(definition.kind, DefinitionKind::Method(_))
&& !is_staticmethod(checker, parent),
&& !is_staticmethod(checker, cast::decorator_list(parent)),
),
)
{

View File

@@ -47,4 +47,12 @@ expression: checks
end_location:
row: 299
column: 36
- kind: NoSurroundingWhitespace
location:
row: 581
column: 4
end_location:
row: 581
column: 51
fix: ~

View File

@@ -9,7 +9,14 @@ expression: checks
end_location:
row: 355
column: 17
fix: ~
fix:
content: "."
location:
row: 355
column: 14
end_location:
row: 355
column: 14
- kind: EndsInPeriod
location:
row: 406
@@ -17,7 +24,14 @@ expression: checks
end_location:
row: 406
column: 39
fix: ~
fix:
content: "."
location:
row: 406
column: 36
end_location:
row: 406
column: 36
- kind: EndsInPeriod
location:
row: 410
@@ -25,7 +39,14 @@ expression: checks
end_location:
row: 410
column: 24
fix: ~
fix:
content: "."
location:
row: 410
column: 21
end_location:
row: 410
column: 21
- kind: EndsInPeriod
location:
row: 416
@@ -33,7 +54,14 @@ expression: checks
end_location:
row: 416
column: 24
fix: ~
fix:
content: "."
location:
row: 416
column: 21
end_location:
row: 416
column: 21
- kind: EndsInPeriod
location:
row: 422
@@ -41,7 +69,14 @@ expression: checks
end_location:
row: 422
column: 49
fix: ~
fix:
content: "."
location:
row: 422
column: 46
end_location:
row: 422
column: 46
- kind: EndsInPeriod
location:
row: 429
@@ -49,7 +84,14 @@ expression: checks
end_location:
row: 429
column: 63
fix: ~
fix:
content: "."
location:
row: 429
column: 60
end_location:
row: 429
column: 60
- kind: EndsInPeriod
location:
row: 470
@@ -57,7 +99,14 @@ expression: checks
end_location:
row: 470
column: 24
fix: ~
fix:
content: "."
location:
row: 470
column: 21
end_location:
row: 470
column: 21
- kind: EndsInPeriod
location:
row: 475
@@ -65,7 +114,14 @@ expression: checks
end_location:
row: 475
column: 24
fix: ~
fix:
content: "."
location:
row: 475
column: 21
end_location:
row: 475
column: 21
- kind: EndsInPeriod
location:
row: 480
@@ -73,7 +129,14 @@ expression: checks
end_location:
row: 480
column: 24
fix: ~
fix:
content: "."
location:
row: 480
column: 21
end_location:
row: 480
column: 21
- kind: EndsInPeriod
location:
row: 487
@@ -81,7 +144,14 @@ expression: checks
end_location:
row: 487
column: 24
fix: ~
fix:
content: "."
location:
row: 487
column: 21
end_location:
row: 487
column: 21
- kind: EndsInPeriod
location:
row: 509
@@ -89,7 +159,14 @@ expression: checks
end_location:
row: 509
column: 34
fix: ~
fix:
content: "."
location:
row: 509
column: 31
end_location:
row: 509
column: 31
- kind: EndsInPeriod
location:
row: 514
@@ -97,7 +174,14 @@ expression: checks
end_location:
row: 514
column: 33
fix: ~
fix:
content: "."
location:
row: 514
column: 30
end_location:
row: 514
column: 30
- kind: EndsInPeriod
location:
row: 520
@@ -105,5 +189,27 @@ expression: checks
end_location:
row: 520
column: 32
fix: ~
fix:
content: "."
location:
row: 520
column: 29
end_location:
row: 520
column: 29
- kind: EndsInPeriod
location:
row: 581
column: 4
end_location:
row: 581
column: 51
fix:
content: "."
location:
row: 581
column: 47
end_location:
row: 581
column: 47

View File

@@ -0,0 +1,185 @@
---
source: src/pydocstyle/mod.rs
expression: checks
---
- kind: EndsInPeriod
location:
row: 2
column: 4
end_location:
row: 2
column: 36
fix:
content: "."
location:
row: 2
column: 35
end_location:
row: 2
column: 35
- kind: EndsInPeriod
location:
row: 7
column: 4
end_location:
row: 7
column: 40
fix:
content: "."
location:
row: 7
column: 37
end_location:
row: 7
column: 37
- kind: EndsInPeriod
location:
row: 12
column: 4
end_location:
row: 15
column: 7
fix:
content: "."
location:
row: 14
column: 28
end_location:
row: 14
column: 28
- kind: EndsInPeriod
location:
row: 20
column: 4
end_location:
row: 20
column: 40
fix:
content: "."
location:
row: 20
column: 37
end_location:
row: 20
column: 37
- kind: EndsInPeriod
location:
row: 25
column: 4
end_location:
row: 27
column: 31
fix:
content: "."
location:
row: 27
column: 28
end_location:
row: 27
column: 28
- kind: EndsInPeriod
location:
row: 32
column: 4
end_location:
row: 34
column: 52
fix:
content: "."
location:
row: 34
column: 48
end_location:
row: 34
column: 48
- kind: EndsInPeriod
location:
row: 40
column: 4
end_location:
row: 40
column: 37
fix:
content: "."
location:
row: 40
column: 36
end_location:
row: 40
column: 36
- kind: EndsInPeriod
location:
row: 45
column: 4
end_location:
row: 45
column: 41
fix:
content: "."
location:
row: 45
column: 38
end_location:
row: 45
column: 38
- kind: EndsInPeriod
location:
row: 50
column: 4
end_location:
row: 53
column: 7
fix:
content: "."
location:
row: 52
column: 28
end_location:
row: 52
column: 28
- kind: EndsInPeriod
location:
row: 58
column: 4
end_location:
row: 58
column: 41
fix:
content: "."
location:
row: 58
column: 38
end_location:
row: 58
column: 38
- kind: EndsInPeriod
location:
row: 63
column: 4
end_location:
row: 65
column: 31
fix:
content: "."
location:
row: 65
column: 28
end_location:
row: 65
column: 28
- kind: EndsInPeriod
location:
row: 70
column: 4
end_location:
row: 72
column: 52
fix:
content: "."
location:
row: 72
column: 48
end_location:
row: 72
column: 48

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