Compare commits

...

5 Commits

Author SHA1 Message Date
Charlie Marsh
40b7c64f7d Bump version to 0.0.160 2022-12-05 12:56:38 -05:00
Jonathan Plasse
a76c5d1226 Add allowed-confusable settings (#1059) 2022-12-05 12:53:55 -05:00
Charlie Marsh
5aeddeb825 Include pyproject.toml path in error message (#1068) 2022-12-05 12:04:50 -05:00
Charlie Marsh
5f8294aea4 Preserve star imports when re-formatting import blocks (#1066) 2022-12-05 11:48:38 -05:00
Charlie Marsh
e07d3f6313 Fix clippy 2022-12-05 11:47:42 -05:00
27 changed files with 280 additions and 154 deletions

View File

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

6
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.159-dev.0"
version = "0.0.160-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.159"
version = "0.0.160"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1873,7 +1873,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.159"
version = "0.0.160"
dependencies = [
"anyhow",
"clap 4.0.29",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.159"
version = "0.0.160"
edition = "2021"
rust-version = "1.65.0"

View File

@@ -145,7 +145,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.159
rev: v0.0.160
hooks:
- id: ruff
```
@@ -1295,7 +1295,7 @@ paths.
```toml
[tool.ruff]
exclude = [".venv"]
````
```
---
@@ -1313,7 +1313,7 @@ A list of file patterns to omit from linting, in addition to those specified by
[tool.ruff]
# In addition to the standard set of exclusions, omit all tests, plus a specific file.
extend-exclude = ["tests", "src/bad.py"]
````
```
---
@@ -1509,6 +1509,26 @@ dummy-variable-rgx = "^_$"
---
#### [`allowed-confusables`](#allowed-confusables)
A list of allowed "confusable" Unicode characters to ignore when enforcing `RUF001`, `RUF002`,
and `RUF003`.
**Default value**: `[]`
**Type**: `Vec<char>`
**Example usage**:
```toml
[tool.ruff]
# Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and greek-small-letter-alpha (U+03B1),
# which could be confused for "-", "p", and "*", respectively.
allowed-confusables = ["", "ρ", ""]
```
---
#### [`ignore-init-module-imports`](#ignore-init-module-imports)
Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be

View File

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

View File

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

View File

@@ -243,6 +243,7 @@ mod tests {
fn it_converts_empty() -> Result<()> {
let actual = convert(&HashMap::from([]), None)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -286,6 +287,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -329,6 +331,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -372,6 +375,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -415,6 +419,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -466,6 +471,7 @@ mod tests {
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -544,6 +550,7 @@ mod tests {
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,

View File

@@ -0,0 +1,6 @@
from some_other_module import some_class
from some_other_module import *
# Above
from some_module import some_class # Aside
# Above
from some_module import * # Aside

View File

@@ -1,4 +1,5 @@
[tool.ruff]
allowed-confusables = ["", "ρ", ""]
line-length = 88
extend-exclude = [
"excluded_file.py",
@@ -35,13 +36,8 @@ ignore-names = [
"longMessage",
"maxDiff",
]
classmethod-decorators = [
"classmethod",
"pydantic.validator",
]
staticmethod-decorators = [
"staticmethod",
]
classmethod-decorators = ["classmethod", "pydantic.validator"]
staticmethod-decorators = ["staticmethod"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"

View File

@@ -1,7 +0,0 @@
x = "𝐁ad string"
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...

View File

@@ -1,7 +0,0 @@
x = "𝐁ad string"
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...

View File

@@ -1,7 +1,14 @@
x = "𝐁ad string"
y = ""
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...
def g():
"""Here's a docstring with a greek rho: ρ"""
# And here's a comment with a greek alpha:
...

View File

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

View File

@@ -118,9 +118,9 @@ pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;
/// Extract all `Cmpop` operators from a source code snippet, with appropriate
/// ranges.
///
/// RustPython doesn't include line and column information on `Cmpop` nodes.
/// (CPython doesn't either.) This method iterates over the token stream and
/// re-identifies `Cmpop` nodes, annotating them with valid arnges.
/// `RustPython` doesn't include line and column information on `Cmpop` nodes.
/// `CPython` doesn't either. This method iterates over the token stream and
/// re-identifies `Cmpop` nodes, annotating them with valid ranges.
pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
let mut tok_iter = lexer::make_tokenizer(contents)
.flatten()

View File

@@ -191,7 +191,18 @@ fn normalize_imports(imports: Vec<AnnotatedImport>, combine_as_imports: bool) ->
} => {
// Associate the comments with the first alias (best effort).
if let Some(alias) = names.first() {
if alias.asname.is_none() || combine_as_imports {
if alias.name == "*" {
let entry = block
.import_from_star
.entry(ImportFromData { module, level })
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
} else if alias.asname.is_none() || combine_as_imports {
let entry = &mut block
.import_from
.entry(ImportFromData { module, level })
@@ -225,7 +236,18 @@ fn normalize_imports(imports: Vec<AnnotatedImport>, combine_as_imports: bool) ->
// Create an entry for every alias.
for alias in names {
if alias.asname.is_none() || combine_as_imports {
if alias.name == "*" {
let entry = block
.import_from_star
.entry(ImportFromData { module, level })
.or_default();
for comment in alias.atop {
entry.atop.push(comment.value);
}
for comment in alias.inline {
entry.inline.push(comment.value);
}
} else if alias.asname.is_none() || combine_as_imports {
let entry = block
.import_from
.entry(ImportFromData { module, level })
@@ -323,6 +345,22 @@ fn categorize_imports<'a>(
.import_from_as
.insert((import_from, alias), comments);
}
// Categorize `StmtKind::ImportFrom` (with star).
for (import_from, comments) in block.import_from_star {
let classification = categorize(
&import_from.module_base(),
import_from.level,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
block_by_type
.entry(classification)
.or_default()
.import_from_star
.insert(import_from, comments);
}
block_by_type
}
@@ -367,6 +405,33 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
)
}),
)
.chain(
// Include all star imports.
block
.import_from_star
.into_iter()
.map(|(import_from, comments)| {
(
import_from,
(
CommentSet {
atop: comments.atop,
inline: vec![],
},
FxHashMap::from_iter([(
AliasData {
name: "*",
asname: None,
},
CommentSet {
atop: vec![],
inline: comments.inline,
},
)]),
),
)
}),
)
.map(|(import_from, (comments, aliases))| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
@@ -486,6 +551,7 @@ mod tests {
#[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"))]
#[test_case(Path::new("preserve_import_star.py"))]
#[test_case(Path::new("preserve_indentation.py"))]
#[test_case(Path::new("reorder_within_section.py"))]
#[test_case(Path::new("separate_first_party_imports.py"))]

View File

@@ -0,0 +1,20 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 7
column: 0
fix:
content: "# Above\nfrom some_module import * # Aside\n\n# Above\nfrom some_module import some_class # Aside\nfrom some_other_module import *\nfrom some_other_module import some_class\n"
location:
row: 1
column: 0
end_location:
row: 7
column: 0

View File

@@ -59,6 +59,9 @@ pub struct ImportBlock<'a> {
// Set of (module, level, name, asname), used to track re-exported 'from' imports.
// Ex) `from module import member as member`
pub import_from_as: FxHashMap<(ImportFromData<'a>, AliasData<'a>), CommentSet<'a>>,
// Map from (module, level) to `AliasData`, used to track star imports.
// Ex) `from module import *`
pub import_from_star: FxHashMap<ImportFromData<'a>, CommentSet<'a>>,
}
type AliasDataWithComments<'a> = (AliasData<'a>, CommentSet<'a>);

View File

@@ -1623,41 +1623,45 @@ pub fn ambiguous_unicode_character(
for current_char in text.chars() {
// Search for confusing characters.
if let Some(representant) = CONFUSABLES.get(&(current_char as u32)) {
if let Some(representant) = char::from_u32(*representant) {
let col = if row_offset == 0 {
start.column() + col_offset
} else {
col_offset
};
let location = Location::new(start.row() + row_offset, col);
let end_location = Location::new(location.row(), location.column() + 1);
let mut check = Check::new(
match context {
Context::String => {
CheckKind::AmbiguousUnicodeCharacterString(current_char, representant)
}
Context::Docstring => CheckKind::AmbiguousUnicodeCharacterDocstring(
current_char,
representant,
),
Context::Comment => {
CheckKind::AmbiguousUnicodeCharacterComment(current_char, representant)
}
},
Range {
location,
end_location,
},
);
if settings.enabled.contains(check.kind.code()) {
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::replacement(
representant.to_string(),
if !settings.allowed_confusables.contains(&current_char) {
if let Some(representant) = char::from_u32(*representant) {
let col = if row_offset == 0 {
start.column() + col_offset
} else {
col_offset
};
let location = Location::new(start.row() + row_offset, col);
let end_location = Location::new(location.row(), location.column() + 1);
let mut check = Check::new(
match context {
Context::String => CheckKind::AmbiguousUnicodeCharacterString(
current_char,
representant,
),
Context::Docstring => CheckKind::AmbiguousUnicodeCharacterDocstring(
current_char,
representant,
),
Context::Comment => CheckKind::AmbiguousUnicodeCharacterComment(
current_char,
representant,
),
},
Range {
location,
end_location,
));
},
);
if settings.enabled.contains(check.kind.code()) {
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::replacement(
representant.to_string(),
location,
end_location,
));
}
checks.push(check);
}
checks.push(check);
}
}
}

View File

@@ -4,30 +4,31 @@ pub mod checks;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use rustc_hash::FxHashSet;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::RUF001, Path::new("RUF001.py"); "RUF001")]
#[test_case(CheckCode::RUF002, Path::new("RUF002.py"); "RUF002")]
#[test_case(CheckCode::RUF003, Path::new("RUF003.py"); "RUF003")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
#[test]
fn confusables() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/ruff")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
Path::new("./resources/test/fixtures/ruff/confusables.py"),
&settings::Settings {
allowed_confusables: FxHashSet::from_iter(['', 'ρ', '']),
..settings::Settings::for_rules(vec![
CheckCode::RUF001,
CheckCode::RUF002,
CheckCode::RUF003,
])
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
insta::assert_yaml_snapshot!(checks);
Ok(())
}

View File

@@ -1,23 +0,0 @@
---
source: src/rules/mod.rs
expression: checks
---
- kind:
AmbiguousUnicodeCharacterString:
- 𝐁
- B
location:
row: 1
column: 5
end_location:
row: 1
column: 6
fix:
content: B
location:
row: 1
column: 5
end_location:
row: 1
column: 6

View File

@@ -1,23 +0,0 @@
---
source: src/rules/mod.rs
expression: checks
---
- kind:
AmbiguousUnicodeCharacterDocstring:
-
- )
location:
row: 5
column: 55
end_location:
row: 5
column: 56
fix:
content: )
location:
row: 5
column: 55
end_location:
row: 5
column: 56

View File

@@ -1,23 +0,0 @@
---
source: src/rules/mod.rs
expression: checks
---
- kind:
AmbiguousUnicodeCharacterComment:
-
- /
location:
row: 6
column: 61
end_location:
row: 6
column: 62
fix:
content: /
location:
row: 6
column: 61
end_location:
row: 6
column: 62

View File

@@ -0,0 +1,59 @@
---
source: src/rules/mod.rs
expression: checks
---
- kind:
AmbiguousUnicodeCharacterString:
- 𝐁
- B
location:
row: 1
column: 5
end_location:
row: 1
column: 6
fix:
content: B
location:
row: 1
column: 5
end_location:
row: 1
column: 6
- kind:
AmbiguousUnicodeCharacterDocstring:
-
- )
location:
row: 6
column: 55
end_location:
row: 6
column: 56
fix:
content: )
location:
row: 6
column: 55
end_location:
row: 6
column: 56
- kind:
AmbiguousUnicodeCharacterComment:
-
- /
location:
row: 7
column: 61
end_location:
row: 7
column: 62
fix:
content: /
location:
row: 7
column: 61
end_location:
row: 7
column: 62

View File

@@ -8,6 +8,7 @@ use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex;
use rustc_hash::FxHashSet;
use crate::checks_gen::{CheckCodePrefix, CATEGORIES};
use crate::settings::pyproject::load_options;
@@ -19,6 +20,7 @@ use crate::{
#[derive(Debug)]
pub struct Configuration {
pub allowed_confusables: FxHashSet<char>,
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
@@ -82,9 +84,12 @@ impl Configuration {
) -> Result<Self> {
let options = load_options(pyproject)?;
Ok(Configuration {
allowed_confusables: FxHashSet::from_iter(
options.allowed_confusables.unwrap_or_default(),
),
dummy_variable_rgx: match options.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
src: options.src.map_or_else(

View File

@@ -28,6 +28,7 @@ pub mod types;
#[derive(Debug)]
pub struct Settings {
pub allowed_confusables: FxHashSet<char>,
pub dummy_variable_rgx: Regex,
pub enabled: FxHashSet<CheckCode>,
pub exclude: GlobSet,
@@ -58,6 +59,7 @@ impl Settings {
project_root: Option<&PathBuf>,
) -> Result<Self> {
Ok(Self {
allowed_confusables: config.allowed_confusables,
dummy_variable_rgx: config.dummy_variable_rgx,
enabled: resolve_codes(
&config
@@ -95,6 +97,7 @@ impl Settings {
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
allowed_confusables: FxHashSet::from_iter([]),
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: FxHashSet::from_iter([check_code.clone()]),
exclude: GlobSet::empty(),
@@ -121,6 +124,7 @@ impl Settings {
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
allowed_confusables: FxHashSet::from_iter([]),
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: FxHashSet::from_iter(check_codes.clone()),
exclude: GlobSet::empty(),
@@ -149,6 +153,9 @@ impl Settings {
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
// Add base properties in alphabetical order.
for confusable in &self.allowed_confusables {
confusable.hash(state);
}
self.dummy_variable_rgx.as_str().hash(state);
for value in &self.enabled {
value.hash(state);

View File

@@ -13,6 +13,7 @@ use crate::{
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub allowed_confusables: Option<Vec<char>>,
pub dummy_variable_rgx: Option<String>,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Option<Vec<String>>,

View File

@@ -2,7 +2,7 @@
use std::path::{Path, PathBuf};
use anyhow::Result;
use anyhow::{anyhow, Result};
use common_path::common_path_all;
use log::debug;
use path_absolutize::Absolutize;
@@ -82,7 +82,8 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
pub fn load_options(pyproject: Option<&PathBuf>) -> Result<Options> {
if let Some(pyproject) = pyproject {
Ok(parse_pyproject_toml(pyproject)?
Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default())
@@ -133,6 +134,7 @@ mod tests {
pyproject.tool,
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -174,6 +176,7 @@ line-length = 79
pyproject.tool,
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -215,6 +218,7 @@ exclude = ["foo.py"]
pyproject.tool,
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
line_length: None,
fix: None,
exclude: Some(vec!["foo.py".to_string()]),
@@ -256,6 +260,7 @@ select = ["E501"]
pyproject.tool,
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -298,6 +303,7 @@ ignore = ["E501"]
pyproject.tool,
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -374,6 +380,7 @@ other-attribute = 1
assert_eq!(
config,
Options {
allowed_confusables: Some(vec!['', 'ρ', '']),
line_length: Some(88),
fix: None,
exclude: None,