Implement the `flake8-quotes` plugin (#495)

This commit is contained in:
Charlie Marsh 2022-10-28 17:52:11 -04:00 committed by GitHub
parent a057c9a323
commit 86265c1d7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1524 additions and 7 deletions

View File

@ -104,7 +104,17 @@ per-file-ignores = [
]
```
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
@ -415,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 |
@ -486,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)
@ -506,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,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

@ -8,3 +8,9 @@ extend-exclude = [
per-file-ignores = [
"__init__.py:F401",
]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"
docstring-quotes = "double"
avoid-escape = true

View File

@ -4,7 +4,8 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::operations::SourceCodeLocator;
use crate::checks::{Check, CheckCode};
use crate::{pycodestyle, Settings};
use crate::flake8_quotes::docstring_detection::StateMachine;
use crate::{flake8_quotes, pycodestyle, Settings};
pub fn check_tokens(
checks: &mut Vec<Check>,
@ -12,10 +13,18 @@ pub fn check_tokens(
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 enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
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(
@ -23,5 +32,23 @@ pub fn check_tokens(
));
}
}
// 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(
@ -102,6 +103,11 @@ pub enum CheckCode {
// flake8-print
T201,
T203,
// flake8-quotes
Q000,
Q001,
Q002,
Q003,
// pyupgrade
U001,
U002,
@ -184,6 +190,7 @@ pub enum CheckCategory {
Flake8Bugbear,
Flake8Builtins,
Flake8Print,
Flake8Quotes,
Meta,
}
@ -197,6 +204,7 @@ impl CheckCategory {
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",
@ -299,6 +307,11 @@ pub enum CheckKind {
// flake8-print
PrintFound,
PPrintFound,
// flake8-quotes
BadQuotesInlineString(Quote),
BadQuotesMultilineString(Quote),
BadQuotesDocstring(Quote),
AvoidQuoteEscape,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@ -374,7 +387,11 @@ impl CheckCode {
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
CheckCode::W605 => &LintSource::Tokens,
CheckCode::W605
| CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
| CheckCode::Q003 => &LintSource::Tokens,
CheckCode::E902 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
@ -476,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,
@ -639,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,
@ -788,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,
@ -1101,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())

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

@ -0,0 +1,303 @@
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::settings;
use crate::{flake8_quotes, linter};
use crate::{fs, noqa};
fn check_path(
path: &Path,
settings: &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::Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..settings::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::Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..settings::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::Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Single,
docstring_quotes: Quote::Double,
avoid_escape: true,
},
..settings::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::Settings {
flake8_quotes: flake8_quotes::settings::Settings {
inline_quotes: Quote::Single,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Single,
avoid_escape: true,
},
..settings::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 Config {
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: Config) -> 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

@ -26,6 +26,7 @@ mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_print;
mod flake8_quotes;
pub mod fs;
pub mod linter;
pub mod logging;

View File

@ -8,8 +8,8 @@ use serde::de;
use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode;
use crate::fs;
use crate::settings::PythonVersion;
use crate::{flake8_quotes, fs};
pub fn load_config(pyproject: &Option<PathBuf>, quiet: bool) -> Result<Config> {
match pyproject {
@ -45,6 +45,7 @@ pub struct Config {
pub per_file_ignores: Vec<StrCheckCodePair>,
pub dummy_variable_rgx: Option<String>,
pub target_version: Option<PythonVersion>,
pub flake8_quotes: Option<flake8_quotes::settings::Config>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -195,6 +196,7 @@ mod tests {
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
@ -220,6 +222,7 @@ line-length = 79
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
@ -245,6 +248,7 @@ exclude = ["foo.py"]
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
@ -270,6 +274,7 @@ select = ["E501"]
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
@ -296,6 +301,7 @@ ignore = ["E501"]
per_file_ignores: vec![],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
})
})
);
@ -368,6 +374,7 @@ other-attribute = 1
}],
dummy_variable_rgx: None,
target_version: None,
flake8_quotes: None
}
);

View File

@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::checks::{CheckCategory, CheckCode};
use crate::fs;
use crate::pyproject::{load_config, StrCheckCodePair};
use crate::{flake8_quotes, fs};
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)]
pub enum PythonVersion {
@ -97,6 +97,8 @@ pub struct RawSettings {
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
@ -133,6 +135,7 @@ impl RawSettings {
quiet: bool,
) -> Result<Self> {
let config = load_config(pyproject, quiet)?;
println!("{:?}", config.flake8_quotes);
Ok(RawSettings {
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
@ -173,6 +176,11 @@ impl RawSettings {
.into_iter()
.map(|pair| PerFileIgnore::new(pair, project_root))
.collect(),
// Plugins
flake8_quotes: config
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_config)
.unwrap_or_default(),
})
}
}
@ -186,6 +194,8 @@ pub struct Settings {
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub target_version: PythonVersion,
// Plugins
pub flake8_quotes: flake8_quotes::settings::Settings,
}
impl Settings {
@ -205,9 +215,10 @@ impl Settings {
enabled,
exclude: settings.exclude,
extend_exclude: settings.extend_exclude,
flake8_quotes: settings.flake8_quotes,
line_length: settings.line_length,
per_file_ignores: settings.per_file_ignores,
target_version: PythonVersion::Py310,
target_version: settings.target_version,
}
}
@ -220,6 +231,7 @@ impl Settings {
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
@ -232,6 +244,7 @@ impl Settings {
line_length: 88,
per_file_ignores: vec![],
target_version: PythonVersion::Py310,
flake8_quotes: Default::default(),
}
}
}
@ -285,6 +298,9 @@ pub struct CurrentSettings {
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCode>,
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>,
}
@ -314,6 +330,7 @@ impl CurrentSettings {
per_file_ignores: settings.per_file_ignores,
select: settings.select,
target_version: settings.target_version,
flake8_quotes: settings.flake8_quotes,
project_root,
pyproject,
}