Compare commits

..

21 Commits

Author SHA1 Message Date
Charlie Marsh
9a799eb4e6 Bump version to 0.0.87 2022-10-28 19:00:03 -04:00
Anders Kaseorg
f260b873b6 Fix “not a char boundary” error with Unicode in extract_quote (#497) 2022-10-28 18:59:12 -04:00
Charlie Marsh
782a90b584 Add tests for resolve_codes (#498) 2022-10-28 18:58:46 -04:00
Charlie Marsh
7df903dc4d Move around and rename some of the Settings structs (#496) 2022-10-28 18:46:54 -04:00
Charlie Marsh
8fc5e91ec7 Enable prefix-based check code selection (#493) 2022-10-28 18:19:57 -04:00
Charlie Marsh
9ca1a2c273 Fix failing pyproject.toml test 2022-10-28 18:13:07 -04:00
Charlie Marsh
86265c1d7c Implement the flake8-quotes plugin (#495) 2022-10-28 17:52:11 -04:00
Charlie Marsh
a057c9a323 Move invalid_escape_sequence into pycodestyle (#494) 2022-10-28 12:20:11 -04:00
Trevor Gross
2e63bb6dcb Update hook id in README and in .pre-commit-config.yaml (#492) 2022-10-27 17:32:52 -04:00
Charlie Marsh
1b5db80b32 Update pre-commit invocation in README.md 2022-10-27 17:19:22 -04:00
Charlie Marsh
3f20cea402 Bump version to 0.0.86 2022-10-27 13:09:57 -04:00
Charlie Marsh
389fe1ff64 Avoid auto-fixing unused imports in __init__.py (#489) 2022-10-27 13:08:04 -04:00
Charlie Marsh
bad2d7ba85 Add example of per-file ignores to the README (#488) 2022-10-27 12:58:52 -04:00
Charlie Marsh
416aa298ac Allow whitespace in per-file ignore patterns (#487) 2022-10-27 12:55:28 -04:00
Charlie Marsh
a535b1adbf Replace compliance comments with check codes (#485) 2022-10-27 09:32:18 -04:00
Charlie Marsh
05fbd1a283 Bump version to 0.0.85 2022-10-26 19:13:04 -04:00
Charlie Marsh
63552cbc8e Implement W605 (invalid escape sequence) (#482) 2022-10-26 19:10:24 -04:00
Charlie Marsh
c00bd489f1 Fix multi-segment import removal (#480) 2022-10-26 16:43:55 -04:00
Charlie Marsh
16c2e3a995 Handle multi-segment import-from removal (#479) 2022-10-26 16:36:12 -04:00
Anders Kaseorg
650b025181 Suppress “No pyproject.toml found” message with --quiet (#478) 2022-10-26 16:03:25 -04:00
Anders Kaseorg
8fe46f7400 Rename --quiet to --silent and make --quiet only log errors (#477) 2022-10-26 16:03:10 -04:00
93 changed files with 3785 additions and 594 deletions

View File

@@ -1,8 +1,8 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
rev: v0.0.87
hooks:
- id: lint
- id: ruff
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.10.1

12
Cargo.lock generated
View File

@@ -466,6 +466,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "codegen"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff61280aed771c3070e7dcc9e050c66f1eb1e3b96431ba66f9f74641d02fc41d"
dependencies = [
"indexmap",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@@ -2045,7 +2054,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.84"
version = "0.0.87"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2054,6 +2063,7 @@ dependencies = [
"chrono",
"clap",
"clearscreen",
"codegen",
"colored",
"common-path",
"dirs 4.0.0",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.84"
version = "0.0.87"
edition = "2021"
[lib]
@@ -50,6 +50,7 @@ getrandom = { version = "0.2.7", features = ["js"] }
[dev-dependencies]
assert_cmd = { version = "2.0.4" }
codegen = { version = "0.2.0" }
insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" }

View File

@@ -77,11 +77,14 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.84
rev: v0.0.87
hooks:
- id: lint
- id: ruff
```
<!-- TODO(charlie): Remove this message a few versions after v0.0.86. -->
_Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as the hook ID._
## Configuration
Ruff is configurable both via `pyproject.toml` and the command line.
@@ -95,9 +98,23 @@ select = [
"F401",
"F403",
]
per-file-ignores = [
"__init__.py:F401",
"path/to/file.py:F401"
]
```
Alternatively, on the command-line:
Plugin configurations should be expressed as subsections, e.g.:
```toml
[tool.ruff]
line-length = 88
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
```
Alternatively, common configuration settings can be provided via the command-line:
```shell
ruff path/to/code/ --select F401 --select F403
@@ -257,7 +274,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | |
### pycodestyle
### pycodestyle (error)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -275,7 +292,13 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
| E902 | IOError | IOError: `...` | |
| E999 | SyntaxError | SyntaxError: `...` | |
### pycodestyle (warning)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
### pydocstyle
@@ -402,6 +425,15 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
| T201 | PrintFound | `print` found | 🛠 |
| T203 | PPrintFound | `pprint` found | 🛠 |
### flake8-quotes
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | |
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | |
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
### Meta rules
| Code | Name | Message | Fix |
@@ -473,6 +505,7 @@ including:
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (8/34)
@@ -493,6 +526,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (9/32)

View File

@@ -0,0 +1,105 @@
//! Generate the CheckCodePrefix enum.
use std::collections::{BTreeMap, BTreeSet};
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use strum::IntoEnumIterator;
use ruff::checks::CheckCode;
fn main() {
// Build up a map from prefix to matching CheckCodes.
let mut prefix_to_codes: BTreeMap<String, BTreeSet<CheckCode>> = Default::default();
for check_code in CheckCode::iter() {
let as_ref = check_code.as_ref().to_string();
for i in 1..=as_ref.len() {
let prefix = as_ref[..i].to_string();
let entry = prefix_to_codes
.entry(prefix)
.or_insert_with(|| Default::default());
entry.insert(check_code.clone());
}
}
let mut scope = Scope::new();
// Create the `CheckCodePrefix` definition.
let mut gen = scope
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
.derive("Clone")
.derive("Serialize")
.derive("Deserialize");
for (prefix, _) in &prefix_to_codes {
gen = gen.push_variant(Variant::new(prefix.to_string()));
}
// Create the `PrefixSpecificity` definition.
scope
.new_enum("PrefixSpecificity")
.vis("pub")
.derive("PartialEq")
.derive("Eq")
.derive("PartialOrd")
.derive("Ord")
.push_variant(Variant::new("Category"))
.push_variant(Variant::new("Hundreds"))
.push_variant(Variant::new("Tens"))
.push_variant(Variant::new("Explicit"));
// Create the `match` statement, to map from definition to relevant codes.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("codes")
.arg_ref_self()
.ret(Type::new("Vec<CheckCode>"))
.vis("pub")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
}
gen.line("}");
// Create the `match` statement, to map from definition to specificity.
let mut gen = scope
.new_impl("CheckCodePrefix")
.new_fn("specificity")
.arg_ref_self()
.ret(Type::new("PrefixSpecificity"))
.vis("pub")
.line("match self {");
for (prefix, _) in &prefix_to_codes {
let specificity = match prefix.len() {
4 => "Explicit",
3 => "Tens",
2 => "Hundreds",
1 => "Category",
_ => panic!("Invalid prefix: {}", prefix),
};
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => PrefixSpecificity::{},",
specificity
));
}
gen.line("}");
println!("//! File automatically generated by examples/generate_check_code_prefix.rs.");
println!();
println!("use serde::{{Deserialize, Serialize}};");
println!("use strum_macros::EnumString;");
println!();
println!("use crate::checks::CheckCode;");
println!();
println!("{}", scope.to_string());
}

5
resources/test/fixtures/F401_5.py vendored Normal file
View File

@@ -0,0 +1,5 @@
"""Test: removal of multi-segment and aliases imports."""
from a.b import c
from d.e import f as g
import h.i
import j.k as l

35
resources/test/fixtures/W605.py vendored Normal file
View File

@@ -0,0 +1,35 @@
#: W605:1:10
regex = '\.png$'
#: W605:2:1
regex = '''
\.png$
'''
#: W605:2:6
f(
'\_'
)
#: W605:4:6
"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: Okay
regex = r'\.png$'
regex = '\\.png$'
regex = r'''
\.png$
'''
regex = r'''
\\.png$
'''
s = '\\'
regex = '\w' # noqa
regex = '''
\w
''' # noqa

View File

@@ -1,3 +1,5 @@
import os
print(__path__)
__all__ = ["a", "b", "c"]

View File

@@ -0,0 +1,38 @@
"""
Double quotes multiline module docstring
"""
"""
this is not a docstring
"""
l = []
class Cls:
"""
Double quotes multiline class docstring
"""
"""
this is not a docstring
"""
# The colon in the list indexing below is an edge case for the docstring scanner
def f(self, bar="""
definitely not a docstring""",
val=l[Cls():3]):
"""
Double quotes multiline function docstring
"""
some_expression = 'hello world'
"""
this is not a docstring
"""
if l:
"""
Looks like a docstring, but in reality it isn't - only modules, classes and functions
"""
pass

View File

@@ -0,0 +1,9 @@
class SingleLineDocstrings():
""" Double quotes single line class docstring """
""" Not a docstring """
def foo(self, bar="""not a docstring"""):
""" Double quotes single line method docstring"""
pass
class Nested(foo()[:]): """ inline docstring """; pass

View File

@@ -0,0 +1,22 @@
def foo():
"""function without params, single line docstring"""
""" not a docstring"""
return
def foo2():
"""
function without params, multiline docstring
"""
""" not a docstring"""
return
def fun_with_params_no_docstring(a, b="""
not a
""" """docstring"""):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
""" not a docstring """):
pass

View File

@@ -0,0 +1,11 @@
"""
Double quotes multiline module docstring
"""
"""
this is not a docstring
"""
def foo():
pass
"""
this is not a docstring
"""

View File

@@ -0,0 +1,6 @@
""" Double quotes singleline module docstring """
""" this is not a docstring """
def foo():
pass
""" this is not a docstring """

View File

@@ -0,0 +1,40 @@
'''
Single quotes multiline module docstring
'''
'''
this is not a docstring
'''
l = []
class Cls(MakeKlass('''
class params \t not a docstring
''')):
'''
Single quotes multiline class docstring
'''
'''
this is not a docstring
'''
# The colon in the list indexing below is an edge case for the docstring scanner
def f(self, bar='''
definitely not a docstring''',
val=l[Cls():3]):
'''
Single quotes multiline function docstring
'''
some_expression = 'hello world'
'''
this is not a docstring
'''
if l:
'''
Looks like a docstring, but in reality it isn't - only modules, classes and functions
'''
pass

View File

@@ -0,0 +1,9 @@
class SingleLineDocstrings():
''' Double quotes single line class docstring '''
''' Not a docstring '''
def foo(self, bar='''not a docstring'''):
''' Double quotes single line method docstring'''
pass
class Nested(foo()[:]): ''' inline docstring '''; pass

View File

@@ -0,0 +1,23 @@
def foo():
'''function without params, single line docstring'''
''' not a docstring'''
return
def foo2():
'''
function without params, multiline docstring
'''
''' not a docstring'''
return
def fun_with_params_no_docstring(a, b='''
not a
''' '''docstring'''):
pass
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
''' not a docstring '''):
pass

View File

@@ -0,0 +1,11 @@
'''
Double quotes multiline module docstring
'''
'''
this is not a docstring
'''
def foo():
pass
'''
this is not a docstring
'''

View File

@@ -0,0 +1,6 @@
''' Double quotes singleline module docstring '''
''' this is not a docstring '''
def foo():
pass
''' this is not a docstring '''

View File

@@ -0,0 +1,2 @@
this_should_be_linted = "double quote string"
this_should_be_linted = u"double quote string"

View File

@@ -0,0 +1,5 @@
this_should_raise_Q003 = 'This is a \'string\''
this_is_fine = '"This" is a \'string\''
this_is_fine = "This is a 'string'"
this_is_fine = "\"This\" is a 'string'"
this_is_fine = r'This is a \'string\''

View File

@@ -0,0 +1,9 @@
s = """ This "should"
be
"linted" """
s = ''' This "should"
"not" be
"linted" '''
s = """'This should not be linted due to having would-be quadruple end quote'"""

View File

@@ -0,0 +1 @@
this_should_not_be_linted = "double quote string" # noqa

View File

@@ -0,0 +1,2 @@
s = 'double "quotes" wrapped in singles are ignored'
s = "single 'quotes' wrapped in doubles are ignored"

View File

@@ -0,0 +1,2 @@
this_should_be_linted = 'single quote string'
this_should_be_linted = u'double quote string'

View File

@@ -0,0 +1,5 @@
this_should_raise_Q003 = "This is a \"string\""
this_is_fine = "'This' is a \"string\""
this_is_fine = 'This is a "string"'
this_is_fine = '\'This\' is a "string"'
this_is_fine = r"This is a \"string\""

View File

@@ -0,0 +1,9 @@
s = ''' This 'should'
be
'linted' '''
s = """ This 'should'
'not' be
'linted' """
s = '''"This should not be linted due to having would-be quadruple end quote"'''

View File

@@ -0,0 +1 @@
this_should_not_be_linted = 'single quote string' # noqa

View File

@@ -0,0 +1,2 @@
s = "single 'quotes' wrapped in doubles are ignored"
s = 'double "quotes" wrapped in singles are ignored'

View File

@@ -5,3 +5,12 @@ extend-exclude = [
"migrations",
"directory/also_excluded.py",
]
per-file-ignores = [
"__init__.py:F401",
]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"
docstring-quotes = "double"
avoid-escape = true

View File

@@ -1,3 +1,5 @@
//! Lint rules based on AST traversal.
use std::collections::{BTreeMap, BTreeSet};
use std::ops::Deref;
use std::path::Path;
@@ -24,7 +26,8 @@ use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::settings::{PythonVersion, Settings};
use crate::settings::types::PythonVersion;
use crate::settings::Settings;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, pep8_naming,
@@ -2113,15 +2116,27 @@ impl<'a> Checker<'a> {
None
};
let mut check = Check::new(
CheckKind::UnusedImport(full_names.into_iter().map(String::from).collect()),
self.locate_check(Range::from_located(child)),
);
if let Some(fix) = fix {
check.amend(fix);
if self.path.ends_with("__init__.py") {
self.checks.push(Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
true,
),
self.locate_check(Range::from_located(child)),
));
} else {
let mut check = Check::new(
CheckKind::UnusedImport(
full_names.into_iter().map(String::from).collect(),
false,
),
self.locate_check(Range::from_located(child)),
);
if let Some(fix) = fix {
check.amend(fix);
}
self.checks.push(check);
}
self.checks.push(check);
}
}
}

View File

@@ -1,3 +1,5 @@
//! Lint rules based on checking raw physical lines.
use std::collections::BTreeMap;
use rustpython_parser::ast::Location;
@@ -227,7 +229,7 @@ pub fn check_lines(
mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::settings;
use crate::settings::Settings;
use super::check_lines;
@@ -241,9 +243,9 @@ mod tests {
&mut checks,
line,
&noqa_line_for,
&settings::Settings {
&Settings {
line_length,
..settings::Settings::for_rule(CheckCode::E501)
..Settings::for_rule(CheckCode::E501)
},
&fixer::Mode::Generate,
);

54
src/check_tokens.rs Normal file
View File

@@ -0,0 +1,54 @@
//! Lint rules based on token traversal.
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::operations::SourceCodeLocator;
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::docstring_detection::StateMachine;
use crate::{flake8_quotes, pycodestyle, Settings};
pub fn check_tokens(
checks: &mut Vec<Check>,
contents: &str,
tokens: &[LexResult],
settings: &Settings,
) {
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
let enforce_quotes = settings.enabled.contains(&CheckCode::Q000)
| settings.enabled.contains(&CheckCode::Q001)
| settings.enabled.contains(&CheckCode::Q002)
| settings.enabled.contains(&CheckCode::Q003);
// TODO(charlie): Use a shared SourceCodeLocator between this site and the AST traversal.
let locator = SourceCodeLocator::new(contents);
let mut state_machine = StateMachine::new();
for (start, tok, end) in tokens.iter().flatten() {
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
checks.extend(pycodestyle::checks::invalid_escape_sequence(
&locator, start, end,
));
}
}
// flake8-quotes
if enforce_quotes {
let is_docstring = state_machine.consume(tok);
if matches!(tok, Tok::String { .. }) {
if let Some(check) = flake8_quotes::checks::quotes(
&locator,
start,
end,
is_docstring,
&settings.flake8_quotes,
) {
if settings.enabled.contains(check.kind.code()) {
checks.push(check);
}
}
}
}
}
}

View File

@@ -7,6 +7,7 @@ use strum_macros::{AsRefStr, EnumIter, EnumString};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::flake8_quotes::settings::Quote;
use crate::pyupgrade::types::Primitive;
#[derive(
@@ -41,6 +42,7 @@ pub enum CheckCode {
E999,
// pycodestyle warnings
W292,
W605,
// pyflakes
F401,
F402,
@@ -101,6 +103,11 @@ pub enum CheckCode {
// flake8-print
T201,
T203,
// flake8-quotes
Q000,
Q001,
Q002,
Q003,
// pyupgrade
U001,
U002,
@@ -174,7 +181,8 @@ pub enum CheckCode {
#[derive(EnumIter, Debug, PartialEq, Eq)]
pub enum CheckCategory {
Pyflakes,
Pycodestyle,
PycodestyleError,
PycodestyleWarning,
Pydocstyle,
Pyupgrade,
PEP8Naming,
@@ -182,18 +190,21 @@ pub enum CheckCategory {
Flake8Bugbear,
Flake8Builtins,
Flake8Print,
Flake8Quotes,
Meta,
}
impl CheckCategory {
pub fn title(&self) -> &'static str {
match self {
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::PycodestyleError => "pycodestyle (error)",
CheckCategory::PycodestyleWarning => "pycodestyle (warning)",
CheckCategory::Pyflakes => "Pyflakes",
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -205,8 +216,9 @@ impl CheckCategory {
#[allow(clippy::upper_case_acronyms)]
pub enum LintSource {
AST,
Lines,
FileSystem,
Lines,
Tokens,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -234,6 +246,7 @@ pub enum CheckKind {
TypeComparison,
// pycodestyle warnings
NoNewLineAtEndOfFile,
InvalidEscapeSequence(char),
// pyflakes
AssertTuple,
BreakOutsideLoop,
@@ -260,7 +273,7 @@ pub enum CheckKind {
UndefinedExport(String),
UndefinedLocal(String),
UndefinedName(String),
UnusedImport(Vec<String>),
UnusedImport(Vec<String>, bool),
UnusedVariable(String),
YieldOutsideFunction,
// flake8-builtins
@@ -294,6 +307,11 @@ pub enum CheckKind {
// flake8-print
PrintFound,
PPrintFound,
// flake8-quotes
BadQuotesInlineString(Quote),
BadQuotesMultilineString(Quote),
BadQuotesDocstring(Quote),
AvoidQuoteEscape,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@@ -369,6 +387,11 @@ impl CheckCode {
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
CheckCode::W605
| CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
| CheckCode::Q003 => &LintSource::Tokens,
CheckCode::E902 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
@@ -394,8 +417,9 @@ impl CheckCode {
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
// pycodestyle warnings
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
CheckCode::W605 => CheckKind::InvalidEscapeSequence('c'),
// pyflakes
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()], false),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
@@ -469,6 +493,11 @@ impl CheckCode {
// flake8-print
CheckCode::T201 => CheckKind::PrintFound,
CheckCode::T203 => CheckKind::PPrintFound,
// flake8-quotes
CheckCode::Q000 => CheckKind::BadQuotesInlineString(Quote::Double),
CheckCode::Q001 => CheckKind::BadQuotesMultilineString(Quote::Double),
CheckCode::Q002 => CheckKind::BadQuotesDocstring(Quote::Double),
CheckCode::Q003 => CheckKind::AvoidQuoteEscape,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@@ -561,21 +590,22 @@ impl CheckCode {
pub fn category(&self) -> CheckCategory {
match self {
CheckCode::E402 => CheckCategory::Pycodestyle,
CheckCode::E501 => CheckCategory::Pycodestyle,
CheckCode::E711 => CheckCategory::Pycodestyle,
CheckCode::E712 => CheckCategory::Pycodestyle,
CheckCode::E713 => CheckCategory::Pycodestyle,
CheckCode::E714 => CheckCategory::Pycodestyle,
CheckCode::E721 => CheckCategory::Pycodestyle,
CheckCode::E722 => CheckCategory::Pycodestyle,
CheckCode::E731 => CheckCategory::Pycodestyle,
CheckCode::E741 => CheckCategory::Pycodestyle,
CheckCode::E742 => CheckCategory::Pycodestyle,
CheckCode::E743 => CheckCategory::Pycodestyle,
CheckCode::E902 => CheckCategory::Pycodestyle,
CheckCode::E999 => CheckCategory::Pycodestyle,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::E402 => CheckCategory::PycodestyleError,
CheckCode::E501 => CheckCategory::PycodestyleError,
CheckCode::E711 => CheckCategory::PycodestyleError,
CheckCode::E712 => CheckCategory::PycodestyleError,
CheckCode::E713 => CheckCategory::PycodestyleError,
CheckCode::E714 => CheckCategory::PycodestyleError,
CheckCode::E721 => CheckCategory::PycodestyleError,
CheckCode::E722 => CheckCategory::PycodestyleError,
CheckCode::E731 => CheckCategory::PycodestyleError,
CheckCode::E741 => CheckCategory::PycodestyleError,
CheckCode::E742 => CheckCategory::PycodestyleError,
CheckCode::E743 => CheckCategory::PycodestyleError,
CheckCode::E902 => CheckCategory::PycodestyleError,
CheckCode::E999 => CheckCategory::PycodestyleError,
CheckCode::W292 => CheckCategory::PycodestyleWarning,
CheckCode::W605 => CheckCategory::PycodestyleWarning,
CheckCode::F401 => CheckCategory::Pyflakes,
CheckCode::F402 => CheckCategory::Pyflakes,
CheckCode::F403 => CheckCategory::Pyflakes,
@@ -631,6 +661,10 @@ impl CheckCode {
CheckCode::C417 => CheckCategory::Flake8Comprehensions,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
CheckCode::Q000 => CheckCategory::Flake8Quotes,
CheckCode::Q001 => CheckCategory::Flake8Quotes,
CheckCode::Q002 => CheckCategory::Flake8Quotes,
CheckCode::Q003 => CheckCategory::Flake8Quotes,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@@ -743,11 +777,12 @@ impl CheckKind {
CheckKind::UndefinedExport(_) => &CheckCode::F822,
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
CheckKind::UndefinedName(_) => &CheckCode::F821,
CheckKind::UnusedImport(_) => &CheckCode::F401,
CheckKind::UnusedImport(_, _) => &CheckCode::F401,
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
// pycodestyle warnings
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
CheckKind::InvalidEscapeSequence(_) => &CheckCode::W605,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
@@ -779,6 +814,11 @@ impl CheckKind {
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
// flake8-quotes
CheckKind::BadQuotesInlineString(_) => &CheckCode::Q000,
CheckKind::BadQuotesMultilineString(_) => &CheckCode::Q001,
CheckKind::BadQuotesDocstring(_) => &CheckCode::Q002,
CheckKind::AvoidQuoteEscape => &CheckCode::Q003,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@@ -971,9 +1011,13 @@ impl CheckKind {
CheckKind::UndefinedName(name) => {
format!("Undefined name `{name}`")
}
CheckKind::UnusedImport(names) => {
CheckKind::UnusedImport(names, in_init_py) => {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
format!("{names} imported but unused")
if *in_init_py {
format!("{names} imported but unused and missing from `__all__`")
} else {
format!("{names} imported but unused")
}
}
CheckKind::UnusedVariable(name) => {
format!("Local variable `{name}` is assigned to but never used")
@@ -983,6 +1027,7 @@ impl CheckKind {
}
// pycodestyle warnings
CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(),
CheckKind::InvalidEscapeSequence(char) => format!("Invalid escape sequence: '\\{char}'"),
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
@@ -1087,6 +1132,26 @@ impl CheckKind {
// flake8-print
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
// flake8-quotes
CheckKind::BadQuotesInlineString(quote) => {
match quote {
Quote::Single => "Double quotes found but single quotes preferred".to_string(),
Quote::Double => "Single quotes found but double quotes preferred".to_string(),
}
},
CheckKind::BadQuotesMultilineString(quote) => {
match quote {
Quote::Single => "Double quote multiline found but single quotes preferred".to_string(),
Quote::Double => "Single quote multiline found but double quotes preferred".to_string(),
}
},
CheckKind::BadQuotesDocstring(quote) => {
match quote {
Quote::Single => "Double quote docstring found but single quotes preferred".to_string(),
Quote::Double => "Single quote docstring found but double quotes preferred".to_string(),
}
},
CheckKind::AvoidQuoteEscape => "Change outer quotes to avoid escaping inner quotes".to_string(),
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -1328,7 +1393,7 @@ impl CheckKind {
| CheckKind::SuperCallWithParameters
| CheckKind::TypeOfPrimitive(_)
| CheckKind::UnnecessaryAbspath
| CheckKind::UnusedImport(_)
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedLoopControlVariable(_)
| CheckKind::UnusedNOQA(_)
| CheckKind::UsePEP585Annotation(_)

1031
src/checks_gen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,11 @@ use clap::{command, Parser};
use log::warn;
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::printer::SerializationFormat;
use crate::pyproject::StrCheckCodePair;
use crate::settings::PythonVersion;
use crate::RawSettings;
use crate::settings::configuration::Configuration;
use crate::settings::types::PythonVersion;
use crate::settings::types::StrCheckCodePair;
#[derive(Debug, Parser)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
@@ -21,11 +21,14 @@ pub struct Cli {
#[arg(long)]
pub config: Option<PathBuf>,
/// Enable verbose logging.
#[arg(short, long)]
#[arg(short, long, group = "verbosity")]
pub verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long)]
/// Only log errors.
#[arg(short, long, group = "verbosity")]
pub quiet: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[arg(short, long, group = "verbosity")]
pub silent: bool,
/// Exit with status code "0", even upon detecting errors.
#[arg(short, long)]
pub exit_zero: bool,
@@ -40,16 +43,16 @@ pub struct Cli {
pub no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCode>,
pub select: Vec<CheckCodePrefix>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCode>,
pub extend_select: Vec<CheckCodePrefix>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCode>,
pub ignore: Vec<CheckCodePrefix>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCode>,
pub extend_ignore: Vec<CheckCodePrefix>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
@@ -103,10 +106,10 @@ impl fmt::Display for Warnable {
/// Warn the user if they attempt to enable a code that won't be respected.
pub fn warn_on(
flag: Warnable,
codes: &[CheckCode],
cli_ignore: &[CheckCode],
cli_extend_ignore: &[CheckCode],
pyproject_settings: &RawSettings,
codes: &[CheckCodePrefix],
cli_ignore: &[CheckCodePrefix],
cli_extend_ignore: &[CheckCodePrefix],
pyproject_configuration: &Configuration,
pyproject_path: &Option<PathBuf>,
) {
for code in codes {
@@ -114,7 +117,7 @@ pub fn warn_on(
if cli_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
}
} else if pyproject_settings.ignore.contains(code) {
} else if pyproject_configuration.ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
@@ -128,7 +131,7 @@ pub fn warn_on(
if cli_extend_ignore.contains(code) {
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
}
} else if pyproject_settings.extend_ignore.contains(code) {
} else if pyproject_configuration.extend_ignore.contains(code) {
if let Some(path) = pyproject_path {
warn!(
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",

42
src/cst/helpers.rs Normal file
View File

@@ -0,0 +1,42 @@
use libcst_native::{Expression, NameOrAttribute};
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
match &expr {
Expression::Call(expr) => {
compose_call_path_inner(&expr.func, parts);
}
Expression::Attribute(expr) => {
compose_call_path_inner(&expr.value, parts);
parts.push(expr.attr.value);
}
Expression::Name(expr) => {
parts.push(expr.value);
}
_ => {}
}
}
pub fn compose_call_path(expr: &Expression) -> Option<String> {
let mut segments = vec![];
compose_call_path_inner(expr, &mut segments);
if segments.is_empty() {
None
} else {
Some(segments.join("."))
}
}
pub fn compose_module_path(module: &NameOrAttribute) -> String {
match module {
NameOrAttribute::N(name) => name.value.to_string(),
NameOrAttribute::A(attr) => {
let name = attr.attr.value;
let prefix = compose_call_path(&attr.value);
if let Some(prefix) = prefix {
format!("{prefix}.{name}")
} else {
name.to_string()
}
}
}
}

1
src/cst/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod helpers;

View File

@@ -37,7 +37,7 @@ fn first_argument_with_matching_function<'a>(
Some(&args.first()?.node)
}
/// Check `list(generator)` compliance.
/// C400 (`list(generator)`)
pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("list", func, args)?;
if let ExprKind::GeneratorExp { .. } = argument {
@@ -49,7 +49,7 @@ pub fn unnecessary_generator_list(expr: &Expr, func: &Expr, args: &[Expr]) -> Op
None
}
/// Check `set(generator)` compliance.
/// C401 (`set(generator)`)
pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
if let ExprKind::GeneratorExp { .. } = argument {
@@ -61,7 +61,7 @@ pub fn unnecessary_generator_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Opt
None
}
/// Check `dict((x, y) for x, y in iterable)` compliance.
/// C402 (`dict((x, y) for x, y in iterable)`)
pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
if let ExprKind::GeneratorExp { elt, .. } = argument {
@@ -78,7 +78,7 @@ pub fn unnecessary_generator_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Op
None
}
/// Check `set([...])` compliance.
/// C403 (`set([...])`)
pub fn unnecessary_list_comprehension_set(
expr: &Expr,
func: &Expr,
@@ -94,7 +94,7 @@ pub fn unnecessary_list_comprehension_set(
None
}
/// Check `dict([...])` compliance.
/// C404 (`dict([...])`)
pub fn unnecessary_list_comprehension_dict(
expr: &Expr,
func: &Expr,
@@ -115,7 +115,7 @@ pub fn unnecessary_list_comprehension_dict(
None
}
/// Check `set([1, 2])` compliance.
/// C405 (`set([1, 2])`)
pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("set", func, args)?;
let kind = match argument {
@@ -129,7 +129,7 @@ pub fn unnecessary_literal_set(expr: &Expr, func: &Expr, args: &[Expr]) -> Optio
))
}
/// Check `dict([(1, 2)])` compliance.
/// C406 (`dict([(1, 2)])`)
pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = exactly_one_argument_with_matching_function("dict", func, args)?;
let (kind, elts) = match argument {
@@ -151,6 +151,7 @@ pub fn unnecessary_literal_dict(expr: &Expr, func: &Expr, args: &[Expr]) -> Opti
))
}
/// C408
pub fn unnecessary_collection_call(
expr: &Expr,
func: &Expr,
@@ -162,10 +163,10 @@ pub fn unnecessary_collection_call(
}
let id = function_name(func)?;
match id {
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
"list" | "tuple" => {
// list() or tuple()
}
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => (),
_ => return None,
};
Some(Check::new(
@@ -174,6 +175,7 @@ pub fn unnecessary_collection_call(
))
}
/// C409
pub fn unnecessary_literal_within_tuple_call(
expr: &Expr,
func: &Expr,
@@ -191,6 +193,7 @@ pub fn unnecessary_literal_within_tuple_call(
))
}
/// C410
pub fn unnecessary_literal_within_list_call(
expr: &Expr,
func: &Expr,
@@ -208,6 +211,7 @@ pub fn unnecessary_literal_within_list_call(
))
}
/// C411
pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let argument = first_argument_with_matching_function("list", func, args)?;
if let ExprKind::ListComp { .. } = argument {
@@ -219,6 +223,7 @@ pub fn unnecessary_list_call(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<
None
}
/// C413
pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let outer = function_name(func)?;
if !(outer == "list" || outer == "reversed") {
@@ -235,6 +240,7 @@ pub fn unnecessary_call_around_sorted(expr: &Expr, func: &Expr, args: &[Expr]) -
None
}
/// C414
pub fn unnecessary_double_cast_or_process(
expr: &Expr,
func: &Expr,
@@ -274,6 +280,7 @@ pub fn unnecessary_double_cast_or_process(
None
}
/// C415
pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
let first_arg = args.first()?;
let id = function_name(func)?;
@@ -309,6 +316,7 @@ pub fn unnecessary_subscript_reversal(expr: &Expr, func: &Expr, args: &[Expr]) -
None
}
/// C416
pub fn unnecessary_comprehension(
expr: &Expr,
elt: &Expr,
@@ -337,6 +345,7 @@ pub fn unnecessary_comprehension(
))
}
/// C417
pub fn unnecessary_map(expr: &Expr, func: &Expr, args: &[Expr]) -> Option<Check> {
fn new_check(kind: &str, expr: &Expr) -> Check {
Check::new(

298
src/flake8_quotes/checks.rs Normal file
View File

@@ -0,0 +1,298 @@
use rustpython_ast::Location;
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_quotes::settings::{Quote, Settings};
fn good_single(quote: &Quote) -> char {
match quote {
Quote::Single => '\'',
Quote::Double => '"',
}
}
fn bad_single(quote: &Quote) -> char {
match quote {
Quote::Double => '\'',
Quote::Single => '"',
}
}
fn good_multiline(quote: &Quote) -> &str {
match quote {
Quote::Single => "'''",
Quote::Double => "\"\"\"",
}
}
fn good_multiline_ending(quote: &Quote) -> &str {
match quote {
Quote::Single => "'\"\"\"",
Quote::Double => "\"'''",
}
}
fn good_docstring(quote: &Quote) -> &str {
match quote {
Quote::Single => "'''",
Quote::Double => "\"\"\"",
}
}
pub fn quotes(
locator: &SourceCodeLocator,
start: &Location,
end: &Location,
is_docstring: bool,
settings: &Settings,
) -> Option<Check> {
let text = locator.slice_source_code_range(&Range {
location: *start,
end_location: *end,
});
// Remove any prefixes (e.g., remove `u` from `u"foo"`).
let last_quote_char = text.chars().last().unwrap();
let first_quote_char = text.find(last_quote_char).unwrap();
let prefix = &text[..first_quote_char].to_lowercase();
let raw_text = &text[first_quote_char..];
// Determine if the string is multiline-based.
let is_multiline = if raw_text.len() >= 3 {
let mut chars = raw_text.chars();
let first = chars.next().unwrap();
let second = chars.next().unwrap();
let third = chars.next().unwrap();
first == second && second == third
} else {
false
};
if is_docstring {
if raw_text.contains(good_docstring(&settings.docstring_quotes)) {
return None;
}
return Some(Check::new(
CheckKind::BadQuotesDocstring(settings.docstring_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
} else if is_multiline {
// If our string is or contains a known good string, ignore it.
if raw_text.contains(good_multiline(&settings.multiline_quotes)) {
return None;
}
// If our string ends with a known good ending, then ignore it.
if raw_text.ends_with(good_multiline_ending(&settings.multiline_quotes)) {
return None;
}
return Some(Check::new(
CheckKind::BadQuotesMultilineString(settings.multiline_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
} else {
let string_contents = &raw_text[1..raw_text.len() - 1];
// If we're using the preferred quotation type, check for escapes.
if last_quote_char == good_single(&settings.inline_quotes) {
if !settings.avoid_escape || prefix.contains('r') {
return None;
}
if string_contents.contains(good_single(&settings.inline_quotes))
&& !string_contents.contains(bad_single(&settings.inline_quotes))
{
return Some(Check::new(
CheckKind::AvoidQuoteEscape,
Range {
location: *start,
end_location: *end,
},
));
}
return None;
}
// If we're not using the preferred type, only allow use to avoid escapes.
if !string_contents.contains(good_single(&settings.inline_quotes)) {
return Some(Check::new(
CheckKind::BadQuotesInlineString(settings.inline_quotes.clone()),
Range {
location: *start,
end_location: *end,
},
));
}
}
None
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::{flake8_quotes, linter, Settings};
use crate::{fs, noqa};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
}
#[test_case(Path::new("doubles.py"))]
#[test_case(Path::new("doubles_escaped.py"))]
#[test_case(Path::new("doubles_multiline_string.py"))]
#[test_case(Path::new("doubles_noqa.py"))]
#[test_case(Path::new("doubles_wrapped.py"))]
fn doubles(path: &Path) -> Result<()> {
let snapshot = format!("doubles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("singles.py"))]
#[test_case(Path::new("singles_escaped.py"))]
#[test_case(Path::new("singles_multiline_string.py"))]
#[test_case(Path::new("singles_noqa.py"))]
#[test_case(Path::new("singles_wrapped.py"))]
fn singles(path: &Path) -> Result<()> {
let snapshot = format!("singles_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn double_docstring(path: &Path) -> Result<()> {
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
#[test_case(Path::new("docstring_doubles.py"))]
#[test_case(Path::new("docstring_doubles_module_multiline.py"))]
#[test_case(Path::new("docstring_doubles_module_singleline.py"))]
#[test_case(Path::new("docstring_doubles_class.py"))]
#[test_case(Path::new("docstring_doubles_function.py"))]
#[test_case(Path::new("docstring_singles.py"))]
#[test_case(Path::new("docstring_singles_module_multiline.py"))]
#[test_case(Path::new("docstring_singles_module_singleline.py"))]
#[test_case(Path::new("docstring_singles_class.py"))]
#[test_case(Path::new("docstring_singles_function.py"))]
fn single_docstring(path: &Path) -> Result<()> {
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
&Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..Settings::for_rules(vec![
CheckCode::Q000,
CheckCode::Q001,
CheckCode::Q002,
CheckCode::Q003,
])
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,118 @@
//! Extract docstrings via tokenization.
//!
//! See: https://github.com/zheller/flake8-quotes/blob/ef0d9a90249a080e460b70ab62bf4b65e5aa5816/flake8_quotes/docstring_detection.py#L29
//!
//! TODO(charlie): Consolidate with the existing AST-based docstring extraction.
use rustpython_parser::lexer::Tok;
#[derive(Debug)]
enum State {
// Start of the module: first string gets marked as a docstring.
ExpectModuleDocstring,
// After seeing a class definition, we're waiting for the block colon (and do bracket counting).
ExpectClassColon,
// After seeing the block colon in a class definition, we expect a docstring.
ExpectClassDocstring,
// Same as ExpectClassColon, but for function definitions.
ExpectFunctionColon,
// Same as ExpectClassDocstring, but for function definitions.
ExpectFunctionDocstring,
// Skip tokens until we observe a `class` or `def`.
Other,
}
pub struct StateMachine {
state: State,
bracket_count: usize,
}
impl StateMachine {
pub fn new() -> Self {
Self {
state: State::ExpectModuleDocstring,
bracket_count: 0,
}
}
pub fn consume(&mut self, tok: &Tok) -> bool {
if matches!(tok, Tok::Newline | Tok::Indent | Tok::Dedent) {
return false;
}
if matches!(tok, Tok::String { .. }) {
return if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
true
} else {
false
};
}
if matches!(tok, Tok::Class) {
self.state = State::ExpectClassColon;
self.bracket_count = 0;
return false;
}
if matches!(tok, Tok::Def) {
self.state = State::ExpectFunctionColon;
self.bracket_count = 0;
return false;
}
if matches!(tok, Tok::Colon) {
if self.bracket_count == 0 {
if matches!(self.state, State::ExpectClassColon) {
self.state = State::ExpectClassDocstring;
} else if matches!(self.state, State::ExpectFunctionColon) {
self.state = State::ExpectFunctionDocstring;
}
}
return false;
}
if matches!(tok, Tok::Lpar | Tok::Lbrace | Tok::Lsqb) {
self.bracket_count += 1;
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
}
return false;
}
if matches!(tok, Tok::Rpar | Tok::Rbrace | Tok::Rsqb) {
self.bracket_count -= 1;
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
}
return false;
}
if matches!(
self.state,
State::ExpectModuleDocstring
| State::ExpectClassDocstring
| State::ExpectFunctionDocstring
) {
self.state = State::Other;
return false;
}
false
}
}

3
src/flake8_quotes/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod checks;
pub mod docstring_detection;
pub mod settings;

View File

@@ -0,0 +1,49 @@
//! Settings for the `flake_quotes` plugin.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Quote {
Single,
Double,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub inline_quotes: Option<Quote>,
pub multiline_quotes: Option<Quote>,
pub docstring_quotes: Option<Quote>,
pub avoid_escape: Option<bool>,
}
#[derive(Debug)]
pub struct Settings {
pub inline_quotes: Quote,
pub multiline_quotes: Quote,
pub docstring_quotes: Quote,
pub avoid_escape: bool,
}
impl Settings {
pub fn from_config(config: Options) -> Self {
Self {
inline_quotes: config.inline_quotes.unwrap_or(Quote::Single),
multiline_quotes: config.multiline_quotes.unwrap_or(Quote::Double),
docstring_quotes: config.docstring_quotes.unwrap_or(Quote::Double),
avoid_escape: config.avoid_escape.unwrap_or(true),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
}
}
}

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 1
end_location:
row: 7
column: 4
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 16
column: 5
end_location:
row: 18
column: 8
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 21
end_location:
row: 22
column: 38
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 30
column: 9
end_location:
row: 32
column: 12
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 35
column: 13
end_location:
row: 37
column: 16
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 5
end_location:
row: 3
column: 28
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 5
column: 23
end_location:
row: 5
column: 44
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 3
column: 5
end_location:
row: 3
column: 27
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 11
column: 5
end_location:
row: 11
column: 27
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 15
column: 39
end_location:
row: 17
column: 4
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 17
column: 5
end_location:
row: 17
column: 20
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 21
column: 5
end_location:
row: 21
column: 28
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 4
column: 1
end_location:
row: 6
column: 4
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 9
column: 1
end_location:
row: 11
column: 4
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 2
column: 1
end_location:
row: 2
column: 32
fix: ~
- kind:
BadQuotesMultilineString: single
location:
row: 6
column: 1
end_location:
row: 6
column: 32
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 1
end_location:
row: 3
column: 4
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 14
column: 5
end_location:
row: 16
column: 8
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 26
column: 9
end_location:
row: 28
column: 12
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 2
column: 5
end_location:
row: 2
column: 54
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 6
column: 9
end_location:
row: 6
column: 58
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 9
column: 29
end_location:
row: 9
column: 53
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 2
column: 5
end_location:
row: 2
column: 57
fix: ~
- kind:
BadQuotesDocstring: double
location:
row: 8
column: 5
end_location:
row: 10
column: 8
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 1
end_location:
row: 3
column: 4
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: double
location:
row: 1
column: 1
end_location:
row: 1
column: 50
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesInlineString: single
location:
row: 1
column: 25
end_location:
row: 1
column: 46
fix: ~
- kind:
BadQuotesInlineString: single
location:
row: 2
column: 25
end_location:
row: 2
column: 47
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind: AvoidQuoteEscape
location:
row: 1
column: 26
end_location:
row: 1
column: 48
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: single
location:
row: 1
column: 5
end_location:
row: 3
column: 13
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
[]

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 1
end_location:
row: 3
column: 4
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 12
column: 5
end_location:
row: 14
column: 8
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 24
column: 9
end_location:
row: 26
column: 12
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 2
column: 5
end_location:
row: 2
column: 54
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 6
column: 9
end_location:
row: 6
column: 58
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 9
column: 29
end_location:
row: 9
column: 53
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 2
column: 5
end_location:
row: 2
column: 57
fix: ~
- kind:
BadQuotesDocstring: single
location:
row: 8
column: 5
end_location:
row: 10
column: 8
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 1
end_location:
row: 3
column: 4
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesDocstring: single
location:
row: 1
column: 1
end_location:
row: 1
column: 50
fix: ~

View File

@@ -0,0 +1,59 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 5
column: 1
end_location:
row: 7
column: 4
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 11
column: 21
end_location:
row: 13
column: 4
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 18
column: 5
end_location:
row: 20
column: 8
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 23
column: 21
end_location:
row: 24
column: 38
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 32
column: 9
end_location:
row: 34
column: 12
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 37
column: 13
end_location:
row: 39
column: 16
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 3
column: 5
end_location:
row: 3
column: 28
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 5
column: 23
end_location:
row: 5
column: 44
fix: ~

View File

@@ -0,0 +1,50 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 3
column: 5
end_location:
row: 3
column: 27
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 11
column: 5
end_location:
row: 11
column: 27
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 15
column: 39
end_location:
row: 17
column: 4
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 17
column: 5
end_location:
row: 17
column: 20
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 21
column: 5
end_location:
row: 21
column: 28
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 4
column: 1
end_location:
row: 6
column: 4
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 9
column: 1
end_location:
row: 11
column: 4
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 2
column: 1
end_location:
row: 2
column: 32
fix: ~
- kind:
BadQuotesMultilineString: double
location:
row: 6
column: 1
end_location:
row: 6
column: 32
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesInlineString: double
location:
row: 1
column: 25
end_location:
row: 1
column: 46
fix: ~
- kind:
BadQuotesInlineString: double
location:
row: 2
column: 25
end_location:
row: 2
column: 47
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind: AvoidQuoteEscape
location:
row: 1
column: 26
end_location:
row: 1
column: 48
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
- kind:
BadQuotesMultilineString: double
location:
row: 1
column: 5
end_location:
row: 3
column: 13
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/flake8_quotes/checks.rs
expression: checks
---
[]

View File

@@ -12,7 +12,7 @@ use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode;
use crate::settings::{FilePattern, PerFileIgnore};
use crate::settings::types::{FilePattern, PerFileIgnore};
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@@ -133,7 +133,7 @@ pub fn ignores_from_path<'a>(
[&pattern_code_pair.pattern].into_iter(),
)
})
.map(|pattern_code_pair| &pattern_code_pair.code)
.flat_map(|pattern_code_pair| &pattern_code_pair.codes)
.collect())
}
@@ -178,7 +178,7 @@ mod tests {
use path_absolutize::Absolutize;
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::FilePattern;
use crate::settings::types::FilePattern;
#[test]
fn inclusions() {

View File

@@ -6,24 +6,31 @@ use anyhow::Result;
use log::debug;
use rustpython_parser::lexer::LexResult;
use settings::pyproject;
use settings::Settings;
use crate::autofix::fixer::Mode;
use crate::linter::{check_path, tokenize};
use crate::message::Message;
use crate::settings::{RawSettings, Settings};
use crate::settings::configuration::Configuration;
mod ast;
mod autofix;
pub mod cache;
pub mod check_ast;
mod check_lines;
mod check_tokens;
pub mod checks;
mod checks_gen;
pub mod cli;
pub mod code_gen;
mod cst;
mod docstrings;
mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_print;
mod flake8_quotes;
pub mod fs;
pub mod linter;
pub mod logging;
@@ -34,14 +41,13 @@ pub mod printer;
mod pycodestyle;
mod pydocstyle;
mod pyflakes;
pub mod pyproject;
mod python;
mod pyupgrade;
pub mod settings;
pub mod visibility;
/// Run ruff over Python source code directly.
pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
pub fn check(path: &Path, contents: &str, quiet: bool) -> Result<Vec<Message>> {
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
match &project_root {
@@ -54,7 +60,11 @@ pub fn check(path: &Path, contents: &str) -> Result<Vec<Message>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_raw(RawSettings::from_pyproject(&pyproject, &project_root)?);
let settings = Settings::from_configuration(Configuration::from_pyproject(
&pyproject,
&project_root,
quiet,
)?);
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -14,6 +14,7 @@ use crate::autofix::fixer;
use crate::autofix::fixer::fix_file;
use crate::check_ast::check_ast;
use crate::check_lines::check_lines;
use crate::check_tokens::check_tokens;
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::code_gen::SourceGenerator;
use crate::message::Message;
@@ -45,6 +46,15 @@ pub(crate) fn check_path(
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// Run the token-based checks.
if settings
.enabled
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::Tokens))
{
check_tokens(&mut checks, contents, &tokens, settings);
}
// Run the AST-based checks.
if settings
.enabled
@@ -226,16 +236,12 @@ mod tests {
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::linter;
use crate::linter::tokenize;
use crate::settings;
use crate::{fs, noqa};
use crate::{linter, Settings};
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
@@ -330,6 +336,7 @@ mod tests {
#[test_case(CheckCode::F401, Path::new("F401_2.py"); "F401_2")]
#[test_case(CheckCode::F401, Path::new("F401_3.py"); "F401_3")]
#[test_case(CheckCode::F401, Path::new("F401_4.py"); "F401_4")]
#[test_case(CheckCode::F401, Path::new("F401_5.py"); "F401_5")]
#[test_case(CheckCode::F402, Path::new("F402.py"); "F402")]
#[test_case(CheckCode::F403, Path::new("F403.py"); "F403")]
#[test_case(CheckCode::F404, Path::new("F404.py"); "F404")]
@@ -380,6 +387,7 @@ mod tests {
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]
#[test_case(CheckCode::W605, Path::new("W605.py"); "W605")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = check_path(

View File

@@ -25,10 +25,11 @@ use ruff::linter::{lint_path, lint_stdin};
use ruff::logging::set_up_logging;
use ruff::message::Message;
use ruff::printer::{Printer, SerializationFormat};
use ruff::pyproject::{self};
use ruff::settings::CurrentSettings;
use ruff::settings::RawSettings;
use ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ruff::settings::configuration::Configuration;
use ruff::settings::pyproject;
use ruff::settings::types::{FilePattern, PerFileIgnore};
use ruff::settings::user::UserConfiguration;
use ruff::settings::Settings;
use ruff::tell_user;
#[cfg(feature = "update-informer")]
@@ -73,10 +74,14 @@ fn check_for_updates() {
}
}
fn show_settings(settings: RawSettings, project_root: Option<PathBuf>, pyproject: Option<PathBuf>) {
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) {
println!(
"{:#?}",
CurrentSettings::from_settings(settings, project_root, pyproject)
UserConfiguration::from_configuration(configuration, project_root, pyproject)
);
}
@@ -220,7 +225,8 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> Result<usize> {
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
let mut cli = Cli::parse();
cli.quiet |= cli.silent;
set_up_logging(cli.verbose)?;
@@ -255,15 +261,15 @@ fn inner_main() -> Result<ExitCode> {
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = RawSettings::from_pyproject(&pyproject, &project_root)?;
let mut configuration = Configuration::from_pyproject(&pyproject, &project_root, cli.quiet)?;
if !exclude.is_empty() {
settings.exclude = exclude;
configuration.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
configuration.extend_exclude = extend_exclude;
}
if !per_file_ignores.is_empty() {
settings.per_file_ignores = per_file_ignores;
configuration.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
warn_on(
@@ -271,10 +277,10 @@ fn inner_main() -> Result<ExitCode> {
&cli.select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&configuration,
&pyproject,
);
settings.select = cli.select;
configuration.select = cli.select;
}
if !cli.extend_select.is_empty() {
warn_on(
@@ -282,22 +288,22 @@ fn inner_main() -> Result<ExitCode> {
&cli.extend_select,
&cli.ignore,
&cli.extend_ignore,
&settings,
&configuration,
&pyproject,
);
settings.extend_select = cli.extend_select;
configuration.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
settings.ignore = cli.ignore;
configuration.ignore = cli.ignore;
}
if !cli.extend_ignore.is_empty() {
settings.extend_ignore = cli.extend_ignore;
configuration.extend_ignore = cli.extend_ignore;
}
if let Some(target_version) = cli.target_version {
settings.target_version = target_version;
configuration.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
configuration.dummy_variable_rgx = dummy_variable_rgx;
}
if cli.show_settings && cli.show_files {
@@ -305,11 +311,11 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(settings, project_root, pyproject);
show_settings(configuration, project_root, pyproject);
return Ok(ExitCode::SUCCESS);
}
let settings = Settings::from_raw(settings);
let settings = Settings::from_configuration(configuration);
if cli.show_files {
show_files(&cli.files, &settings);
@@ -342,7 +348,7 @@ fn inner_main() -> Result<ExitCode> {
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
if !cli.silent {
printer.write_continuously(&messages)?;
}
@@ -362,7 +368,7 @@ fn inner_main() -> Result<ExitCode> {
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
if !cli.silent {
printer.write_continuously(&messages)?;
}
}
@@ -388,13 +394,13 @@ fn inner_main() -> Result<ExitCode> {
let path = Path::new(&filename);
(
run_once_stdin(&settings, path, cli.fix)?,
!cli.quiet && !cli.fix,
!cli.silent && !cli.fix,
false,
)
} else {
(
run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?,
!cli.quiet,
!cli.silent,
!cli.quiet,
)
};

View File

@@ -169,7 +169,6 @@ y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
@@ -179,7 +178,6 @@ y = 2
z = x + 1",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
@@ -189,7 +187,6 @@ z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
@@ -200,7 +197,6 @@ z = x + 1
",
)
.collect();
println!("{:?}", extract_noqa_line_for(&lxr));
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(

View File

@@ -1,6 +1,8 @@
use itertools::izip;
use rustpython_ast::Location;
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind, Unaryop};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{CheckLocator, Range};
use crate::checks::{Check, CheckKind, RejectedCmpop};
@@ -8,7 +10,7 @@ fn is_ambiguous_name(name: &str) -> bool {
name == "l" || name == "I" || name == "O"
}
/// Check AmbiguousVariableName compliance.
/// E741
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -20,7 +22,7 @@ pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check AmbiguousClassName compliance.
/// E742
pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -32,7 +34,7 @@ pub fn ambiguous_class_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check AmbiguousFunctionName compliance.
/// E743
pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
@@ -44,7 +46,7 @@ pub fn ambiguous_function_name(name: &str, location: Range) -> Option<Check> {
}
}
/// Check DoNotAssignLambda compliance.
/// E731
pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Lambda { .. } = &value.node {
Some(Check::new(CheckKind::DoNotAssignLambda, location))
@@ -53,7 +55,7 @@ pub fn do_not_assign_lambda(value: &Expr, location: Range) -> Option<Check> {
}
}
/// Check NotInTest and NotIsTest compliance.
/// E713, E714
pub fn not_tests(
op: &Unaryop,
operand: &Expr,
@@ -92,7 +94,7 @@ pub fn not_tests(
checks
}
/// Check TrueFalseComparison and NoneComparison compliance.
/// E711, E712
pub fn literal_comparisons(
left: &Expr,
ops: &[Cmpop],
@@ -201,7 +203,7 @@ pub fn literal_comparisons(
checks
}
/// Check TypeComparison compliance.
/// E721
pub fn type_comparison(ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -236,3 +238,92 @@ pub fn type_comparison(ops: &[Cmpop], comparators: &[Expr], location: Range) ->
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',
'7', 'x', // Escape sequences only recognized in string literals
'N', 'u', 'U',
];
/// Return the quotation markers used for a String token.
fn extract_quote(text: &str) -> &str {
for quote in ["'''", "\"\"\"", "'", "\""] {
if text.ends_with(quote) {
return quote;
}
}
panic!("Unable to find quotation mark for String token.")
}
/// W605
pub fn invalid_escape_sequence(
locator: &SourceCodeLocator,
start: &Location,
end: &Location,
) -> Vec<Check> {
let mut checks = vec![];
let text = locator.slice_source_code_range(&Range {
location: *start,
end_location: *end,
});
// Determine whether the string is single- or triple-quoted.
let quote = extract_quote(text);
let quote_pos = text.find(quote).unwrap();
let prefix = text[..quote_pos].to_lowercase();
let body = &text[(quote_pos + quote.len())..(text.len() - quote.len())];
if !prefix.contains('r') {
let mut col_offset = 0;
let mut row_offset = 0;
let mut in_escape = false;
let mut chars = body.chars();
let mut current = chars.next();
let mut next = chars.next();
while let (Some(current_char), Some(next_char)) = (current, next) {
// If we see an escaped backslash, avoid treating the character _after_ the
// escaped backslash as itself an escaped character.
if in_escape {
in_escape = false;
} else {
in_escape = current_char == '\\' && next_char == '\\';
if current_char == '\\' && !VALID_ESCAPE_SEQUENCES.contains(&next_char) {
// Compute the location of the escape sequence by offsetting the location of the
// string token by the characters we've seen thus far.
let location = if row_offset == 0 {
Location::new(
start.row() + row_offset,
start.column() + prefix.len() + quote.len() + col_offset,
)
} else {
Location::new(start.row() + row_offset, col_offset + 1)
};
let end_location = Location::new(location.row(), location.column() + 1);
checks.push(Check::new(
CheckKind::InvalidEscapeSequence(next_char),
Range {
location,
end_location,
},
))
}
}
// Track the offset from the start position as we iterate over the body.
if current_char == '\n' {
col_offset = 0;
row_offset += 1;
} else {
col_offset += 1;
}
current = next;
next = chars.next();
}
}
checks
}

View File

@@ -10,7 +10,7 @@ use rustpython_parser::ast::{
use crate::ast::types::{BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
/// Check IfTuple compliance.
/// F634
pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
@@ -20,7 +20,7 @@ pub fn if_tuple(test: &Expr, location: Range) -> Option<Check> {
None
}
/// Check AssertTuple compliance.
/// F631
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
if let ExprKind::Tuple { elts, .. } = &test.node {
if !elts.is_empty() {
@@ -30,7 +30,7 @@ pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
None
}
/// Check UnusedVariable compliance.
/// F841
pub fn unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
@@ -63,7 +63,7 @@ pub fn unused_variables(
checks
}
/// Check DefaultExceptNotLast compliance.
/// F707
pub fn default_except_not_last(handlers: &[Excepthandler]) -> Option<Check> {
for (idx, handler) in handlers.iter().enumerate() {
let ExcepthandlerKind::ExceptHandler { type_, .. } = &handler.node;
@@ -78,7 +78,7 @@ pub fn default_except_not_last(handlers: &[Excepthandler]) -> Option<Check> {
None
}
/// Check RaiseNotImplemented compliance.
/// F901
pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
match &expr.node {
ExprKind::Call { func, .. } => {
@@ -105,7 +105,7 @@ pub fn raise_not_implemented(expr: &Expr) -> Option<Check> {
None
}
/// Check DuplicateArgumentName compliance.
/// F831
pub fn duplicate_arguments(arguments: &Arguments) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -153,7 +153,7 @@ fn convert_to_value(expr: &Expr) -> Option<DictionaryKey> {
}
}
/// Check MultiValueRepeatedKeyLiteral and MultiValueRepeatedKeyVariable compliance.
/// F601, F602
pub fn repeated_keys(
keys: &[Expr],
check_repeated_literals: bool,
@@ -215,7 +215,7 @@ fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// Check IsLiteral compliance.
/// F632
pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
@@ -232,7 +232,7 @@ pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Ra
checks
}
/// Check TwoStarredExpressions and TooManyExpressionsInStarredAssignment compliance.
/// F621, F622
pub fn starred_expressions(
elts: &[Expr],
check_too_many_expressions: bool,
@@ -262,7 +262,7 @@ pub fn starred_expressions(
None
}
/// Check BreakOutsideLoop compliance.
/// F701
pub fn break_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],
@@ -303,7 +303,7 @@ pub fn break_outside_loop(
}
}
/// Check ContinueOutsideLoop compliance.
/// F702
pub fn continue_outside_loop(
stmt: &Stmt,
parents: &[&Stmt],

View File

@@ -1,11 +1,11 @@
use libcst_native::ImportNames::Aliases;
use libcst_native::NameOrAttribute::N;
use libcst_native::{Codegen, SmallStatement, Statement};
use anyhow::Result;
use libcst_native::{Codegen, ImportNames, NameOrAttribute, SmallStatement, Statement};
use rustpython_ast::Stmt;
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::Range;
use crate::autofix::{helpers, Fix};
use crate::cst::helpers::compose_module_path;
/// Generate a Fix to remove any unused imports from an `import` statement.
pub fn remove_unused_imports(
@@ -14,7 +14,7 @@ pub fn remove_unused_imports(
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
) -> anyhow::Result<Fix> {
) -> Result<Fix> {
let mut tree = match libcst_native::parse_module(
locator.slice_source_code_range(&Range::from_located(stmt)),
None,
@@ -40,13 +40,11 @@ pub fn remove_unused_imports(
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
// Identify unused imports from within the `import from`.
// Identify unused imports from within the `import`.
let mut removable = vec![];
for (index, alias) in aliases.iter().enumerate() {
if let N(import_name) = &alias.name {
if full_names.contains(&import_name.value) {
removable.push(index);
}
if full_names.contains(&compose_module_path(&alias.name).as_str()) {
removable.push(index);
}
}
// TODO(charlie): This is quadratic.
@@ -79,7 +77,7 @@ pub fn remove_unused_import_froms(
stmt: &Stmt,
parent: Option<&Stmt>,
deleted: &[&Stmt],
) -> anyhow::Result<Fix> {
) -> Result<Fix> {
let mut tree = match libcst_native::parse_module(
locator.slice_source_code_range(&Range::from_located(stmt)),
None,
@@ -100,7 +98,8 @@ pub fn remove_unused_import_froms(
"Expected node to be: SmallStatement::ImportFrom."
));
};
let aliases = if let Aliases(aliases) = &mut body.names {
let aliases = if let ImportNames::Aliases(aliases) = &mut body.names {
aliases
} else {
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
@@ -112,13 +111,16 @@ pub fn remove_unused_import_froms(
// Identify unused imports from within the `import from`.
let mut removable = vec![];
for (index, alias) in aliases.iter().enumerate() {
if let N(name) = &alias.name {
let import_name = if let Some(N(module_name)) = &body.module {
format!("{}.{}", module_name.value, name.value)
} else {
name.value.to_string()
};
if full_names.contains(&import_name.as_str()) {
if let NameOrAttribute::N(name) = &alias.name {
let import_name = name.value.to_string();
let full_name = body
.module
.as_ref()
.map(compose_module_path)
.map(|module_name| format!("{module_name}.{import_name}"))
.unwrap_or(import_name);
if full_names.contains(&full_name.as_str()) {
removable.push(index);
}
}

View File

@@ -5,7 +5,7 @@ use crate::ast::types::{Binding, BindingKind, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pyupgrade::types::Primitive;
/// Check that `super()` has no args
/// U008
pub fn super_args(
scope: &Scope,
parents: &[&Stmt],
@@ -70,7 +70,7 @@ pub fn super_args(
None
}
/// Check UselessMetaclassType compliance.
/// U001
pub fn useless_metaclass_type(targets: &[Expr], value: &Expr, location: Range) -> Option<Check> {
if targets.len() == 1 {
if let ExprKind::Name { id, .. } = targets.first().map(|expr| &expr.node).unwrap() {
@@ -86,7 +86,7 @@ pub fn useless_metaclass_type(targets: &[Expr], value: &Expr, location: Range) -
None
}
/// Check UnnecessaryAbspath compliance.
/// U002
pub fn unnecessary_abspath(func: &Expr, args: &[Expr], location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {
@@ -106,7 +106,7 @@ pub fn unnecessary_abspath(func: &Expr, args: &[Expr], location: Range) -> Optio
None
}
/// Check UselessObjectInheritance compliance.
/// U004
pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) -> Option<Check> {
for expr in bases {
if let ExprKind::Name { id, .. } = &expr.node {
@@ -131,7 +131,7 @@ pub fn useless_object_inheritance(name: &str, bases: &[Expr], scope: &Scope) ->
None
}
/// Check TypeOfPrimitive compliance.
/// U003
pub fn type_of_primitive(func: &Expr, args: &[Expr], location: Range) -> Option<Check> {
// Validate the arguments.
if args.len() == 1 {

View File

@@ -1,320 +0,0 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::checks::{CheckCategory, CheckCode};
use crate::fs;
use crate::pyproject::{load_config, StrCheckCodePair};
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub code: CheckCode,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let code = user_in.code;
Self { pattern, code }
}
}
#[derive(Debug)]
pub struct RawSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCode>,
pub extend_select: Vec<CheckCode>,
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl RawSettings {
pub fn from_pyproject(
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
) -> Result<Self> {
let config = load_config(pyproject)?;
Ok(RawSettings {
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: config.target_version.unwrap_or(PythonVersion::Py310),
exclude: config
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: config
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: config.extend_ignore,
select: config.select.unwrap_or_else(|| {
CheckCode::iter()
.filter(|code| {
matches!(
code.category(),
CheckCategory::Pycodestyle | CheckCategory::Pyflakes
)
})
.collect()
}),
extend_select: config.extend_select,
ignore: config.ignore,
line_length: config.line_length.unwrap_or(88),
per_file_ignores: config
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
})
}
}
#[derive(Debug)]
pub struct Settings {
pub dummy_variable_rgx: Regex,
pub enabled: BTreeSet<CheckCode>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
}
impl Settings {
pub fn from_raw(settings: RawSettings) -> Self {
// Materialize the set of enabled CheckCodes.
let mut enabled: BTreeSet<CheckCode> = BTreeSet::new();
enabled.extend(settings.select);
enabled.extend(settings.extend_select);
for code in &settings.ignore {
enabled.remove(code);
}
for code in &settings.extend_ignore {
enabled.remove(code);
}
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
enabled,
exclude: settings.exclude,
extend_exclude: settings.extend_exclude,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
target_version: PythonVersion::Py310,
}
}
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
enabled: BTreeSet::from([check_code]),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
enabled: BTreeSet::from_iter(check_codes),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.enabled.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing Settings.
#[derive(Debug)]
pub struct CurrentSettings {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCode>,
pub extend_select: Vec<CheckCode>,
pub ignore: Vec<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
}
impl CurrentSettings {
pub fn from_settings(
settings: RawSettings,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_ignore: settings.extend_ignore,
extend_select: settings.extend_select,
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
select: settings.select,
target_version: settings.target_version,
project_root,
pyproject,
}
}
}

View File

@@ -0,0 +1,105 @@
//! User-provided program settings, taking into account pyproject.toml and command-line options.
//! Structure mirrors the user-facing representation of the various parameters.
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
#[derive(Debug)]
pub struct Configuration {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Configuration {
pub fn from_pyproject(
pyproject: &Option<PathBuf>,
project_root: &Option<PathBuf>,
quiet: bool,
) -> Result<Self> {
let options = load_options(pyproject, quiet)?;
Ok(Configuration {
dummy_variable_rgx: match options.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
extend_exclude: options
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect(),
extend_ignore: options.extend_ignore,
select: options
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
extend_select: options.extend_select,
ignore: options.ignore,
line_length: options.line_length.unwrap_or(88),
per_file_ignores: options
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
// Plugins
flake8_quotes: options
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_config)
.unwrap_or_default(),
})
}
}

161
src/settings/mod.rs Normal file
View File

@@ -0,0 +1,161 @@
//! Effective program settings, taking into account pyproject.toml and command-line options.
//! Structure is optimized for internal usage, as opposed to external visibility or parsing.
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::flake8_quotes;
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
pub mod configuration;
pub mod options;
pub mod pyproject;
pub mod types;
pub mod user;
#[derive(Debug)]
pub struct Settings {
pub dummy_variable_rgx: Regex,
pub enabled: BTreeSet<CheckCode>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
impl Settings {
pub fn from_configuration(config: Configuration) -> Self {
Self {
dummy_variable_rgx: config.dummy_variable_rgx,
enabled: resolve_codes(
&config.select,
&config.extend_select,
&config.ignore,
&config.extend_ignore,
),
exclude: config.exclude,
extend_exclude: config.extend_exclude,
flake8_quotes: config.flake8_quotes,
line_length: config.line_length,
per_file_ignores: config.per_file_ignores,
target_version: config.target_version,
}
}
pub fn for_rule(check_code: CheckCode) -> Self {
Self {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: BTreeSet::from([check_code]),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
pub fn for_rules(check_codes: Vec<CheckCode>) -> Self {
Self {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: BTreeSet::from_iter(check_codes),
exclude: vec![],
extend_exclude: vec![],
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.enabled.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes.
fn resolve_codes(
select: &[CheckCodePrefix],
extend_select: &[CheckCodePrefix],
ignore: &[CheckCodePrefix],
extend_ignore: &[CheckCodePrefix],
) -> BTreeSet<CheckCode> {
let mut codes: BTreeSet<CheckCode> = BTreeSet::new();
for specificity in [
PrefixSpecificity::Category,
PrefixSpecificity::Hundreds,
PrefixSpecificity::Tens,
PrefixSpecificity::Explicit,
] {
for prefix in select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in extend_select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
for prefix in ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
for prefix in extend_ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
}
codes
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::resolve_codes;
#[test]
fn resolver() {
let actual = resolve_codes(&[CheckCodePrefix::W], &[], &[], &[]);
let expected = BTreeSet::from_iter([CheckCode::W292, CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W6], &[], &[], &[]);
let expected = BTreeSet::from_iter([CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W], &[], &[CheckCodePrefix::W292], &[]);
let expected = BTreeSet::from_iter([CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W605], &[], &[CheckCodePrefix::W605], &[]);
let expected = BTreeSet::from_iter([]);
assert_eq!(actual, expected);
}
}

28
src/settings/options.rs Normal file
View File

@@ -0,0 +1,28 @@
//! Options that the user can provide via pyproject.toml.
use serde::Deserialize;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::settings::types::{PythonVersion, StrCheckCodePair};
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub line_length: Option<usize>,
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub extend_exclude: Vec<String>,
pub select: Option<Vec<CheckCodePrefix>>,
#[serde(default)]
pub extend_select: Vec<CheckCodePrefix>,
#[serde(default)]
pub ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub extend_ignore: Vec<CheckCodePrefix>,
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
}

View File

@@ -1,103 +1,26 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
//! Utilities for locating (and extracting configuration from) a pyproject.toml.
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use path_absolutize::Absolutize;
use serde::de;
use serde::{Deserialize, Deserializer};
use serde::Deserialize;
use crate::checks::CheckCode;
use crate::fs;
use crate::settings::PythonVersion;
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
Ok(Default::default())
}
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub extend_exclude: Vec<String>,
pub select: Option<Vec<CheckCode>>,
#[serde(default)]
pub extend_select: Vec<CheckCode>,
#[serde(default)]
pub ignore: Vec<CheckCode>,
#[serde(default)]
pub extend_ignore: Vec<CheckCode>,
#[serde(default)]
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCode,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0], tokens[1])
};
let code = CheckCode::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}
use crate::settings::options::Options;
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct Tools {
ruff: Option<Config>,
ruff: Option<Options>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct PyProject {
struct Pyproject {
tool: Option<Tools>,
}
fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(|e| e.into())
}
@@ -146,6 +69,22 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
None
}
pub fn load_options(pyproject: &Option<PathBuf>, quiet: bool) -> Result<Options> {
match pyproject {
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
if !quiet {
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
}
Ok(Default::default())
}
}
}
#[cfg(test)]
mod tests {
use std::env::current_dir;
@@ -154,26 +93,27 @@ mod tests {
use anyhow::Result;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_quotes;
use crate::flake8_quotes::settings::Quote;
use crate::settings::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools,
};
use super::StrCheckCodePair;
use crate::settings::types::StrCheckCodePair;
#[test]
fn deserialize() -> Result<()> {
let pyproject: PyProject = toml::from_str(r#""#)?;
let pyproject: Pyproject = toml::from_str(r#""#)?;
assert_eq!(pyproject.tool, None);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
"#,
)?;
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@@ -182,7 +122,7 @@ mod tests {
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
@@ -193,11 +133,12 @@ mod tests {
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@@ -207,7 +148,7 @@ line-length = 79
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: Some(79),
exclude: None,
extend_exclude: vec![],
@@ -218,11 +159,12 @@ line-length = 79
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@@ -232,7 +174,7 @@ exclude = ["foo.py"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: vec![],
@@ -243,11 +185,12 @@ exclude = ["foo.py"]
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@@ -257,22 +200,23 @@ select = ["E501"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
select: Some(vec![CheckCode::E501]),
select: Some(vec![CheckCodePrefix::E501]),
extend_select: vec![],
ignore: vec![],
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
let pyproject: PyProject = toml::from_str(
let pyproject: Pyproject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
@@ -283,22 +227,23 @@ ignore = ["E501"]
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
ruff: Some(Options {
line_length: None,
exclude: None,
extend_exclude: vec![],
select: None,
extend_select: vec![CheckCode::M001],
ignore: vec![CheckCode::E501],
extend_select: vec![CheckCodePrefix::M001],
ignore: vec![CheckCodePrefix::E501],
extend_ignore: vec![],
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@@ -307,7 +252,7 @@ line_length = 79
)
.is_err());
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@@ -316,7 +261,7 @@ select = ["E123"]
)
.is_err());
assert!(toml::from_str::<PyProject>(
assert!(toml::from_str::<Pyproject>(
r#"
[tool.black]
[tool.ruff]
@@ -348,7 +293,7 @@ other-attribute = 1
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
Config {
Options {
line_length: Some(88),
exclude: None,
extend_exclude: vec![
@@ -360,9 +305,18 @@ other-attribute = 1
extend_select: vec![],
ignore: vec![],
extend_ignore: vec![],
per_file_ignores: vec![],
per_file_ignores: vec![StrCheckCodePair {
pattern: "__init__.py".to_string(),
code: CheckCodePrefix::F401
}],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(Quote::Single),
multiline_quotes: Some(Quote::Double),
docstring_quotes: Some(Quote::Double),
avoid_escape: Some(true),
})
}
);
@@ -373,6 +327,8 @@ other-attribute = 1
fn str_check_code_pair_strings() {
let result = StrCheckCodePair::from_str("foo:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("foo: E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("E501:foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("E501");

125
src/settings/types.rs Normal file
View File

@@ -0,0 +1,125 @@
use std::collections::BTreeSet;
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use serde::{de, Deserialize, Deserializer, Serialize};
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::fs;
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
Py33,
Py34,
Py35,
Py36,
Py37,
Py38,
Py39,
Py310,
Py311,
}
impl FromStr for PythonVersion {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"py33" => Ok(PythonVersion::Py33),
"py34" => Ok(PythonVersion::Py34),
"py35" => Ok(PythonVersion::Py35),
"py36" => Ok(PythonVersion::Py36),
"py37" => Ok(PythonVersion::Py37),
"py38" => Ok(PythonVersion::Py38),
"py39" => Ok(PythonVersion::Py39),
"py310" => Ok(PythonVersion::Py310),
"py311" => Ok(PythonVersion::Py311),
_ => Err(anyhow!("Unknown version: {}", string)),
}
}
}
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub codes: BTreeSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let codes = BTreeSet::from_iter(user_in.code.codes());
Self { pattern, codes }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCodePrefix,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0].trim(), tokens[1].trim())
};
let code = CheckCodePrefix::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}

84
src/settings/user.rs Normal file
View File

@@ -0,0 +1,84 @@
//! Structs to render user-facing settings.
use std::path::PathBuf;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{flake8_quotes, Configuration};
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing configuration.
#[derive(Debug)]
pub struct UserConfiguration {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
// Non-settings exposed to the user
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
}
impl UserConfiguration {
pub fn from_configuration(
settings: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: settings.dummy_variable_rgx,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_ignore: settings.extend_ignore,
extend_select: settings.extend_select,
ignore: settings.ignore,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
select: settings.select,
target_version: settings.target_version,
flake8_quotes: settings.flake8_quotes,
project_root,
pyproject,
}
}
}

View File

@@ -4,7 +4,8 @@ expression: checks
---
- kind:
UnusedImport:
- functools
- - functools
- false
location:
row: 2
column: 1
@@ -23,7 +24,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- collections.OrderedDict
- - collections.OrderedDict
- false
location:
row: 4
column: 1
@@ -42,7 +44,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- logging.handlers
- - logging.handlers
- false
location:
row: 12
column: 1
@@ -51,17 +54,18 @@ expression: checks
column: 24
fix:
patch:
content: import logging.handlers
content: ""
location:
row: 12
column: 1
end_location:
row: 12
column: 24
row: 13
column: 1
applied: false
- kind:
UnusedImport:
- shelve
- - shelve
- false
location:
row: 33
column: 5
@@ -80,7 +84,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- importlib
- - importlib
- false
location:
row: 34
column: 5
@@ -99,7 +104,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- pathlib
- - pathlib
- false
location:
row: 38
column: 5
@@ -118,7 +124,8 @@ expression: checks
applied: false
- kind:
UnusedImport:
- pickle
- - pickle
- false
location:
row: 53
column: 9

View File

@@ -0,0 +1,85 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedImport:
- - a.b.c
- false
location:
row: 2
column: 1
end_location:
row: 2
column: 18
fix:
patch:
content: ""
location:
row: 2
column: 1
end_location:
row: 3
column: 1
applied: false
- kind:
UnusedImport:
- - d.e.f
- false
location:
row: 3
column: 1
end_location:
row: 3
column: 23
fix:
patch:
content: ""
location:
row: 3
column: 1
end_location:
row: 4
column: 1
applied: false
- kind:
UnusedImport:
- - h.i
- false
location:
row: 4
column: 1
end_location:
row: 4
column: 11
fix:
patch:
content: ""
location:
row: 4
column: 1
end_location:
row: 5
column: 1
applied: false
- kind:
UnusedImport:
- - j.k
- false
location:
row: 5
column: 1
end_location:
row: 5
column: 16
fix:
patch:
content: ""
location:
row: 5
column: 1
end_location:
row: 6
column: 1
applied: false

View File

@@ -0,0 +1,41 @@
---
source: src/linter.rs
expression: checks
---
- kind:
InvalidEscapeSequence: "."
location:
row: 2
column: 10
end_location:
row: 2
column: 11
fix: ~
- kind:
InvalidEscapeSequence: "."
location:
row: 6
column: 1
end_location:
row: 6
column: 2
fix: ~
- kind:
InvalidEscapeSequence: _
location:
row: 11
column: 6
end_location:
row: 11
column: 7
fix: ~
- kind:
InvalidEscapeSequence: _
location:
row: 18
column: 6
end_location:
row: 18
column: 7
fix: ~

View File

@@ -4,7 +4,8 @@ expression: checks
---
- kind:
UnusedImport:
- models.Nut
- - models.Nut
- false
location:
row: 5
column: 1