Compare commits

...

10 Commits

Author SHA1 Message Date
Charlie Marsh
1822b57ed5 Remove 'static 2022-12-27 21:57:32 -05:00
Charlie Marsh
c679570041 Bump version to 0.0.198 2022-12-27 21:39:53 -05:00
Charlie Marsh
edcb3a7217 Support --select ALL to enable all error codes (#1418) 2022-12-27 21:38:26 -05:00
Charlie Marsh
6e43dc7270 Add nbQA support to the docs (#1417) 2022-12-27 21:24:07 -05:00
Charlie Marsh
570d0864f2 Add rule to detect keyword arguments before starred arguments (#1416) 2022-12-27 21:17:22 -05:00
Charlie Marsh
d22e96916c Automatically detect and respect indentation and quotation code style (#1413) 2022-12-27 19:45:50 -05:00
Charlie Marsh
043d31dcdf Bump version to 0.0.197 2022-12-27 17:05:15 -05:00
Charlie Marsh
1392e4cced Default to double quotes in code_gen.rs (#1412) 2022-12-27 16:17:49 -05:00
Charlie Marsh
59ee89a091 Fix it_converts_docstring_conventions test 2022-12-27 15:41:29 -05:00
Charlie Marsh
6a7c3728ee Set convention in flake8-to-ruff (#1410) 2022-12-27 13:51:24 -05:00
48 changed files with 1366 additions and 191 deletions

View File

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

8
Cargo.lock generated
View File

@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.196-dev.0"
version = "0.0.198-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1878,7 +1878,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.196"
version = "0.0.198"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1945,7 +1945,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.196"
version = "0.0.198"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1966,7 +1966,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.196"
version = "0.0.198"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.196"
version = "0.0.198"
edition = "2021"
rust-version = "1.65.0"
@@ -45,7 +45,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
quick-junit = { version = "0.3.2" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.196", path = "ruff_macros" }
ruff_macros = { version = "0.0.198", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }

View File

@@ -167,7 +167,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.196'
rev: 'v0.0.198'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -225,8 +225,8 @@ max-complexity = 10
```
As an example, the following would configure Ruff to: (1) avoid checking for line-length
violations (`E501`); (2), always autofix, but never remove unused imports (`F401`); and (3) ignore
import-at-top-of-file errors (`E402`) in `__init__.py` files:
violations (`E501`); (2) never remove unused imports (`F401`); and (3) ignore import-at-top-of-file
errors (`E402`) in `__init__.py` files:
```toml
[tool.ruff]
@@ -236,8 +236,7 @@ select = ["E", "F"]
# Never enforce `E501` (line length violations).
ignore = ["E501"]
# Always autofix, but never try to fix `F401` (unused imports).
fix = true
# Never try to fix `F401` (unused imports).
unfixable = ["F401"]
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
@@ -257,6 +256,17 @@ select = ["E", "F", "Q"]
docstring-quotes = "double"
```
Ruff mirrors Flake8's error code system, in which each error code consists of a one-to-three letter
prefix, followed by three digits (e.g., `F401`). The prefix indicates that "source" of the error
code (e.g., `F` for Pyflakes, `E` for `pycodestyle`, `ANN` for `flake8-annotations`). The set of
enabled errors is determined by the `select` and `ignore` options, which support both the full
error code (e.g., `F401`) and the prefix (e.g., `F`).
As a special-case, Ruff also supports the `ALL` error code, which enables all error codes. Note that
some of the `pydocstyle` error codes are conflicting (e.g., `D203` and `D211`) as they represent
alternative docstring formats. Enabling `ALL` without further configuration may result in suboptimal
behavior, especially for the `pydocstyle` plugin.
As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` file, which implements
an equivalent schema (though the `[tool.ruff]` hierarchy can be omitted). For example, the above
`pyproject.toml` described above would be represented via the following `ruff.toml`:
@@ -984,6 +994,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| RUF001 | AmbiguousUnicodeCharacterString | String contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 |
| RUF002 | AmbiguousUnicodeCharacterDocstring | Docstring contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 |
| RUF003 | AmbiguousUnicodeCharacterComment | Comment contains ambiguous unicode character '𝐁' (did you mean 'B'?) | |
| RUF004 | KeywordArgumentBeforeStarArgument | Keyword argument `...` must come after starred arguments | |
| RUF100 | UnusedNOQA | Unused blanket `noqa` directive | 🛠 |
<!-- End auto-generated sections. -->
@@ -1368,6 +1379,22 @@ src = ["src", "tests"]
known-first-party = ["my_module1", "my_module2"]
```
### Does Ruff support Jupyter Notebooks?
Ruff is integrated into [nbQA](https://github.com/nbQA-dev/nbQA), a tool for running linters and
code formatters over Jupyter Notebooks.
After installing `ruff` and `nbqa`, you can run Ruff over a notebook like so:
```shell
> nbqa ruff Untitled.ipynb
Untitled.ipynb:cell_1:2:5: F841 Local variable `x` is assigned to but never used
Untitled.ipynb:cell_2:1:1: E402 Module level import not at top of file
Untitled.ipynb:cell_2:1:8: F401 `os` imported but unused
Found 3 error(s).
1 potentially fixable with the --fix option.
```
### Does Ruff support NumPy- or Google-style docstrings?
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and

View File

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

View File

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

View File

@@ -4,11 +4,12 @@ use anyhow::Result;
use ruff::checks_gen::CheckCodePrefix;
use ruff::flake8_quotes::settings::Quote;
use ruff::flake8_tidy_imports::settings::Strictness;
use ruff::pydocstyle::settings::Convention;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_quotes, flake8_tidy_imports, mccabe,
pep8_naming,
pep8_naming, pydocstyle,
};
use crate::black::Black;
@@ -91,6 +92,7 @@ pub fn convert(
let mut flake8_tidy_imports = flake8_tidy_imports::settings::Options::default();
let mut mccabe = mccabe::settings::Options::default();
let mut pep8_naming = pep8_naming::settings::Options::default();
let mut pydocstyle = pydocstyle::settings::Options::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
@@ -200,9 +202,12 @@ pub fn convert(
_ => eprintln!("Unexpected '{key}' value: {value}"),
},
// flake8-docstrings
"docstring-convention" => {
// No-op (handled above).
}
"docstring-convention" => match value.trim() {
"google" => pydocstyle.convention = Some(Convention::Google),
"numpy" => pydocstyle.convention = Some(Convention::Numpy),
"pep257" | "all" => pydocstyle.convention = None,
_ => eprintln!("Unexpected '{key}' value: {value}"),
},
// mccabe
"max-complexity" | "max_complexity" => match value.clone().parse::<usize>() {
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
@@ -247,6 +252,9 @@ pub fn convert(
if pep8_naming != pep8_naming::settings::Options::default() {
options.pep8_naming = Some(pep8_naming);
}
if pydocstyle != pydocstyle::settings::Options::default() {
options.pydocstyle = Some(pydocstyle);
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = black {
@@ -271,9 +279,10 @@ mod tests {
use anyhow::Result;
use ruff::checks_gen::CheckCodePrefix;
use ruff::flake8_quotes;
use ruff::pydocstyle::settings::Convention;
use ruff::settings::options::Options;
use ruff::settings::pyproject::Pyproject;
use ruff::{flake8_quotes, pydocstyle};
use crate::converter::convert;
use crate::plugin::Plugin;
@@ -659,7 +668,9 @@ mod tests {
isort: None,
mccabe: None,
pep8_naming: None,
pydocstyle: None,
pydocstyle: Some(pydocstyle::settings::Options {
convention: Some(Convention::Numpy),
}),
pyupgrade: None,
});
assert_eq!(actual, expected);

13
resources/test/fixtures/ruff/RUF004.py vendored Normal file
View File

@@ -0,0 +1,13 @@
def f(*args, **kwargs):
pass
a = (1, 2)
b = (3, 4)
c = (5, 6)
f(a, b)
f(a, kw=b)
f(*a, kw=b)
f(kw=a, *b)
f(kw=a, *b, *c)

View File

@@ -377,6 +377,7 @@
"A001",
"A002",
"A003",
"ALL",
"ANN",
"ANN0",
"ANN00",
@@ -805,6 +806,7 @@
"RUF001",
"RUF002",
"RUF003",
"RUF004",
"RUF1",
"RUF10",
"RUF100",

View File

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

View File

@@ -13,6 +13,8 @@ use itertools::Itertools;
use ruff::checks::{CheckCode, PREFIX_REDIRECTS};
use strum::IntoEnumIterator;
const ALL: &str = "ALL";
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
@@ -34,9 +36,15 @@ pub fn main(cli: &Cli) -> Result<()> {
let code_suffix_len = code_str.len() - code_prefix_len;
for i in 0..=code_suffix_len {
let prefix = code_str[..code_prefix_len + i].to_string();
let entry = prefix_to_codes.entry(prefix).or_default();
entry.insert(check_code.clone());
prefix_to_codes
.entry(prefix)
.or_default()
.insert(check_code.clone());
}
prefix_to_codes
.entry(ALL.to_string())
.or_default()
.insert(check_code.clone());
}
// Add any prefix aliases (e.g., "U" to "UP").
@@ -79,6 +87,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.derive("Eq")
.derive("PartialOrd")
.derive("Ord")
.push_variant(Variant::new("None"))
.push_variant(Variant::new("Zero"))
.push_variant(Variant::new("One"))
.push_variant(Variant::new("Two"))
@@ -129,14 +138,18 @@ pub fn main(cli: &Cli) -> Result<()> {
.line("#[allow(clippy::match_same_arms)]")
.line("match self {");
for prefix in prefix_to_codes.keys() {
let num_numeric = prefix.chars().filter(|char| char.is_numeric()).count();
let specificity = match num_numeric {
0 => "Zero",
1 => "One",
2 => "Two",
3 => "Three",
4 => "Four",
_ => panic!("Invalid prefix: {prefix}"),
let specificity = if prefix == "ALL" {
"None"
} else {
let num_numeric = prefix.chars().filter(|char| char.is_numeric()).count();
match num_numeric {
0 => "Zero",
1 => "One",
2 => "Two",
3 => "Three",
4 => "Four",
_ => panic!("Invalid prefix: {prefix}"),
}
};
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => SuffixLength::{specificity},"

View File

@@ -5,7 +5,9 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::code_gen::SourceGenerator;
use ruff::source_code_generator::SourceCodeGenerator;
use ruff::source_code_locator::SourceCodeLocator;
use ruff::source_code_style::SourceCodeStyleDetector;
use rustpython_parser::parser;
#[derive(Args)]
@@ -18,7 +20,9 @@ pub struct Cli {
pub fn main(cli: &Cli) -> Result<()> {
let contents = fs::read_to_string(&cli.file)?;
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
let mut generator = SourceGenerator::new();
let locator = SourceCodeLocator::new(&contents);
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_suite(&python_ast);
println!("{}", generator.generate()?);
Ok(())

View File

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

View File

@@ -33,14 +33,16 @@ use crate::python::typing::SubscriptKind;
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::vendored::cformat::{CFormatError, CFormatErrorType};
use crate::source_code_style::SourceCodeStyleDetector;
use crate::vendor::cformat::{CFormatError, CFormatErrorType};
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
flake8_debugger, flake8_errmsg, flake8_import_conventions, flake8_print, flake8_return,
flake8_simplify, flake8_tidy_imports, flake8_unused_arguments, mccabe, noqa, pandas_vet,
pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, visibility,
pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff,
visibility,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -56,6 +58,7 @@ pub struct Checker<'a> {
pub(crate) settings: &'a Settings,
pub(crate) noqa_line_for: &'a IntMap<usize, usize>,
pub(crate) locator: &'a SourceCodeLocator<'a>,
pub(crate) style: &'a SourceCodeStyleDetector<'a>,
// Computed checks.
checks: Vec<Check>,
// Function and class definition tracking (e.g., for docstring enforcement).
@@ -107,6 +110,7 @@ impl<'a> Checker<'a> {
noqa: flags::Noqa,
path: &'a Path,
locator: &'a SourceCodeLocator,
style: &'a SourceCodeStyleDetector,
) -> Checker<'a> {
Checker {
settings,
@@ -115,6 +119,7 @@ impl<'a> Checker<'a> {
noqa,
path,
locator,
style,
checks: vec![],
definitions: vec![],
deletions: FxHashSet::default(),
@@ -1642,27 +1647,34 @@ where
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::UP003) {
pyupgrade::plugins::type_of_primitive(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::UP005) {
pyupgrade::plugins::deprecated_unittest_alias(self, func);
}
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);
}
if self.settings.enabled.contains(&CheckCode::UP015) {
pyupgrade::plugins::redundant_open_modes(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP016) {
pyupgrade::plugins::remove_six_compat(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP018) {
pyupgrade::plugins::native_literals(self, expr, func, args, keywords);
}
if self.settings.enabled.contains(&CheckCode::UP020) {
pyupgrade::plugins::open_alias(self, expr, func);
}
if self.settings.enabled.contains(&CheckCode::UP021) {
pyupgrade::plugins::replace_universal_newlines(self, expr, keywords);
}
// flake8-super
if self.settings.enabled.contains(&CheckCode::UP008) {
pyupgrade::plugins::super_call_with_parameters(self, expr, func, args);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
@@ -1725,7 +1737,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C401) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_generator_set(
expr,
@@ -1739,7 +1750,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C402) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_generator_dict(
expr,
@@ -1753,7 +1763,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C403) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_list_comprehension_set(
@@ -1769,7 +1778,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C404) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_list_comprehension_dict(
@@ -1785,7 +1793,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C405) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_literal_set(
expr,
@@ -1799,7 +1806,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C406) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_literal_dict(
expr,
@@ -1813,7 +1819,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C408) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_collection_call(
expr,
@@ -1827,7 +1832,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C409) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_literal_within_tuple_call(
@@ -1842,7 +1846,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C410) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_literal_within_list_call(
@@ -1857,7 +1860,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C411) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_list_call(
expr,
@@ -1870,7 +1872,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C413) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_call_around_sorted(
@@ -1885,7 +1886,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C414) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_double_cast_or_process(
@@ -1897,7 +1897,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C415) {
if let Some(check) =
flake8_comprehensions::checks::unnecessary_subscript_reversal(
@@ -1909,7 +1908,6 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::C417) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_map(
func,
@@ -1920,19 +1918,6 @@ where
};
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::UP003) {
pyupgrade::plugins::type_of_primitive(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::UP015) {
pyupgrade::plugins::redundant_open_modes(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP020) {
pyupgrade::plugins::open_alias(self, expr, func);
}
// flake8-boolean-trap
if self.settings.enabled.contains(&CheckCode::FBT003) {
flake8_boolean_trap::plugins::check_boolean_positional_value_in_function_call(
@@ -1963,7 +1948,6 @@ where
if self.settings.enabled.contains(&CheckCode::PD002) {
self.add_checks(pandas_vet::checks::inplace_argument(keywords).into_iter());
}
for (code, name) in vec![
(CheckCode::PD003, "isnull"),
(CheckCode::PD004, "notnull"),
@@ -1980,7 +1964,6 @@ where
}
}
}
if self.settings.enabled.contains(&CheckCode::PD015) {
if let Some(check) = pandas_vet::checks::use_of_pd_merge(func) {
self.add_check(check);
@@ -2074,6 +2057,14 @@ where
if self.settings.enabled.contains(&CheckCode::PLR1722) {
pylint::plugins::use_sys_exit(self, func);
}
// ruff
if self.settings.enabled.contains(&CheckCode::RUF004) {
self.add_checks(
ruff::checks::keyword_argument_before_star_argument(args, keywords)
.into_iter(),
);
}
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.enabled.contains(&CheckCode::F601);
@@ -3926,16 +3917,26 @@ impl<'a> Checker<'a> {
}
}
#[allow(clippy::too_many_arguments)]
pub fn check_ast(
python_ast: &Suite,
locator: &SourceCodeLocator,
stylist: &SourceCodeStyleDetector,
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: flags::Autofix,
noqa: flags::Noqa,
path: &Path,
) -> Vec<Check> {
let mut checker = Checker::new(settings, noqa_line_for, autofix, noqa, path, locator);
let mut checker = Checker::new(
settings,
noqa_line_for,
autofix,
noqa,
path,
locator,
stylist,
);
checker.push_scope(Scope::new(ScopeKind::Module));
checker.bind_builtins();

View File

@@ -328,6 +328,7 @@ pub enum CheckCode {
RUF001,
RUF002,
RUF003,
RUF004,
RUF100,
// pygrep-hooks
PGH001,
@@ -955,6 +956,7 @@ pub enum CheckKind {
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
AmbiguousUnicodeCharacterComment(char, char),
KeywordArgumentBeforeStarArgument(String),
UnusedNOQA(Option<UnusedCodes>),
// flake8-datetimez
CallDatetimeWithoutTzinfo,
@@ -1360,6 +1362,7 @@ impl CheckCode {
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
CheckCode::RUF003 => CheckKind::AmbiguousUnicodeCharacterComment('𝐁', 'B'),
CheckCode::RUF004 => CheckKind::KeywordArgumentBeforeStarArgument("...".to_string()),
CheckCode::RUF100 => CheckKind::UnusedNOQA(None),
}
}
@@ -1612,6 +1615,7 @@ impl CheckCode {
CheckCode::RUF001 => CheckCategory::Ruff,
CheckCode::RUF002 => CheckCategory::Ruff,
CheckCode::RUF003 => CheckCategory::Ruff,
CheckCode::RUF004 => CheckCategory::Ruff,
CheckCode::RUF100 => CheckCategory::Ruff,
CheckCode::S101 => CheckCategory::Flake8Bandit,
CheckCode::S102 => CheckCategory::Flake8Bandit,
@@ -1980,6 +1984,7 @@ impl CheckKind {
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
CheckKind::AmbiguousUnicodeCharacterComment(..) => &CheckCode::RUF003,
CheckKind::KeywordArgumentBeforeStarArgument(..) => &CheckCode::RUF004,
CheckKind::UnusedNOQA(..) => &CheckCode::RUF100,
}
}
@@ -2882,6 +2887,9 @@ impl CheckKind {
'{representant}'?)"
)
}
CheckKind::KeywordArgumentBeforeStarArgument(name) => {
format!("Keyword argument `{name}` must come after starred arguments")
}
CheckKind::UnusedNOQA(codes) => match codes {
None => "Unused blanket `noqa` directive".to_string(),
Some(codes) => {

View File

@@ -28,6 +28,7 @@ pub enum CheckCodePrefix {
A001,
A002,
A003,
ALL,
ANN,
ANN0,
ANN00,
@@ -456,6 +457,7 @@ pub enum CheckCodePrefix {
RUF001,
RUF002,
RUF003,
RUF004,
RUF1,
RUF10,
RUF100,
@@ -558,6 +560,7 @@ pub enum CheckCodePrefix {
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum SuffixLength {
None,
Zero,
One,
Two,
@@ -575,6 +578,297 @@ impl CheckCodePrefix {
CheckCodePrefix::A001 => vec![CheckCode::A001],
CheckCodePrefix::A002 => vec![CheckCode::A002],
CheckCodePrefix::A003 => vec![CheckCode::A003],
CheckCodePrefix::ALL => vec![
CheckCode::E401,
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::W292,
CheckCode::W605,
CheckCode::F401,
CheckCode::F402,
CheckCode::F403,
CheckCode::F404,
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F501,
CheckCode::F502,
CheckCode::F503,
CheckCode::F504,
CheckCode::F505,
CheckCode::F506,
CheckCode::F507,
CheckCode::F508,
CheckCode::F509,
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F811,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F842,
CheckCode::F901,
CheckCode::PLC0414,
CheckCode::PLC2201,
CheckCode::PLC3002,
CheckCode::PLE0117,
CheckCode::PLE0118,
CheckCode::PLE1142,
CheckCode::PLR0206,
CheckCode::PLR0402,
CheckCode::PLR1701,
CheckCode::PLR1722,
CheckCode::PLW0120,
CheckCode::PLW0602,
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
CheckCode::B002,
CheckCode::B003,
CheckCode::B004,
CheckCode::B005,
CheckCode::B006,
CheckCode::B007,
CheckCode::B008,
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B012,
CheckCode::B013,
CheckCode::B014,
CheckCode::B015,
CheckCode::B016,
CheckCode::B017,
CheckCode::B018,
CheckCode::B019,
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
CheckCode::B905,
CheckCode::BLE001,
CheckCode::C400,
CheckCode::C401,
CheckCode::C402,
CheckCode::C403,
CheckCode::C404,
CheckCode::C405,
CheckCode::C406,
CheckCode::C408,
CheckCode::C409,
CheckCode::C410,
CheckCode::C411,
CheckCode::C413,
CheckCode::C414,
CheckCode::C415,
CheckCode::C416,
CheckCode::C417,
CheckCode::T100,
CheckCode::C901,
CheckCode::TID252,
CheckCode::RET501,
CheckCode::RET502,
CheckCode::RET503,
CheckCode::RET504,
CheckCode::RET505,
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
CheckCode::T201,
CheckCode::T203,
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
CheckCode::ANN001,
CheckCode::ANN002,
CheckCode::ANN003,
CheckCode::ANN101,
CheckCode::ANN102,
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
CheckCode::ANN401,
CheckCode::YTT101,
CheckCode::YTT102,
CheckCode::YTT103,
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
CheckCode::YTT301,
CheckCode::YTT302,
CheckCode::YTT303,
CheckCode::SIM118,
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,
CheckCode::UP016,
CheckCode::UP017,
CheckCode::UP018,
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::D100,
CheckCode::D101,
CheckCode::D102,
CheckCode::D103,
CheckCode::D104,
CheckCode::D105,
CheckCode::D106,
CheckCode::D107,
CheckCode::D200,
CheckCode::D201,
CheckCode::D202,
CheckCode::D203,
CheckCode::D204,
CheckCode::D205,
CheckCode::D206,
CheckCode::D207,
CheckCode::D208,
CheckCode::D209,
CheckCode::D210,
CheckCode::D211,
CheckCode::D212,
CheckCode::D213,
CheckCode::D214,
CheckCode::D215,
CheckCode::D300,
CheckCode::D301,
CheckCode::D400,
CheckCode::D402,
CheckCode::D403,
CheckCode::D404,
CheckCode::D405,
CheckCode::D406,
CheckCode::D407,
CheckCode::D408,
CheckCode::D409,
CheckCode::D410,
CheckCode::D411,
CheckCode::D412,
CheckCode::D413,
CheckCode::D414,
CheckCode::D415,
CheckCode::D416,
CheckCode::D417,
CheckCode::D418,
CheckCode::D419,
CheckCode::N801,
CheckCode::N802,
CheckCode::N803,
CheckCode::N804,
CheckCode::N805,
CheckCode::N806,
CheckCode::N807,
CheckCode::N811,
CheckCode::N812,
CheckCode::N813,
CheckCode::N814,
CheckCode::N815,
CheckCode::N816,
CheckCode::N817,
CheckCode::N818,
CheckCode::I001,
CheckCode::ERA001,
CheckCode::S101,
CheckCode::S102,
CheckCode::S104,
CheckCode::S105,
CheckCode::S106,
CheckCode::S107,
CheckCode::FBT001,
CheckCode::FBT002,
CheckCode::FBT003,
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
CheckCode::ICN001,
CheckCode::DTZ001,
CheckCode::DTZ002,
CheckCode::DTZ003,
CheckCode::DTZ004,
CheckCode::DTZ005,
CheckCode::DTZ006,
CheckCode::DTZ007,
CheckCode::DTZ011,
CheckCode::DTZ012,
CheckCode::RUF001,
CheckCode::RUF002,
CheckCode::RUF003,
CheckCode::RUF004,
CheckCode::RUF100,
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PD002,
CheckCode::PD003,
CheckCode::PD004,
CheckCode::PD007,
CheckCode::PD008,
CheckCode::PD009,
CheckCode::PD010,
CheckCode::PD011,
CheckCode::PD012,
CheckCode::PD013,
CheckCode::PD015,
CheckCode::PD901,
CheckCode::EM101,
CheckCode::EM102,
CheckCode::EM103,
],
CheckCodePrefix::ANN => vec![
CheckCode::ANN001,
CheckCode::ANN002,
@@ -2025,13 +2319,25 @@ impl CheckCodePrefix {
CheckCode::RUF001,
CheckCode::RUF002,
CheckCode::RUF003,
CheckCode::RUF004,
CheckCode::RUF100,
],
CheckCodePrefix::RUF0 => vec![CheckCode::RUF001, CheckCode::RUF002, CheckCode::RUF003],
CheckCodePrefix::RUF00 => vec![CheckCode::RUF001, CheckCode::RUF002, CheckCode::RUF003],
CheckCodePrefix::RUF0 => vec![
CheckCode::RUF001,
CheckCode::RUF002,
CheckCode::RUF003,
CheckCode::RUF004,
],
CheckCodePrefix::RUF00 => vec![
CheckCode::RUF001,
CheckCode::RUF002,
CheckCode::RUF003,
CheckCode::RUF004,
],
CheckCodePrefix::RUF001 => vec![CheckCode::RUF001],
CheckCodePrefix::RUF002 => vec![CheckCode::RUF002],
CheckCodePrefix::RUF003 => vec![CheckCode::RUF003],
CheckCodePrefix::RUF004 => vec![CheckCode::RUF004],
CheckCodePrefix::RUF1 => vec![CheckCode::RUF100],
CheckCodePrefix::RUF10 => vec![CheckCode::RUF100],
CheckCodePrefix::RUF100 => vec![CheckCode::RUF100],
@@ -2478,6 +2784,7 @@ impl CheckCodePrefix {
CheckCodePrefix::A001 => SuffixLength::Three,
CheckCodePrefix::A002 => SuffixLength::Three,
CheckCodePrefix::A003 => SuffixLength::Three,
CheckCodePrefix::ALL => SuffixLength::None,
CheckCodePrefix::ANN => SuffixLength::Zero,
CheckCodePrefix::ANN0 => SuffixLength::One,
CheckCodePrefix::ANN00 => SuffixLength::Two,
@@ -2906,6 +3213,7 @@ impl CheckCodePrefix {
CheckCodePrefix::RUF001 => SuffixLength::Three,
CheckCodePrefix::RUF002 => SuffixLength::Three,
CheckCodePrefix::RUF003 => SuffixLength::Three,
CheckCodePrefix::RUF004 => SuffixLength::Three,
CheckCodePrefix::RUF1 => SuffixLength::One,
CheckCodePrefix::RUF10 => SuffixLength::Two,
CheckCodePrefix::RUF100 => SuffixLength::Three,
@@ -3010,6 +3318,7 @@ impl CheckCodePrefix {
pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::A,
CheckCodePrefix::ALL,
CheckCodePrefix::ANN,
CheckCodePrefix::ARG,
CheckCodePrefix::B,

View File

@@ -4,7 +4,7 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
fn assertion_error(msg: Option<&Expr>) -> Stmt {
Stmt::new(
@@ -47,7 +47,8 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
let mut check = Check::new(CheckKind::DoNotAssertFalse, Range::from_located(test));
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
let mut generator =
SourceCodeGenerator::new(checker.style.indentation(), checker.style.quote());
generator.unparse_stmt(&assertion_error(msg));
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(

View File

@@ -7,7 +7,7 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
fn type_pattern(elts: Vec<&Expr>) -> Expr {
Expr::new(
@@ -54,7 +54,8 @@ fn duplicate_handler_exceptions<'a>(
Range::from_located(expr),
);
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
let mut generator =
SourceCodeGenerator::new(checker.style.indentation(), checker.style.quote());
if unique_elts.len() == 1 {
generator.unparse_expr(unique_elts[0], 0);
} else {

View File

@@ -4,9 +4,9 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::source_code_generator::SourceCodeGenerator;
fn attribute(value: &Expr, attr: &str) -> Expr {
Expr::new(
@@ -46,7 +46,8 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
let mut check = Check::new(CheckKind::GetAttrWithConstant, Range::from_located(expr));
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
let mut generator =
SourceCodeGenerator::new(checker.style.indentation(), checker.style.quote());
generator.unparse_expr(&attribute(obj, value), 0);
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(

View File

@@ -4,7 +4,7 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
/// B013
pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[Excepthandler]) {
@@ -23,7 +23,8 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E
Range::from_located(type_),
);
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
let mut generator =
SourceCodeGenerator::new(checker.style.indentation(), checker.style.quote());
generator.unparse_expr(elt, 0);
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(

View File

@@ -6,11 +6,17 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
fn assignment(obj: &Expr, name: &str, value: &Expr) -> Result<String> {
fn assignment(
obj: &Expr,
name: &str,
value: &Expr,
stylist: &SourceCodeStyleDetector,
) -> Result<String> {
let stmt = Stmt::new(
Location::default(),
Location::default(),
@@ -28,7 +34,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr) -> Result<String> {
type_comment: None,
},
);
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_stmt(&stmt);
generator.generate().map_err(std::convert::Into::into)
}
@@ -63,7 +69,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if expr == child.as_ref() {
let mut check = Check::new(CheckKind::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(check.kind.code()) {
match assignment(obj, name, value) {
match assignment(obj, name, value, checker.style) {
Ok(content) => check.amend(Fix::replacement(
content,
expr.location,

View File

@@ -25,7 +25,7 @@ expression: checks
row: 10
column: 12
fix:
content: "raise AssertionError('message')"
content: "raise AssertionError(\"message\")"
location:
row: 10
column: 0

View File

@@ -24,7 +24,6 @@ mod checkers;
pub mod checks;
pub mod checks_gen;
pub mod cli;
pub mod code_gen;
mod cst;
mod directives;
mod docstrings;
@@ -60,7 +59,7 @@ mod pandas_vet;
pub mod pep8_naming;
pub mod printer;
mod pycodestyle;
mod pydocstyle;
pub mod pydocstyle;
mod pyflakes;
mod pygrep_hooks;
mod pylint;
@@ -70,8 +69,10 @@ pub mod resolver;
mod ruff;
mod rustpython_helpers;
pub mod settings;
pub mod source_code_generator;
pub mod source_code_locator;
mod vendored;
pub mod source_code_style;
mod vendor;
pub mod visibility;
cfg_if! {

View File

@@ -11,6 +11,7 @@ use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::{flags, pyproject, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{directives, packages, resolver};
/// Load the relevant `Settings` for a given `Path`.
@@ -38,9 +39,12 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Initialize the SourceCodeLocator (which computes offsets lazily).
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
@@ -55,6 +59,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
contents,
tokens,
&locator,
&stylist,
&directives,
&settings,
autofix.into(),

View File

@@ -14,6 +14,7 @@ use crate::settings::configuration::Configuration;
use crate::settings::options::Options;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
@@ -69,9 +70,12 @@ pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Initialize the SourceCodeLocator (which computes offsets lazily).
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(&tokens, &locator, directives::Flags::empty());
@@ -82,6 +86,7 @@ pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
contents,
tokens,
&locator,
&stylist,
&directives,
&settings,
flags::Autofix::Enabled,

View File

@@ -17,12 +17,13 @@ use crate::checkers::lines::check_lines;
use crate::checkers::noqa::check_noqa;
use crate::checkers::tokens::check_tokens;
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::code_gen::SourceGenerator;
use crate::directives::Directives;
use crate::message::{Message, Source};
use crate::noqa::add_noqa;
use crate::settings::{flags, Settings};
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{cache, directives, fs, rustpython_helpers};
#[derive(Debug, Default)]
@@ -53,6 +54,7 @@ pub(crate) fn check_path(
contents: &str,
tokens: Vec<LexResult>,
locator: &SourceCodeLocator,
stylist: &SourceCodeStyleDetector,
directives: &Directives,
settings: &Settings,
autofix: flags::Autofix,
@@ -89,6 +91,7 @@ pub(crate) fn check_path(
checks.extend(check_ast(
&python_ast,
locator,
stylist,
&directives.noqa_line_for,
settings,
autofix,
@@ -216,9 +219,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Tokenize once.
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
// Initialize the SourceCodeLocator (which computes offsets lazily).
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(&contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
@@ -233,6 +239,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
&contents,
tokens,
&locator,
&stylist,
&directives,
settings,
flags::Autofix::Disabled,
@@ -259,9 +266,15 @@ pub fn autoformat_path(path: &Path, settings: &Settings) -> Result<()> {
// Tokenize once.
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(&contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
// Generate the AST.
let python_ast = rustpython_helpers::parse_program_tokens(tokens, "<filename>")?;
let mut generator = SourceGenerator::default();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_suite(&python_ast);
write(path, generator.generate()?)?;
@@ -318,9 +331,12 @@ fn lint(
// Tokenize once.
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
// Initialize the SourceCodeLocator (which computes offsets lazily).
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(&contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
@@ -335,6 +351,7 @@ fn lint(
&contents,
tokens,
&locator,
&stylist,
&directives,
settings,
autofix.into(),
@@ -381,6 +398,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
let directives = directives::extract_directives(
&tokens,
&locator,
@@ -392,6 +410,7 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Check>> {
&contents,
tokens,
&locator,
&stylist,
&directives,
settings,
flags::Autofix::Enabled,

View File

@@ -14,6 +14,7 @@ mod tests {
use crate::linter::check_path;
use crate::settings::flags;
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{directives, rustpython_helpers, settings};
fn check_code(contents: &str, expected: &[CheckCode]) -> Result<()> {
@@ -21,6 +22,7 @@ mod tests {
let settings = settings::Settings::for_rules(CheckCodePrefix::PD.codes());
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
let directives = directives::extract_directives(
&tokens,
&locator,
@@ -32,6 +34,7 @@ mod tests {
&contents,
tokens,
&locator,
&stylist,
&directives,
&settings,
flags::Autofix::Enabled,

View File

@@ -12,9 +12,15 @@ use crate::ast::whitespace::leading_space;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind, RejectedCmpop};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
fn compare(left: &Expr, ops: &[Cmpop], comparators: &[Expr]) -> Option<String> {
fn compare(
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
stylist: &SourceCodeStyleDetector,
) -> Option<String> {
let cmp = Expr::new(
Location::default(),
Location::default(),
@@ -24,7 +30,7 @@ fn compare(left: &Expr, ops: &[Cmpop], comparators: &[Expr]) -> Option<String> {
comparators: comparators.to_vec(),
},
);
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_expr(&cmp, 0);
generator.generate().ok()
}
@@ -194,7 +200,7 @@ pub fn literal_comparisons(
.map(|(idx, op)| bad_ops.get(&idx).unwrap_or(op))
.cloned()
.collect::<Vec<_>>();
if let Some(content) = compare(left, &ops, comparators) {
if let Some(content) = compare(left, &ops, comparators, checker.style) {
for check in &mut checks {
check.amend(Fix::replacement(
content.to_string(),
@@ -233,7 +239,9 @@ pub fn not_tests(
let mut check =
Check::new(CheckKind::NotInTest, Range::from_located(operand));
if checker.patch(check.kind.code()) && should_fix {
if let Some(content) = compare(left, &[Cmpop::NotIn], comparators) {
if let Some(content) =
compare(left, &[Cmpop::NotIn], comparators, checker.style)
{
check.amend(Fix::replacement(
content,
expr.location,
@@ -249,7 +257,9 @@ pub fn not_tests(
let mut check =
Check::new(CheckKind::NotIsTest, Range::from_located(operand));
if checker.patch(check.kind.code()) && should_fix {
if let Some(content) = compare(left, &[Cmpop::IsNot], comparators) {
if let Some(content) =
compare(left, &[Cmpop::IsNot], comparators, checker.style)
{
check.amend(Fix::replacement(
content,
expr.location,
@@ -267,7 +277,12 @@ pub fn not_tests(
}
}
fn function(name: &str, args: &Arguments, body: &Expr) -> Result<String> {
fn function(
name: &str,
args: &Arguments,
body: &Expr,
stylist: &SourceCodeStyleDetector,
) -> Result<String> {
let body = Stmt::new(
Location::default(),
Location::default(),
@@ -287,7 +302,7 @@ fn function(name: &str, args: &Arguments, body: &Expr) -> Result<String> {
type_comment: None,
},
);
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_stmt(&func);
Ok(generator.generate()?)
}
@@ -301,7 +316,7 @@ pub fn do_not_assign_lambda(checker: &mut Checker, target: &Expr, value: &Expr,
if !match_leading_content(stmt, checker.locator)
&& !match_trailing_content(stmt, checker.locator)
{
match function(id, args, body) {
match function(id, args, body, checker.style) {
Ok(content) => {
let first_line = checker.locator.slice_source_code_range(&Range {
location: Location::new(stmt.location.row(), 0),

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use rustc_hash::FxHashSet;
use crate::vendored::cformat::{
use crate::vendor::cformat::{
CFormatError, CFormatPart, CFormatQuantity, CFormatSpec, CFormatString,
};

View File

@@ -4,7 +4,7 @@ use std::fmt;
use rustc_hash::FxHashSet;
use crate::vendored::format::{
use crate::vendor::format::{
FieldName, FieldType, FormatParseError, FormatPart, FormatString, FromTemplate,
};
@@ -82,7 +82,7 @@ impl TryFrom<&str> for FormatSummary {
#[cfg(test)]
mod tests {
use super::*;
use crate::vendored::format::FromTemplate;
use crate::vendor::format::FromTemplate;
#[test]
fn test_format_summary() {

View File

@@ -20,6 +20,7 @@ mod tests {
use crate::linter::{check_path, test_path};
use crate::settings::flags;
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{directives, rustpython_helpers, settings};
#[test_case(CheckCode::F401, Path::new("F401_0.py"); "F401_0")]
@@ -171,6 +172,7 @@ mod tests {
let settings = settings::Settings::for_rules(CheckCodePrefix::F.codes());
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
let directives = directives::extract_directives(
&tokens,
&locator,
@@ -182,6 +184,7 @@ mod tests {
&contents,
tokens,
&locator,
&stylist,
&directives,
&settings,
flags::Autofix::Enabled,

View File

@@ -7,9 +7,10 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
/// Return the typename, args, keywords and mother class
fn match_named_tuple_assign<'a>(
@@ -163,8 +164,9 @@ fn convert_to_class(
typename: &str,
body: Vec<Stmt>,
base_class: &ExprKind,
stylist: &SourceCodeStyleDetector,
) -> Result<Fix> {
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_stmt(&create_class_def_stmt(typename, body, base_class));
let content = generator.generate()?;
Ok(Fix::replacement(
@@ -194,7 +196,7 @@ pub fn convert_named_tuple_functional_to_class(
Range::from_located(stmt),
);
if checker.patch(check.kind.code()) {
match convert_to_class(stmt, typename, properties, base_class) {
match convert_to_class(stmt, typename, properties, base_class, checker.style) {
Ok(fix) => check.amend(fix),
Err(err) => error!("Failed to convert `NamedTuple`: {err}"),
}

View File

@@ -9,9 +9,10 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
/// Return the class name, arguments, keywords and base class for a `TypedDict`
/// assignment.
@@ -196,8 +197,9 @@ fn convert_to_class(
body: Vec<Stmt>,
total_keyword: Option<KeywordData>,
base_class: &ExprKind,
stylist: &SourceCodeStyleDetector,
) -> Result<Fix> {
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_stmt(&create_class_def_stmt(
class_name,
body,
@@ -236,7 +238,14 @@ pub fn convert_typed_dict_functional_to_class(
Range::from_located(stmt),
);
if checker.patch(check.kind.code()) {
match convert_to_class(stmt, class_name, body, total_keyword, base_class) {
match convert_to_class(
stmt,
class_name,
body,
total_keyword,
base_class,
checker.style,
) {
Ok(fix) => check.amend(fix),
Err(err) => error!("Failed to convert TypedDict: {err}"),
};

View File

@@ -7,7 +7,8 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::SourceCodeLocator;
/// Return `true` if the `Expr` is a reference to `${module}.${any}`.
@@ -87,13 +88,14 @@ fn replace_call_on_arg_by_arg_attribute(
arg: &Expr,
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Check> {
let attribute = ExprKind::Attribute {
value: Box::new(arg.clone()),
attr: attr.to_string(),
ctx: ExprContext::Load,
};
replace_by_expr_kind(attribute, expr, patch)
replace_by_expr_kind(attribute, expr, patch, stylist)
}
// `func(arg, **args)` => `arg.method(**args)`
@@ -102,6 +104,7 @@ fn replace_call_on_arg_by_arg_method_call(
args: &[Expr],
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Option<Check>> {
if args.is_empty() {
bail!("Expected at least one argument");
@@ -119,7 +122,7 @@ fn replace_call_on_arg_by_arg_method_call(
.collect(),
keywords: vec![],
};
let expr = replace_by_expr_kind(call, expr, patch)?;
let expr = replace_by_expr_kind(call, expr, patch, stylist)?;
Ok(Some(expr))
} else {
Ok(None)
@@ -127,10 +130,15 @@ fn replace_call_on_arg_by_arg_method_call(
}
// `expr` => `Expr(expr_kind)`
fn replace_by_expr_kind(node: ExprKind, expr: &Expr, patch: bool) -> Result<Check> {
fn replace_by_expr_kind(
node: ExprKind,
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Check> {
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
if patch {
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_expr(&create_expr(node), 0);
let content = generator.generate()?;
check.amend(Fix::replacement(
@@ -142,10 +150,15 @@ fn replace_by_expr_kind(node: ExprKind, expr: &Expr, patch: bool) -> Result<Chec
Ok(check)
}
fn replace_by_stmt_kind(node: StmtKind, expr: &Expr, patch: bool) -> Result<Check> {
fn replace_by_stmt_kind(
node: StmtKind,
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Check> {
let mut check = Check::new(CheckKind::RemoveSixCompat, Range::from_located(expr));
if patch {
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(stylist.indentation(), stylist.quote());
generator.unparse_stmt(&create_stmt(node));
let content = generator.generate()?;
check.amend(Fix::replacement(
@@ -163,12 +176,13 @@ fn replace_by_raise_from(
cause: Option<ExprKind>,
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Check> {
let stmt_kind = StmtKind::Raise {
exc: exc.map(|exc| Box::new(create_expr(exc))),
cause: cause.map(|cause| Box::new(create_expr(cause))),
};
replace_by_stmt_kind(stmt_kind, expr, patch)
replace_by_stmt_kind(stmt_kind, expr, patch, stylist)
}
fn replace_by_index_on_arg(
@@ -176,16 +190,22 @@ fn replace_by_index_on_arg(
index: &ExprKind,
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Check> {
let index = ExprKind::Subscript {
value: Box::new(create_expr(arg.node.clone())),
slice: Box::new(create_expr(index.clone())),
ctx: ExprContext::Load,
};
replace_by_expr_kind(index, expr, patch)
replace_by_expr_kind(index, expr, patch, stylist)
}
fn handle_reraise(args: &[Expr], expr: &Expr, patch: bool) -> Result<Option<Check>> {
fn handle_reraise(
args: &[Expr],
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
) -> Result<Option<Check>> {
if let [_, exc, tb] = args {
let check = replace_by_raise_from(
Some(ExprKind::Call {
@@ -200,6 +220,7 @@ fn handle_reraise(args: &[Expr], expr: &Expr, patch: bool) -> Result<Option<Chec
None,
expr,
patch,
stylist,
)?;
Ok(Some(check))
} else if let [arg] = args {
@@ -208,7 +229,7 @@ fn handle_reraise(args: &[Expr], expr: &Expr, patch: bool) -> Result<Option<Chec
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Name { id, .. } = &value.node {
if id == "sys" && attr == "exc_info" {
let check = replace_by_raise_from(None, None, expr, patch)?;
let check = replace_by_raise_from(None, None, expr, patch, stylist)?;
return Ok(Some(check));
};
};
@@ -227,6 +248,7 @@ fn handle_func(
keywords: &[Keyword],
expr: &Expr,
patch: bool,
stylist: &SourceCodeStyleDetector,
locator: &SourceCodeLocator,
) -> Result<Option<Check>> {
let func_name = match &func.node {
@@ -241,72 +263,82 @@ fn handle_func(
("ensure_str", [arg], []) => replace_by_str_literal(arg, false, expr, patch, locator),
("ensure_text", [arg], []) => replace_by_str_literal(arg, false, expr, patch, locator),
("iteritems", args, []) => {
replace_call_on_arg_by_arg_method_call("items", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("items", args, expr, patch, stylist)?
}
("viewitems", args, []) => {
replace_call_on_arg_by_arg_method_call("items", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("items", args, expr, patch, stylist)?
}
("iterkeys", args, []) => {
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch, stylist)?
}
("viewkeys", args, []) => {
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("keys", args, expr, patch, stylist)?
}
("itervalues", args, []) => {
replace_call_on_arg_by_arg_method_call("values", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("values", args, expr, patch, stylist)?
}
("viewvalues", args, []) => {
replace_call_on_arg_by_arg_method_call("values", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("values", args, expr, patch, stylist)?
}
("get_method_function", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__func__", arg, expr, patch,
"__func__", arg, expr, patch, stylist,
)?),
("get_method_self", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__self__", arg, expr, patch,
"__self__", arg, expr, patch, stylist,
)?),
("get_function_closure", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__closure__",
arg,
expr,
patch,
stylist,
)?),
("get_function_code", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__code__", arg, expr, patch,
"__code__", arg, expr, patch, stylist,
)?),
("get_function_defaults", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__defaults__",
arg,
expr,
patch,
stylist,
)?),
("get_function_globals", [arg], []) => Some(replace_call_on_arg_by_arg_attribute(
"__globals__",
arg,
expr,
patch,
stylist,
)?),
("create_unbound_method", [arg, _], _) => Some(replace_by_expr_kind(
arg.node.clone(),
expr,
patch,
stylist,
)?),
("get_unbound_function", [arg], []) => Some(replace_by_expr_kind(
arg.node.clone(),
expr,
patch,
stylist,
)?),
("create_unbound_method", [arg, _], _) => {
Some(replace_by_expr_kind(arg.node.clone(), expr, patch)?)
}
("get_unbound_function", [arg], []) => {
Some(replace_by_expr_kind(arg.node.clone(), expr, patch)?)
}
("assertCountEqual", args, []) => {
replace_call_on_arg_by_arg_method_call("assertCountEqual", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("assertCountEqual", args, expr, patch, stylist)?
}
("assertRaisesRegex", args, []) => {
replace_call_on_arg_by_arg_method_call("assertRaisesRegex", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("assertRaisesRegex", args, expr, patch, stylist)?
}
("assertRegex", args, []) => {
replace_call_on_arg_by_arg_method_call("assertRegex", args, expr, patch)?
replace_call_on_arg_by_arg_method_call("assertRegex", args, expr, patch, stylist)?
}
("raise_from", [exc, cause], []) => Some(replace_by_raise_from(
Some(exc.node.clone()),
Some(cause.node.clone()),
expr,
patch,
stylist,
)?),
("reraise", args, []) => handle_reraise(args, expr, patch)?,
("reraise", args, []) => handle_reraise(args, expr, patch, stylist)?,
("byte2int", [arg], []) => Some(replace_by_index_on_arg(
arg,
&ExprKind::Constant {
@@ -315,10 +347,15 @@ fn handle_func(
},
expr,
patch,
stylist,
)?),
("indexbytes", [arg, index], []) => Some(replace_by_index_on_arg(
arg,
&index.node,
expr,
patch,
stylist,
)?),
("indexbytes", [arg, index], []) => {
Some(replace_by_index_on_arg(arg, &index.node, expr, patch)?)
}
("int2byte", [arg], []) => Some(replace_by_expr_kind(
ExprKind::Call {
func: Box::new(create_expr(ExprKind::Name {
@@ -333,6 +370,7 @@ fn handle_func(
},
expr,
patch,
stylist,
)?),
_ => None,
};
@@ -382,6 +420,7 @@ fn handle_next_on_six_dict(expr: &Expr, patch: bool, checker: &Checker) -> Resul
},
arg,
patch,
checker.style,
) {
Ok(check) => Ok(Some(check)),
Err(err) => Err(err),
@@ -409,7 +448,15 @@ pub fn remove_six_compat(checker: &mut Checker, expr: &Expr) {
func,
args,
keywords,
} => match handle_func(func, args, keywords, expr, patch, checker.locator) {
} => match handle_func(
func,
args,
keywords,
expr,
patch,
checker.style,
checker.locator,
) {
Ok(check) => check,
Err(err) => {
error!("Failed to remove `six` reference: {err}");

View File

@@ -5,7 +5,7 @@ use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::source_code_generator::SourceCodeGenerator;
fn optional(expr: &Expr) -> Expr {
Expr::new(
@@ -65,7 +65,8 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
if checker.match_typing_call_path(&call_path, "Optional") {
let mut check = Check::new(CheckKind::UsePEP604Annotation, Range::from_located(expr));
if checker.patch(check.kind.code()) {
let mut generator = SourceGenerator::new();
let mut generator =
SourceCodeGenerator::new(checker.style.indentation(), checker.style.quote());
generator.unparse_expr(&optional(slice), 0);
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(
@@ -84,7 +85,10 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
// Invalid type annotation.
}
ExprKind::Tuple { elts, .. } => {
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(
checker.style.indentation(),
checker.style.quote(),
);
generator.unparse_expr(&union(elts), 0);
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(
@@ -96,7 +100,10 @@ pub fn use_pep604_annotation(checker: &mut Checker, expr: &Expr, value: &Expr, s
}
_ => {
// Single argument.
let mut generator = SourceGenerator::new();
let mut generator = SourceCodeGenerator::new(
checker.style.indentation(),
checker.style.quote(),
);
generator.unparse_expr(slice, 0);
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(

View File

@@ -75,7 +75,7 @@ expression: checks
row: 17
column: 46
fix:
content: "class MyType5(TypedDict):\n a: 'hello'"
content: "class MyType5(TypedDict):\n a: \"hello\""
location:
row: 17
column: 0
@@ -91,7 +91,7 @@ expression: checks
row: 18
column: 41
fix:
content: "class MyType6(TypedDict):\n a: 'hello'"
content: "class MyType6(TypedDict):\n a: \"hello\""
location:
row: 18
column: 0
@@ -139,7 +139,7 @@ expression: checks
row: 30
column: 59
fix:
content: "class MyType10(TypedDict):\n key: Literal['value']"
content: "class MyType10(TypedDict):\n key: Literal[\"value\"]"
location:
row: 30
column: 0

View File

@@ -27,7 +27,7 @@ expression: checks
row: 12
column: 1
fix:
content: "class NT2(NamedTuple):\n a: int\n b: str = 'foo'\n c: list[bool] = [True]"
content: "class NT2(NamedTuple):\n a: int\n b: str = \"foo\"\n c: list[bool] = [True]"
location:
row: 8
column: 0

View File

@@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use rustpython_ast::Location;
use rustpython_ast::{Expr, ExprKind, Keyword, KeywordData, Location};
use crate::ast::types::Range;
use crate::autofix::Fix;
@@ -1680,3 +1680,25 @@ pub fn ambiguous_unicode_character(
checks
}
/// RUF004
pub fn keyword_argument_before_star_argument(args: &[Expr], keywords: &[Keyword]) -> Vec<Check> {
let mut checks = vec![];
if let Some(arg) = args
.iter()
.find(|arg| matches!(arg.node, ExprKind::Starred { .. }))
{
for keyword in keywords {
if keyword.location < arg.location {
let KeywordData { arg, .. } = &keyword.node;
if let Some(arg) = arg {
checks.push(Check::new(
CheckKind::KeywordArgumentBeforeStarArgument(arg.to_string()),
Range::from_located(keyword),
));
}
}
}
}
checks
}

View File

@@ -8,10 +8,24 @@ mod tests {
use anyhow::Result;
use rustc_hash::FxHashSet;
use test_case::test_case;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::RUF004, Path::new("RUF004.py"); "RUF004")]
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/ruff")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test]
fn confusables() -> Result<()> {

View File

@@ -0,0 +1,23 @@
---
source: src/ruff/mod.rs
expression: checks
---
- kind:
KeywordArgumentBeforeStarArgument: kw
location:
row: 12
column: 2
end_location:
row: 12
column: 6
fix: ~
- kind:
KeywordArgumentBeforeStarArgument: kw
location:
row: 13
column: 2
end_location:
row: 13
column: 6
fix: ~

View File

@@ -379,6 +379,7 @@ fn resolve_codes<'a>(specs: impl Iterator<Item = CheckCodeSpec<'a>>) -> FxHashSe
let mut codes: FxHashSet<CheckCode> = FxHashSet::default();
for spec in specs {
for specificity in [
SuffixLength::None,
SuffixLength::Zero,
SuffixLength::One,
SuffixLength::Two,

View File

@@ -1,14 +1,19 @@
//! Generate Python source code from an abstract syntax tree (AST).
use std::fmt;
use std::ops::Deref;
use std::string::FromUtf8Error;
use anyhow::Result;
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Suite, Withitem};
use rustpython_common::str;
use rustpython_parser::ast::{
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, ConversionFlag, Expr, ExprKind,
Operator, Stmt, StmtKind,
};
use crate::source_code_style::{Indentation, Quote};
use crate::vendor::{bytes, str};
mod precedence {
macro_rules! precedence {
($($op:ident,)*) => {
@@ -27,25 +32,27 @@ mod precedence {
pub const EXPR: u8 = BOR;
}
pub struct SourceGenerator {
pub struct SourceCodeGenerator<'a> {
/// The indentation style to use.
indent: &'a Indentation,
/// The quote style to use for string literals.
quote: &'a Quote,
buffer: Vec<u8>,
indentation: usize,
new_lines: usize,
indent_depth: usize,
num_newlines: usize,
initial: bool,
}
impl Default for SourceGenerator {
fn default() -> Self {
Self::new()
}
}
impl SourceGenerator {
pub fn new() -> Self {
SourceGenerator {
impl<'a> SourceCodeGenerator<'a> {
pub fn new(indent: &'a Indentation, quote: &'a Quote) -> Self {
SourceCodeGenerator {
// Style preferences.
indent,
quote,
// Internal state.
buffer: vec![],
indentation: 0,
new_lines: 0,
indent_depth: 0,
num_newlines: 0,
initial: true,
}
}
@@ -56,30 +63,30 @@ impl SourceGenerator {
fn newline(&mut self) {
if !self.initial {
self.new_lines = std::cmp::max(self.new_lines, 1);
self.num_newlines = std::cmp::max(self.num_newlines, 1);
}
}
fn newlines(&mut self, extra: usize) {
if !self.initial {
self.new_lines = std::cmp::max(self.new_lines, 1 + extra);
self.num_newlines = std::cmp::max(self.num_newlines, 1 + extra);
}
}
fn body<U>(&mut self, stmts: &[Stmt<U>]) {
self.indentation += 1;
self.indent_depth += 1;
for stmt in stmts {
self.unparse_stmt(stmt);
}
self.indentation -= 1;
self.indent_depth -= 1;
}
fn p(&mut self, s: &str) {
if self.new_lines > 0 {
for _ in 0..self.new_lines {
if self.num_newlines > 0 {
for _ in 0..self.num_newlines {
self.buffer.extend("\n".as_bytes());
}
self.new_lines = 0;
self.num_newlines = 0;
}
self.buffer.extend(s.as_bytes());
}
@@ -108,7 +115,7 @@ impl SourceGenerator {
macro_rules! statement {
($body:block) => {{
self.newline();
self.p(&" ".repeat(self.indentation));
self.p(&self.indent.deref().repeat(self.indent_depth));
$body
self.initial = false;
}};
@@ -123,7 +130,7 @@ impl SourceGenerator {
..
} => {
// TODO(charlie): Handle decorators.
self.newlines(if self.indentation == 0 { 2 } else { 1 });
self.newlines(if self.indent_depth == 0 { 2 } else { 1 });
statement!({
self.p("def ");
self.p(name);
@@ -137,7 +144,7 @@ impl SourceGenerator {
self.p(":");
});
self.body(body);
if self.indentation == 0 {
if self.indent_depth == 0 {
self.newlines(2);
}
}
@@ -149,7 +156,7 @@ impl SourceGenerator {
..
} => {
// TODO(charlie): Handle decorators.
self.newlines(if self.indentation == 0 { 2 } else { 1 });
self.newlines(if self.indent_depth == 0 { 2 } else { 1 });
statement!({
self.p("async def ");
self.p(name);
@@ -163,7 +170,7 @@ impl SourceGenerator {
self.p(":");
});
self.body(body);
if self.indentation == 0 {
if self.indent_depth == 0 {
self.newlines(2);
}
}
@@ -175,7 +182,7 @@ impl SourceGenerator {
..
} => {
// TODO(charlie): Handle decorators.
self.newlines(if self.indentation == 0 { 2 } else { 1 });
self.newlines(if self.indent_depth == 0 { 2 } else { 1 });
statement!({
self.p("class ");
self.p(name);
@@ -200,7 +207,7 @@ impl SourceGenerator {
self.p(":");
});
self.body(body);
if self.indentation == 0 {
if self.indent_depth == 0 {
self.newlines(2);
}
}
@@ -788,6 +795,12 @@ impl SourceGenerator {
{
self.p(&value.to_string().replace("inf", inf_str));
}
Constant::Bytes(b) => {
self.p(&bytes::repr(b, self.quote.into()));
}
Constant::Str(s) => {
self.p(&format!("{}", str::repr(s, self.quote.into())));
}
_ => self.p(&format!("{value}")),
}
}
@@ -931,7 +944,7 @@ impl SourceGenerator {
}
fn unparse_formatted<U>(&mut self, val: &Expr<U>, conversion: usize, spec: Option<&Expr<U>>) {
let mut generator = SourceGenerator::default();
let mut generator = SourceCodeGenerator::new(self.indent, self.quote);
generator.unparse_expr(val, precedence::TEST + 1);
let brace = if generator.buffer.starts_with("{".as_bytes()) {
// put a space to avoid escaping the bracket
@@ -987,10 +1000,10 @@ impl SourceGenerator {
self.unparse_fstring_body(values, is_spec);
} else {
self.p("f");
let mut generator = SourceGenerator::default();
let mut generator = SourceCodeGenerator::new(self.indent, self.quote);
generator.unparse_fstring_body(values, is_spec);
let body = std::str::from_utf8(&generator.buffer).unwrap();
self.p(&format!("{}", str::repr(body)));
self.p(&format!("{}", str::repr(body, self.quote.into())));
}
}
@@ -1010,3 +1023,139 @@ impl SourceGenerator {
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_parser::parser;
use crate::source_code_generator::SourceCodeGenerator;
use crate::source_code_style::{Indentation, Quote};
fn round_trip(contents: &str) -> Result<String> {
let indentation = Indentation::default();
let quote = Quote::default();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let mut generator = SourceCodeGenerator::new(&indentation, &quote);
generator.unparse_stmt(stmt);
generator.generate().map_err(std::convert::Into::into)
}
fn round_trip_with(indentation: &Indentation, quote: &Quote, contents: &str) -> Result<String> {
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let mut generator = SourceCodeGenerator::new(indentation, quote);
generator.unparse_stmt(stmt);
generator.generate().map_err(std::convert::Into::into)
}
#[test]
fn quote() -> Result<()> {
assert_eq!(round_trip(r#""hello""#)?, r#""hello""#);
assert_eq!(round_trip(r#"'hello'"#)?, r#""hello""#);
assert_eq!(round_trip(r#"u'hello'"#)?, r#"u"hello""#);
assert_eq!(round_trip(r#"r'hello'"#)?, r#""hello""#);
assert_eq!(round_trip(r#"b'hello'"#)?, r#"b"hello""#);
assert_eq!(round_trip(r#"("abc" "def" "ghi")"#)?, r#""abcdefghi""#);
assert_eq!(round_trip(r#""he\"llo""#)?, r#"'he"llo'"#);
assert_eq!(round_trip(r#"f'abc{"def"}{1}'"#)?, r#"f'abc{"def"}{1}'"#);
assert_eq!(round_trip(r#"f"abc{'def'}{1}""#)?, r#"f'abc{"def"}{1}'"#);
Ok(())
}
#[test]
fn indent() -> Result<()> {
assert_eq!(
round_trip(
r#"
if True:
pass
"#
.trim(),
)?,
r#"
if True:
pass
"#
.trim()
);
Ok(())
}
#[test]
fn set_quote() -> Result<()> {
assert_eq!(
round_trip_with(&Indentation::default(), &Quote::Double, r#""hello""#)?,
r#""hello""#
);
assert_eq!(
round_trip_with(&Indentation::default(), &Quote::Single, r#""hello""#)?,
r#"'hello'"#
);
assert_eq!(
round_trip_with(&Indentation::default(), &Quote::Double, r#"'hello'"#)?,
r#""hello""#
);
assert_eq!(
round_trip_with(&Indentation::default(), &Quote::Single, r#"'hello'"#)?,
r#"'hello'"#
);
Ok(())
}
#[test]
fn set_indent() -> Result<()> {
assert_eq!(
round_trip_with(
&Indentation::new(" ".to_string()),
&Quote::default(),
r#"
if True:
pass
"#
.trim(),
)?,
r#"
if True:
pass
"#
.trim()
);
assert_eq!(
round_trip_with(
&Indentation::new(" ".to_string()),
&Quote::default(),
r#"
if True:
pass
"#
.trim(),
)?,
r#"
if True:
pass
"#
.trim()
);
assert_eq!(
round_trip_with(
&Indentation::new("\t".to_string()),
&Quote::default(),
r#"
if True:
pass
"#
.trim(),
)?,
r#"
if True:
pass
"#
.trim()
);
Ok(())
}
}

200
src/source_code_style.rs Normal file
View File

@@ -0,0 +1,200 @@
//! Detect code style from Python source code.
use std::ops::Deref;
use once_cell::unsync::OnceCell;
use rustpython_ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::types::Range;
use crate::pydocstyle::helpers::leading_quote;
use crate::source_code_locator::SourceCodeLocator;
use crate::vendor;
pub struct SourceCodeStyleDetector<'a> {
contents: &'a str,
locator: &'a SourceCodeLocator<'a>,
indentation: OnceCell<Indentation>,
quote: OnceCell<Quote>,
}
impl<'a> SourceCodeStyleDetector<'a> {
pub fn indentation(&'a self) -> &'a Indentation {
self.indentation
.get_or_init(|| detect_indentation(self.contents, self.locator).unwrap_or_default())
}
pub fn quote(&'a self) -> &'a Quote {
self.quote
.get_or_init(|| detect_quote(self.contents, self.locator).unwrap_or_default())
}
pub fn from_contents(contents: &'a str, locator: &'a SourceCodeLocator<'a>) -> Self {
Self {
contents,
locator,
indentation: OnceCell::default(),
quote: OnceCell::default(),
}
}
}
/// The quotation style used in Python source code.
#[derive(Debug, PartialEq, Eq)]
pub enum Quote {
Single,
Double,
}
impl Default for Quote {
fn default() -> Self {
Quote::Double
}
}
impl From<&Quote> for vendor::str::Quote {
fn from(val: &Quote) -> Self {
match val {
Quote::Single => vendor::str::Quote::Single,
Quote::Double => vendor::str::Quote::Double,
}
}
}
/// The indentation style used in Python source code.
#[derive(Debug, PartialEq, Eq)]
pub struct Indentation(String);
impl Indentation {
pub fn new(indentation: String) -> Self {
Self(indentation)
}
}
impl Default for Indentation {
fn default() -> Self {
Indentation(" ".to_string())
}
}
impl Deref for Indentation {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// Detect the indentation style of the given tokens.
fn detect_indentation(contents: &str, locator: &SourceCodeLocator) -> Option<Indentation> {
for (_start, tok, end) in lexer::make_tokenizer(contents).flatten() {
if let Tok::Indent { .. } = tok {
let start = Location::new(end.row(), 0);
let whitespace = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
return Some(Indentation(whitespace.to_string()));
}
}
None
}
/// Detect the quotation style of the given tokens.
fn detect_quote(contents: &str, locator: &SourceCodeLocator) -> Option<Quote> {
for (start, tok, end) in lexer::make_tokenizer(contents).flatten() {
if let Tok::String { .. } = tok {
let content = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
if let Some(pattern) = leading_quote(&content) {
if pattern.contains('\'') {
return Some(Quote::Single);
} else if pattern.contains('"') {
return Some(Quote::Double);
}
unreachable!("Expected string to start with a valid quote prefix")
}
}
}
None
}
#[cfg(test)]
mod tests {
use crate::source_code_style::{detect_indentation, detect_quote, Indentation, Quote};
use crate::SourceCodeLocator;
#[test]
fn indentation() {
let contents = r#"x = 1"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_indentation(contents, &locator), None);
let contents = r#"
if True:
pass
"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(
detect_indentation(contents, &locator),
Some(Indentation(" ".to_string()))
);
let contents = r#"
if True:
pass
"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(
detect_indentation(contents, &locator),
Some(Indentation(" ".to_string()))
);
let contents = r#"
if True:
pass
"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(
detect_indentation(contents, &locator),
Some(Indentation("\t".to_string()))
);
// TODO(charlie): Should non-significant whitespace be detected?
let contents = r#"
x = (
1,
2,
3,
)
"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_indentation(contents, &locator), None);
}
#[test]
fn quote() {
let contents = r#"x = 1"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_quote(contents, &locator), None);
let contents = r#"x = '1'"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_quote(contents, &locator), Some(Quote::Single));
let contents = r#"x = "1""#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_quote(contents, &locator), Some(Quote::Double));
let contents = r#"
def f():
"""Docstring."""
pass
"#;
let locator = SourceCodeLocator::new(contents);
assert_eq!(detect_quote(contents, &locator), Some(Quote::Double));
}
}

68
src/vendor/bytes.rs vendored Normal file
View File

@@ -0,0 +1,68 @@
//! Vendored from [bytes.rs in rustpython-common](https://github.com/RustPython/RustPython/blob/1d8269fb729c91fc56064e975172d3a11bd62d07/common/src/bytes.rs).
//! The only changes we make are to remove dead code and make the default quote
//! type configurable.
use crate::vendor;
use crate::vendor::str::Quote;
pub fn repr(b: &[u8], quote: Quote) -> String {
repr_with(b, &[], "", quote)
}
pub fn repr_with(b: &[u8], prefixes: &[&str], suffix: &str, quote: Quote) -> String {
use std::fmt::Write;
let mut out_len = 0usize;
let mut squote = 0;
let mut dquote = 0;
for &ch in b {
let incr = match ch {
b'\'' => {
squote += 1;
1
}
b'"' => {
dquote += 1;
1
}
b'\\' | b'\t' | b'\r' | b'\n' => 2,
0x20..=0x7e => 1,
_ => 4, // \xHH
};
// TODO: OverflowError
out_len = out_len.checked_add(incr).unwrap();
}
let (quote, num_escaped_quotes) = vendor::str::choose_quotes_for_repr(squote, dquote, quote);
// we'll be adding backslashes in front of the existing inner quotes
out_len += num_escaped_quotes;
// 3 is for b prefix + outer quotes
out_len += 3 + prefixes.iter().map(|s| s.len()).sum::<usize>() + suffix.len();
let mut res = String::with_capacity(out_len);
res.extend(prefixes.iter().copied());
res.push('b');
res.push(quote);
for &ch in b {
match ch {
b'\t' => res.push_str("\\t"),
b'\n' => res.push_str("\\n"),
b'\r' => res.push_str("\\r"),
// printable ascii range
0x20..=0x7e => {
let ch = ch as char;
if ch == quote || ch == '\\' {
res.push('\\');
}
res.push(ch);
}
_ => write!(res, "\\x{ch:02x}").unwrap(),
}
}
res.push(quote);
res.push_str(suffix);
res
}

View File

@@ -1,7 +1,7 @@
//! Implementation of Printf-Style string formatting
//! as per the [Python Docs](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting).
//! Vendored from [cformat.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/cformat.rs).
//! The only changes we make are to remove dead code and code involving the vm.
//! The only changes we make are to remove dead code and code involving the VM.
use std::fmt;
use std::iter::{Enumerate, Peekable};
use std::str::FromStr;

View File

@@ -1,5 +1,5 @@
//! Vendored from [format.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/format.rs).
//! The only changes we make are to remove dead code and code involving the vm.
//! The only changes we make are to remove dead code and code involving the VM.
use itertools::{Itertools, PeekingNext};
#[derive(Debug, PartialEq)]

View File

@@ -1,2 +1,4 @@
pub mod bytes;
pub mod cformat;
pub mod format;
pub mod str;

182
src/vendor/str.rs vendored Normal file
View File

@@ -0,0 +1,182 @@
//! Vendored from [str.rs in rustpython-common](https://github.com/RustPython/RustPython/blob/1d8269fb729c91fc56064e975172d3a11bd62d07/common/src/str.rs).
//! The only changes we make are to remove dead code and make the default quote
//! type configurable.
use std::fmt;
use once_cell::unsync::OnceCell;
#[derive(Debug, Clone, Copy)]
pub enum Quote {
Single,
Double,
}
/// Get a Display-able type that formats to the python `repr()` of the string
/// value.
#[inline]
pub fn repr(s: &str, quote: Quote) -> Repr<'_> {
Repr {
s,
quote,
info: OnceCell::new(),
}
}
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
pub struct ReprOverflowError;
impl fmt::Display for ReprOverflowError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("string is too long to generate repr")
}
}
#[derive(Copy, Clone)]
struct ReprInfo {
dquoted: bool,
out_len: usize,
}
impl ReprInfo {
fn get(s: &str, quote: Quote) -> Result<Self, ReprOverflowError> {
let mut out_len = 0usize;
let mut squote = 0;
let mut dquote = 0;
for ch in s.chars() {
let incr = match ch {
'\'' => {
squote += 1;
1
}
'"' => {
dquote += 1;
1
}
'\\' | '\t' | '\r' | '\n' => 2,
ch if ch < ' ' || ch as u32 == 0x7f => 4, // \xHH
ch if ch.is_ascii() => 1,
ch if rustpython_common::char::is_printable(ch) => {
// max = std::cmp::max(ch, max);
ch.len_utf8()
}
ch if (ch as u32) < 0x100 => 4, // \xHH
ch if (ch as u32) < 0x10000 => 6, // \uHHHH
_ => 10, // \uHHHHHHHH
};
out_len += incr;
if out_len > std::isize::MAX as usize {
return Err(ReprOverflowError);
}
}
let (quote, num_escaped_quotes) = choose_quotes_for_repr(squote, dquote, quote);
// we'll be adding backslashes in front of the existing inner quotes
out_len += num_escaped_quotes;
// start and ending quotes
out_len += 2;
let dquoted = quote == '"';
Ok(ReprInfo { dquoted, out_len })
}
}
pub struct Repr<'a> {
s: &'a str,
// the quote type we prefer to use
quote: Quote,
// the tuple is dquouted, out_len
info: OnceCell<Result<ReprInfo, ReprOverflowError>>,
}
impl Repr<'_> {
fn get_info(&self) -> Result<ReprInfo, ReprOverflowError> {
*self.info.get_or_init(|| ReprInfo::get(self.s, self.quote))
}
fn _fmt<W: fmt::Write>(&self, repr: &mut W, info: ReprInfo) -> fmt::Result {
let s = self.s;
let in_len = s.len();
let ReprInfo { dquoted, out_len } = info;
let quote = if dquoted { '"' } else { '\'' };
// if we don't need to escape anything we can just copy
let unchanged = out_len == in_len;
repr.write_char(quote)?;
if unchanged {
repr.write_str(s)?;
} else {
for ch in s.chars() {
match ch {
'\n' => repr.write_str("\\n"),
'\t' => repr.write_str("\\t"),
'\r' => repr.write_str("\\r"),
// these 2 branches *would* be handled below, but we shouldn't have to do a
// unicodedata lookup just for ascii characters
'\x20'..='\x7e' => {
// printable ascii range
if ch == quote || ch == '\\' {
repr.write_char('\\')?;
}
repr.write_char(ch)
}
ch if ch.is_ascii() => {
write!(repr, "\\x{:02x}", ch as u8)
}
ch if rustpython_common::char::is_printable(ch) => repr.write_char(ch),
'\0'..='\u{ff}' => {
write!(repr, "\\x{:02x}", ch as u32)
}
'\0'..='\u{ffff}' => {
write!(repr, "\\u{:04x}", ch as u32)
}
_ => {
write!(repr, "\\U{:08x}", ch as u32)
}
}?;
}
}
repr.write_char(quote)
}
}
impl fmt::Display for Repr<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let info = self.get_info().unwrap();
self._fmt(f, info)
}
}
/// Returns the outer quotes to use and the number of quotes that need to be
/// escaped.
pub(crate) fn choose_quotes_for_repr(
num_squotes: usize,
num_dquotes: usize,
quote: Quote,
) -> (char, usize) {
match quote {
Quote::Single => {
// always use squote unless we have squotes but no dquotes
let use_dquote = num_squotes > 0 && num_dquotes == 0;
if use_dquote {
('"', num_dquotes)
} else {
('\'', num_squotes)
}
}
Quote::Double => {
// always use dquote unless we have dquotes but no squotes
let use_squote = num_dquotes > 0 && num_squotes == 0;
if use_squote {
('\'', num_squotes)
} else {
('"', num_dquotes)
}
}
}
}