Compare commits

...

32 Commits

Author SHA1 Message Date
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
Charlie Marsh
995994be3e Bump version to 0.0.162 2022-12-05 19:07:44 -05:00
Charlie Marsh
f001305b2e Only autofix D205 by deleting blank lines (#1091) 2022-12-05 19:01:32 -05:00
Charlie Marsh
e88093541f Avoid wrapping import-star statements (#1089) 2022-12-05 18:39:16 -05:00
Charlie Marsh
da41a495f1 Remove extraneous plugin creation script 2022-12-05 18:31:19 -05:00
Charlie Marsh
55b7ec8f85 Ignore newline enforcement when imports break indentation boundaries (#1085) 2022-12-05 18:02:41 -05:00
128 changed files with 4581 additions and 1258 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.161
rev: v0.0.167
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.161-dev.0"
version = "0.0.167-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.161"
version = "0.0.167"
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.161"
version = "0.0.167"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1889,6 +1890,16 @@ dependencies = [
"strum_macros",
]
[[package]]
name = "ruff_macros"
version = "0.0.161"
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.161"
version = "0.0.167"
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.161", 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" }

750
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
set -euxo pipefail
NAME=$1
mkdir -p src/$1
mkdir -p resources/test/fixtures/$1
touch src/$1/mod.rs
sed -i "" "s/mod flake8_print;/mod flake8_print; mod flake8_return;/g" src/lib.rs
sed -i "" "s|// flake8-print|// flake8-return\n// flake8-print|g" src/checks.rs

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.161-dev.0"
version = "0.0.167-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

@@ -14,3 +14,28 @@ 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

@@ -0,0 +1 @@
from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore

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.161"
version = "0.0.167"
edition = "2021"
[dependencies]

View File

@@ -76,6 +76,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("AsRefStr")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
@@ -170,9 +171,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;");

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.161"
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")),
}
}

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

@@ -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,
@@ -20,7 +19,7 @@ use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, 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 +35,8 @@ 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, mccabe,
pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -82,7 +81,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 +128,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 +142,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`.
@@ -730,6 +728,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 {
@@ -1066,9 +1077,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) {
@@ -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);
}
@@ -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);
@@ -2327,16 +2344,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 +2511,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 +2817,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 {
@@ -3142,6 +3161,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 +3177,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

@@ -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(

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
@@ -290,6 +292,8 @@ pub enum CheckCode {
FBT001,
FBT002,
FBT003,
// flake8-import-conventions
ICN001,
// Ruff
RUF001,
RUF002,
@@ -317,6 +321,7 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
Flake8Return,
@@ -354,6 +359,7 @@ 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",
@@ -371,6 +377,41 @@ 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::I25],
CheckCategory::Isort => vec![CheckCodePrefix::I00],
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 +453,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,
@@ -589,7 +631,7 @@ pub enum CheckKind {
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-blind-except
BlindExcept,
BlindExcept(String),
// flake8-bugbear
AbstractBaseClassWithoutAbstractMethod(String),
AssignmentToOsEnviron,
@@ -618,6 +660,7 @@ pub enum CheckKind {
UselessComparison,
UselessContextlibSuppress,
UselessExpression,
ZipWithoutExplicitStrict,
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -774,6 +817,8 @@ pub enum CheckKind {
BooleanPositionalValueInFunctionCall,
// pygrep-hooks
NoEval,
// flake8-import-conventions
ImportAliasIsNotConventional(String, String),
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -924,6 +969,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,
@@ -999,7 +1045,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 +1159,10 @@ impl CheckCode {
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// pygrep-hooks
CheckCode::PGH001 => CheckKind::NoEval,
// flake8-import-conventions
CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
}
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -1165,6 +1215,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,6 +1339,7 @@ impl CheckCode {
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::N801 => CheckCategory::PEP8Naming,
CheckCode::N802 => CheckCategory::PEP8Naming,
@@ -1465,6 +1517,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 +1529,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,
@@ -1633,6 +1686,8 @@ impl CheckKind {
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// pygrep-hooks
CheckKind::NoEval => &CheckCode::PGH001,
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1976,6 +2031,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 +2460,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 +2477,10 @@ impl CheckKind {
}
// pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(name, asname) => {
format!("`{name}` should be imported as `{asname}`")
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -2497,6 +2559,8 @@ impl CheckKind {
| CheckKind::DoNotAssertFalse
| CheckKind::DoNotAssignLambda
| CheckKind::DuplicateHandlerException(..)
| CheckKind::EndsInPeriod
| CheckKind::EndsInPunctuation
| CheckKind::GetAttrWithConstant
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue

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,
@@ -68,6 +70,7 @@ pub enum CheckCodePrefix {
B9,
B90,
B904,
B905,
BLE,
BLE0,
BLE00,
@@ -256,6 +259,10 @@ pub enum CheckCodePrefix {
I2,
I25,
I252,
ICN,
ICN0,
ICN00,
ICN001,
N,
N8,
N80,
@@ -513,6 +520,7 @@ impl CheckCodePrefix {
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
CheckCode::B905,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -600,9 +608,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],
@@ -1138,6 +1147,10 @@ impl CheckCodePrefix {
CheckCodePrefix::I2 => vec![CheckCode::I252],
CheckCodePrefix::I25 => vec![CheckCode::I252],
CheckCodePrefix::I252 => vec![CheckCode::I252],
CheckCodePrefix::ICN => vec![CheckCode::ICN001],
CheckCodePrefix::ICN0 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN00 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN001 => vec![CheckCode::ICN001],
CheckCodePrefix::N => vec![
CheckCode::N801,
CheckCode::N802,
@@ -1719,6 +1732,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 +1921,10 @@ 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::N => SuffixLength::Zero,
CheckCodePrefix::N8 => SuffixLength::One,
CheckCodePrefix::N80 => SuffixLength::Two,
@@ -2087,6 +2105,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::F,
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::N,
CheckCodePrefix::PGH,
CheckCodePrefix::PLC,

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,62 @@
use rustpython_ast::{Arguments, Expr, Stmt, StmtKind};
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, 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, 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,4 +1,4 @@
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;
@@ -6,6 +6,7 @@ use crate::ast::visitor::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`.

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

@@ -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,37 @@
use std::collections::BTreeMap;
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: &BTreeMap<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,100 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::path::Path;
use anyhow::Result;
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(BTreeMap::from([
("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(BTreeMap::from([
("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(BTreeMap::from([(
"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,84 @@
//! Settings for import conventions.
use std::collections::BTreeMap;
use ruff_macros::ConfigurationOptions;
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 = "BTreeMap<String, String>",
example = r#"
# Declare the default aliases.
altair = "alt"
matplotlib.pyplot = "plt"
numpy = "np"
pandas = "pd"
seaborn = "sns"
"#
)]
pub aliases: Option<BTreeMap<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 = "BTreeMap<String, String>",
example = r#"
# Declare a custom alias for the `matplotlib` module.
"dask.dataframe" = "dd"
"#
)]
pub extend_aliases: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Hash)]
pub struct Settings {
pub aliases: BTreeMap<String, String>,
}
fn default_aliases() -> BTreeMap<String, String> {
CONVENTIONAL_ALIASES
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect::<BTreeMap<_, _>>()
}
fn resolve_aliases(options: Options) -> BTreeMap<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

@@ -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

@@ -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

@@ -42,6 +42,15 @@ pub fn format_import_from(
force_wrap_aliases: bool,
is_first: bool,
) -> String {
if aliases.len() == 1
&& aliases
.iter()
.all(|(alias, _)| alias.name == "*" && alias.asname.is_none())
{
let (single_line, ..) = format_single_line(import_from, comments, aliases, is_first);
return single_line;
}
// We can only inline if: (1) none of the aliases have atop comments, and (3)
// only the last alias (if any) has inline comments.
if aliases
@@ -58,7 +67,7 @@ pub fn format_import_from(
{
let (single_line, import_length) =
format_single_line(import_from, comments, aliases, is_first);
if import_length <= line_length {
if import_length <= line_length || aliases.iter().any(|(alias, _)| alias.name == "*") {
return single_line;
}
}

View File

@@ -561,6 +561,7 @@ mod tests {
#[test_case(Path::new("insert_empty_lines.py"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("no_wrap_star.py"))]
#[test_case(Path::new("order_by_type.py"))]
#[test_case(Path::new("order_relative_imports_by_level.py"))]
#[test_case(Path::new("preserve_comment_order.py"))]

View File

@@ -51,7 +51,11 @@ pub fn check_imports(
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(block.imports.first().unwrap(), locator);
let has_trailing_content = match_trailing_content(block.imports.last().unwrap(), locator);
let num_trailing_lines = count_trailing_lines(block.imports.last().unwrap(), locator);
let num_trailing_lines = if block.trailer.is_none() {
0
} else {
count_trailing_lines(block.imports.last().unwrap(), locator)
};
// Generate the sorted import block.
let expected = format_imports(

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,20 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 2
column: 0
fix:
content: "from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore\n"
location:
row: 1
column: 0
end_location:
row: 2
column: 0

View File

@@ -23,6 +23,7 @@ pub struct ImportTracker<'a> {
blocks: Vec<Block<'a>>,
directives: &'a IsortDirectives,
split_index: usize,
nested: bool,
}
impl<'a> ImportTracker<'a> {
@@ -31,6 +32,7 @@ impl<'a> ImportTracker<'a> {
directives,
blocks: vec![Block::default()],
split_index: 0,
nested: false,
}
}
@@ -60,12 +62,16 @@ 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
self.finalize(Some(if self.nested {
Trailer::Sibling
} else {
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}));
self.split_index += 1;
} else {
@@ -81,16 +87,22 @@ where
{
self.track_import(stmt);
} else {
self.finalize(Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
self.finalize(Some(if self.nested {
Trailer::Sibling
} else {
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
}));
}
// Track scope.
let prev_nested = self.nested;
self.nested = true;
match &stmt.node {
StmtKind::FunctionDef { body, .. } => {
for stmt in body {
@@ -198,6 +210,7 @@ where
}
_ => {}
}
self.nested = prev_nested;
}
fn visit_annotation(&mut self, _: &'b Expr) {}
@@ -219,11 +232,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,6 +49,7 @@ 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;
@@ -70,7 +71,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

@@ -133,7 +133,7 @@ pub(crate) fn check_path(
Ok(checks)
}
const MAX_ITERATIONS: usize = 100;
const MAX_ITERATIONS: usize = 1;
/// Lint the source code at the given `Path`.
pub fn lint_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 =

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,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

@@ -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

@@ -16,6 +16,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
@@ -388,15 +389,17 @@ pub fn blank_after_summary(checker: &mut Checker, definition: &Definition) {
}
}
// Insert one blank line after the summary (replacing any existing lines).
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() + summary_line + 1, 0),
Location::new(
docstring.location.row() + summary_line + 1 + blanks_count,
0,
),
));
if blanks_count > 1 {
// Insert one blank line after the summary (replacing any existing lines).
check.amend(Fix::replacement(
"\n".to_string(),
Location::new(docstring.location.row() + summary_line + 1, 0),
Location::new(
docstring.location.row() + summary_line + 1 + blanks_count,
0,
),
));
}
}
checker.add_check(check);
}
@@ -619,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(
@@ -642,7 +638,6 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, definition: &Definition)
docstring.location.column() + pattern.len() + line.chars().count(),
),
));
break;
}
}
}
@@ -741,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
@@ -868,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

View File

@@ -9,14 +9,7 @@ expression: checks
end_location:
row: 203
column: 7
fix:
content: "\n"
location:
row: 201
column: 0
end_location:
row: 201
column: 0
fix: ~
- kind: BlankLineAfterSummary
location:
row: 210

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

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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
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: EndsInPunctuation
location:
row: 520
@@ -97,5 +174,27 @@ expression: checks
end_location:
row: 520
column: 32
fix: ~
fix:
content: "."
location:
row: 520
column: 29
end_location:
row: 520
column: 29
- kind: EndsInPunctuation
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

@@ -25,29 +25,30 @@ impl TryFrom<&str> for CFormatSummary {
let mut keywords = FxHashSet::default();
for format_part in format_string.parts {
if let CFormatPart::Spec(CFormatSpec {
let CFormatPart::Spec(CFormatSpec {
mapping_key,
min_field_width,
precision,
..
}) = format_part.1
}) = format_part.1 else
{
match mapping_key {
Some(k) => {
keywords.insert(k);
}
None => {
num_positional += 1;
}
};
if min_field_width == Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
continue;
};
match mapping_key {
Some(k) => {
keywords.insert(k);
}
if precision == Some(CFormatQuantity::FromValuesTuple) {
None => {
num_positional += 1;
starred = true;
}
};
if min_field_width == Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}
if precision == Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}
}

View File

@@ -77,43 +77,39 @@ pub(crate) fn percent_format_extra_named_arguments(
if summary.num_positional > 0 {
return None;
}
if let ExprKind::Dict { keys, values } = &right.node {
if values.len() > keys.len() {
return None; // contains **x splat
}
let missing: Vec<&String> = keys
.iter()
.filter_map(|k| match &k.node {
// We can only check that string literals exist
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
if summary.keywords.contains(value) {
None
} else {
Some(value)
}
}
_ => None,
})
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::PercentFormatExtraNamedArguments(
missing.iter().map(|&s| s.clone()).collect(),
),
location,
))
}
} else {
None
let ExprKind::Dict { keys, values } = &right.node else {
return None;
};
if values.len() > keys.len() {
return None; // contains **x splat
}
let missing: Vec<&String> = keys
.iter()
.filter_map(|k| match &k.node {
// We can only check that string literals exist
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
if summary.keywords.contains(value) {
None
} else {
Some(value)
}
}
_ => None,
})
.collect();
if missing.is_empty() {
return None;
}
Some(Check::new(
CheckKind::PercentFormatExtraNamedArguments(missing.iter().map(|&s| s.clone()).collect()),
location,
))
}
/// F505

View File

@@ -43,30 +43,31 @@ impl TryFrom<&str> for FormatSummary {
let mut keywords = FxHashSet::default();
for format_part in format_string.format_parts {
if let FormatPart::Field {
let FormatPart::Field {
field_name,
format_spec,
..
} = format_part
{
} = format_part else {
continue;
};
let parsed = FieldName::parse(&field_name)?;
match parsed.field_type {
FieldType::Auto => autos.insert(autos.len()),
FieldType::Index(i) => indexes.insert(i),
FieldType::Keyword(k) => keywords.insert(k),
};
let nested = FormatString::from_str(&format_spec)?;
for nested_part in nested.format_parts {
let FormatPart::Field { field_name, .. } = nested_part else {
continue;
};
let parsed = FieldName::parse(&field_name)?;
match parsed.field_type {
FieldType::Auto => autos.insert(autos.len()),
FieldType::Index(i) => indexes.insert(i),
FieldType::Keyword(k) => keywords.insert(k),
};
let nested = FormatString::from_str(&format_spec)?;
for nested_part in nested.format_parts {
if let FormatPart::Field { field_name, .. } = nested_part {
let parsed = FieldName::parse(&field_name)?;
match parsed.field_type {
FieldType::Auto => autos.insert(autos.len()),
FieldType::Index(i) => indexes.insert(i),
FieldType::Keyword(k) => keywords.insert(k),
};
}
}
}
}

View File

@@ -71,7 +71,8 @@ mod tests {
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]
#[test_case(CheckCode::F841, Path::new("F841.py"); "F841")]
#[test_case(CheckCode::F841, Path::new("F841_0.py"); "F841_0")]
#[test_case(CheckCode::F841, Path::new("F841_1.py"); "F841_1")]
#[test_case(CheckCode::F901, Path::new("F901.py"); "F901")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
@@ -90,7 +91,7 @@ mod tests {
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/pyflakes/F841.py"),
Path::new("./resources/test/fixtures/pyflakes/F841_0.py"),
&settings::Settings {
dummy_variable_rgx: Regex::new(r"^z$").unwrap(),
..settings::Settings::for_rule(CheckCode::F841)

View File

@@ -6,19 +6,22 @@ use crate::checks::{Check, CheckKind};
/// F633
pub fn invalid_print_syntax(checker: &mut Checker, left: &Expr) {
if let ExprKind::Name { id, .. } = &left.node {
if id == "print" {
let scope = checker.current_scope();
if let Some(Binding {
kind: BindingKind::Builtin,
..
}) = scope.values.get("print")
{
checker.add_check(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}
}
let ExprKind::Name { id, .. } = &left.node else {
return;
};
if id != "print" {
return;
}
let scope = checker.current_scope();
let Some(Binding {
kind: BindingKind::Builtin,
..
}) = scope.values.get("print") else
{
return;
};
checker.add_check(Check::new(
CheckKind::InvalidPrintSyntax,
Range::from_located(left),
));
}

View File

@@ -26,15 +26,16 @@ fn match_not_implemented(expr: &Expr) -> Option<&Expr> {
/// F901
pub fn raise_not_implemented(checker: &mut Checker, expr: &Expr) {
if let Some(expr) = match_not_implemented(expr) {
let mut check = Check::new(CheckKind::RaiseNotImplemented, Range::from_located(expr));
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
"NotImplementedError".to_string(),
expr.location,
expr.end_location.unwrap(),
));
}
checker.add_check(check);
let Some(expr) = match_not_implemented(expr) else {
return;
};
let mut check = Check::new(CheckKind::RaiseNotImplemented, Range::from_located(expr));
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
"NotImplementedError".to_string(),
expr.location,
expr.end_location.unwrap(),
));
}
checker.add_check(check);
}

View File

@@ -47,6 +47,15 @@ expression: checks
row: 21
column: 9
fix: ~
- kind:
UnusedVariable: baz
location:
row: 26
column: 13
end_location:
row: 26
column: 16
fix: ~
- kind:
UnusedVariable: b
location:
@@ -56,4 +65,13 @@ expression: checks
row: 51
column: 9
fix: ~
- kind:
UnusedVariable: f
location:
row: 71
column: 25
end_location:
row: 71
column: 26
fix: ~

View File

@@ -0,0 +1,77 @@
---
source: src/pyflakes/mod.rs
expression: checks
---
- kind:
UnusedVariable: x
location:
row: 6
column: 4
end_location:
row: 6
column: 5
fix: ~
- kind:
UnusedVariable: y
location:
row: 6
column: 7
end_location:
row: 6
column: 8
fix: ~
- kind:
UnusedVariable: coords
location:
row: 16
column: 13
end_location:
row: 16
column: 19
fix: ~
- kind:
UnusedVariable: coords
location:
row: 20
column: 4
end_location:
row: 20
column: 10
fix: ~
- kind:
UnusedVariable: a
location:
row: 24
column: 5
end_location:
row: 24
column: 6
fix: ~
- kind:
UnusedVariable: b
location:
row: 24
column: 8
end_location:
row: 24
column: 9
fix: ~
- kind:
UnusedVariable: x
location:
row: 24
column: 14
end_location:
row: 24
column: 15
fix: ~
- kind:
UnusedVariable: y
location:
row: 24
column: 17
end_location:
row: 24
column: 18
fix: ~

View File

@@ -38,6 +38,15 @@ expression: checks
row: 21
column: 9
fix: ~
- kind:
UnusedVariable: baz
location:
row: 26
column: 13
end_location:
row: 26
column: 16
fix: ~
- kind:
UnusedVariable: _
location:
@@ -74,4 +83,13 @@ expression: checks
row: 51
column: 9
fix: ~
- kind:
UnusedVariable: f
location:
row: 71
column: 25
end_location:
row: 71
column: 26
fix: ~

View File

@@ -5,11 +5,14 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn no_eval(checker: &mut Checker, func: &Expr) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "eval" {
if checker.is_builtin("eval") {
checker.add_check(Check::new(CheckKind::NoEval, Range::from_located(func)));
}
}
let ExprKind::Name { id, .. } = &func.node else {
return;
};
if id != "eval" {
return;
}
if !checker.is_builtin("eval") {
return;
}
checker.add_check(Check::new(CheckKind::NoEval, Range::from_located(func)));
}

View File

@@ -20,25 +20,28 @@ pub fn consider_merging_isinstance(
let mut obj_to_types: FxHashMap<String, (usize, FxHashSet<String>)> = FxHashMap::default();
for value in values {
if let ExprKind::Call { func, args, .. } = &value.node {
if matches!(&func.node, ExprKind::Name { id, .. } if id == "isinstance") {
if let [obj, types] = &args[..] {
let (num_calls, matches) = obj_to_types
.entry(obj.to_string())
.or_insert_with(|| (0, FxHashSet::default()));
*num_calls += 1;
matches.extend(match &types.node {
ExprKind::Tuple { elts, .. } => {
elts.iter().map(std::string::ToString::to_string).collect()
}
_ => {
vec![types.to_string()]
}
});
}
}
let ExprKind::Call { func, args, .. } = &value.node else {
continue;
};
if !matches!(&func.node, ExprKind::Name { id, .. } if id == "isinstance") {
continue;
}
let [obj, types] = &args[..] else {
continue;
};
let (num_calls, matches) = obj_to_types
.entry(obj.to_string())
.or_insert_with(|| (0, FxHashSet::default()));
*num_calls += 1;
matches.extend(match &types.node {
ExprKind::Tuple { elts, .. } => {
elts.iter().map(std::string::ToString::to_string).collect()
}
_ => {
vec![types.to_string()]
}
});
}
for (obj, (num_calls, types)) in obj_to_types {

View File

@@ -7,14 +7,17 @@ use crate::Check;
/// PLR0402
pub fn consider_using_from_import(checker: &mut Checker, alias: &Alias) {
if let Some(asname) = &alias.node.asname {
if let Some((module, name)) = alias.node.name.rsplit_once('.') {
if name == asname {
checker.add_check(Check::new(
CheckKind::ConsiderUsingFromImport(module.to_string(), name.to_string()),
Range::from_located(alias),
));
}
}
let Some(asname) = &alias.node.asname else {
return;
};
let Some((module, name)) = alias.node.name.rsplit_once('.') else {
return;
};
if name != asname {
return;
}
checker.add_check(Check::new(
CheckKind::ConsiderUsingFromImport(module.to_string(), name.to_string()),
Range::from_located(alias),
));
}

View File

@@ -61,27 +61,29 @@ fn get_member_import_name_alias(checker: &Checker, module: &str, member: &str) -
/// RUF004
pub fn consider_using_sys_exit(checker: &mut Checker, func: &Expr) {
if let ExprKind::Name { id, .. } = &func.node {
for name in ["exit", "quit"] {
if id == name {
if !(name == "exit" && is_module_star_imported(checker, "sys"))
&& checker.is_builtin(name)
{
let mut check =
Check::new(CheckKind::ConsiderUsingSysExit, Range::from_located(func));
if checker.patch(check.kind.code()) {
if let Some(content) = get_member_import_name_alias(checker, "sys", "exit")
{
check.amend(Fix::replacement(
content,
func.location,
func.end_location.unwrap(),
));
}
}
checker.add_check(check);
}
let ExprKind::Name { id, .. } = &func.node else {
return;
};
for name in ["exit", "quit"] {
if id != name {
continue;
}
if name == "exit" && is_module_star_imported(checker, "sys") {
continue;
}
if !checker.is_builtin(name) {
continue;
}
let mut check = Check::new(CheckKind::ConsiderUsingSysExit, Range::from_located(func));
if checker.patch(check.kind.code()) {
if let Some(content) = get_member_import_name_alias(checker, "sys", "exit") {
check.amend(Fix::replacement(
content,
func.location,
func.end_location.unwrap(),
));
}
}
checker.add_check(check);
}
}

View File

@@ -14,35 +14,43 @@ pub fn misplaced_comparison_constant(
ops: &[Cmpop],
comparators: &[Expr],
) {
if let ([op], [right]) = (ops, comparators) {
if matches!(
op,
Cmpop::Eq | Cmpop::NotEq | Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE,
) && matches!(&left.node, &ExprKind::Constant { .. })
&& !matches!(&right.node, &ExprKind::Constant { .. })
{
let reversed_op = match op {
Cmpop::Eq => "==",
Cmpop::NotEq => "!=",
Cmpop::Lt => ">",
Cmpop::LtE => ">=",
Cmpop::Gt => "<",
Cmpop::GtE => "<=",
_ => unreachable!("Expected comparison operator"),
};
let suggestion = format!("{right} {reversed_op} {left}");
let mut check = Check::new(
CheckKind::MisplacedComparisonConstant(suggestion.clone()),
Range::from_located(expr),
);
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
suggestion,
expr.location,
expr.end_location.unwrap(),
));
}
checker.add_check(check);
}
let ([op], [right]) = (ops, comparators) else {
return;
};
if !matches!(
op,
Cmpop::Eq | Cmpop::NotEq | Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE,
) {
return;
}
if !matches!(&left.node, &ExprKind::Constant { .. }) {
return;
}
if matches!(&right.node, &ExprKind::Constant { .. }) {
return;
}
let reversed_op = match op {
Cmpop::Eq => "==",
Cmpop::NotEq => "!=",
Cmpop::Lt => ">",
Cmpop::LtE => ">=",
Cmpop::Gt => "<",
Cmpop::GtE => "<=",
_ => unreachable!("Expected comparison operator"),
};
let suggestion = format!("{right} {reversed_op} {left}");
let mut check = Check::new(
CheckKind::MisplacedComparisonConstant(suggestion.clone()),
Range::from_located(expr),
);
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
suggestion,
expr.location,
expr.end_location.unwrap(),
));
}
checker.add_check(check);
}

View File

@@ -12,23 +12,24 @@ pub fn property_with_parameters(
decorator_list: &[Expr],
args: &Arguments,
) {
if decorator_list
if !decorator_list
.iter()
.any(|d| matches!(&d.node, ExprKind::Name { id, .. } if id == "property"))
{
if checker.is_builtin("property")
&& args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
.count()
> 1
{
checker.add_check(Check::new(
CheckKind::PropertyWithParameters,
Range::from_located(stmt),
));
}
return;
}
if checker.is_builtin("property")
&& args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
.count()
> 1
{
checker.add_check(Check::new(
CheckKind::PropertyWithParameters,
Range::from_located(stmt),
));
}
}

View File

@@ -8,17 +8,23 @@ use crate::Check;
/// PLC0414
pub fn useless_import_alias(checker: &mut Checker, alias: &Alias) {
if let Some(asname) = &alias.node.asname {
if !alias.node.name.contains('.') && &alias.node.name == asname {
let mut check = Check::new(CheckKind::UselessImportAlias, Range::from_located(alias));
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
asname.to_string(),
alias.location,
alias.end_location.unwrap(),
));
}
checker.add_check(check);
}
let Some(asname) = &alias.node.asname else {
return;
};
if alias.node.name.contains('.') {
return;
}
if &alias.node.name != asname {
return;
}
let mut check = Check::new(CheckKind::UselessImportAlias, Range::from_located(alias));
if checker.patch(check.kind.code()) {
check.amend(Fix::replacement(
asname.to_string(),
alias.location,
alias.end_location.unwrap(),
));
}
checker.add_check(check);
}

View File

@@ -79,7 +79,9 @@ pub const BUILTINS: &[&str] = &[
"__package__",
"__spec__",
"abs",
"aiter",
"all",
"anext",
"any",
"ascii",
"bin",

View File

@@ -113,26 +113,28 @@ pub fn remove_super_arguments(locator: &SourceCodeLocator, expr: &Expr) -> Optio
let mut tree = libcst_native::parse_module(&contents, None).ok()?;
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
if let Expression::Call(body) = &mut body.value {
body.args = vec![];
body.whitespace_before_args = ParenthesizableWhitespace::default();
body.whitespace_after_func = ParenthesizableWhitespace::default();
let Statement::Simple(body) = tree.body.first_mut()? else {
return None;
};
let SmallStatement::Expr(body) = body.body.first_mut()? else {
return None;
};
let Expression::Call(body) = &mut body.value else {
return None;
};
let mut state = CodegenState::default();
tree.codegen(&mut state);
body.args = vec![];
body.whitespace_before_args = ParenthesizableWhitespace::default();
body.whitespace_after_func = ParenthesizableWhitespace::default();
return Some(Fix::replacement(
state.to_string(),
range.location,
range.end_location,
));
}
}
}
let mut state = CodegenState::default();
tree.codegen(&mut state);
None
Some(Fix::replacement(
state.to_string(),
range.location,
range.end_location,
))
}
/// UP010

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