Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b60242fb7 | ||
|
|
65b77feeb8 | ||
|
|
04b9c0a31d | ||
|
|
49dc8231be | ||
|
|
92ca114882 | ||
|
|
553bc7443a | ||
|
|
a3af6c1ea5 | ||
|
|
b50016fe89 | ||
|
|
2cf2805848 | ||
|
|
33fbef7700 | ||
|
|
68668a584b | ||
|
|
6cd8655d29 | ||
|
|
72a9bd3cfb | ||
|
|
58aac21a36 | ||
|
|
77e0be3464 | ||
|
|
bd08fc359d | ||
|
|
19ad6ab4f5 | ||
|
|
4b2df99e78 |
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.135
|
||||
rev: v0.0.138
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -670,7 +670,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.135-dev.0"
|
||||
version = "0.0.138-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
@@ -769,10 +769,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
name = "globset"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr 0.2.17",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
@@ -1029,7 +1036,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
|
||||
dependencies = [
|
||||
"chic",
|
||||
"itertools",
|
||||
@@ -1044,7 +1051,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcst_derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -1768,7 +1775,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1787,7 +1794,7 @@ dependencies = [
|
||||
"fern",
|
||||
"filetime",
|
||||
"getrandom 0.2.8",
|
||||
"glob",
|
||||
"globset",
|
||||
"insta",
|
||||
"itertools",
|
||||
"libcst",
|
||||
@@ -1818,7 +1825,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.22",
|
||||
|
||||
@@ -6,8 +6,9 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[lib]
|
||||
name = "ruff"
|
||||
@@ -26,9 +27,9 @@ common-path = { version = "1.0.0" }
|
||||
dirs = { version = "4.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.17" }
|
||||
glob = { version = "0.3.0" }
|
||||
globset = {version = "0.4.9" }
|
||||
itertools = { version = "0.10.5" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
|
||||
log = { version = "0.4.17" }
|
||||
nohash-hasher = { version = "0.2.0" }
|
||||
notify = { version = "4.0.17" }
|
||||
|
||||
25
LICENSE
25
LICENSE
@@ -443,3 +443,28 @@ are:
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
- RustPython, licensed as follows:
|
||||
"""
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 RustPython Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
@@ -107,7 +107,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.135
|
||||
rev: v0.0.138
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -350,6 +350,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
|
||||
| F521 | StringDotFormatInvalidFormat | '...'.format(...) has invalid format string: ... | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
|
||||
@@ -574,6 +575,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
|
||||
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
|
||||
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
|
||||
| B904 | RaiseWithoutFromInsideExcept | Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling | |
|
||||
|
||||
### flake8-builtins
|
||||
|
||||
@@ -823,7 +825,7 @@ including:
|
||||
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (27/32)
|
||||
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
|
||||
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
|
||||
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
|
||||
@@ -854,7 +856,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
|
||||
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
|
||||
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (27/32)
|
||||
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
|
||||
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
|
||||
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
|
||||
|
||||
8
flake8_to_ruff/Cargo.lock
generated
8
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1265,7 +1265,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
|
||||
dependencies = [
|
||||
"chic",
|
||||
"itertools",
|
||||
@@ -1280,7 +1280,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "libcst_derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
|
||||
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.135-dev.0"
|
||||
version = "0.0.138-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
55
resources/test/fixtures/B904.py
vendored
Normal file
55
resources/test/fixtures/B904.py
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Should emit:
|
||||
B904 - on lines 10, 11 and 16
|
||||
"""
|
||||
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
if "abc":
|
||||
raise TypeError
|
||||
raise UserWarning
|
||||
except AssertionError:
|
||||
raise # Bare `raise` should not be an error
|
||||
except Exception as err:
|
||||
assert err
|
||||
raise Exception("No cause here...")
|
||||
except BaseException as base_err:
|
||||
# Might use this instead of bare raise with the `.with_traceback()` method
|
||||
raise base_err
|
||||
finally:
|
||||
raise Exception("Nothing to chain from, so no warning here")
|
||||
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
# should not emit, since we are not raising something
|
||||
def proxy():
|
||||
raise NameError
|
||||
|
||||
|
||||
try:
|
||||
from preferred_library import Thing
|
||||
except ImportError:
|
||||
try:
|
||||
from fallback_library import Thing
|
||||
except ImportError:
|
||||
|
||||
class Thing:
|
||||
def __getattr__(self, name):
|
||||
# same as the case above, should not emit.
|
||||
raise AttributeError
|
||||
|
||||
|
||||
try:
|
||||
from preferred_library import Thing
|
||||
except ImportError:
|
||||
try:
|
||||
from fallback_library import Thing
|
||||
except ImportError:
|
||||
|
||||
def context_switch():
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise
|
||||
29
resources/test/fixtures/F521.py
vendored
Normal file
29
resources/test/fixtures/F521.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"{".format(1)
|
||||
"}".format(1)
|
||||
"{foo[}".format(foo=1)
|
||||
# too much string recursion (placeholder-in-placeholder)
|
||||
"{:{:{}}}".format(1, 2, 3)
|
||||
# ruff picks these issues up, but flake8 doesn't
|
||||
"{foo[]}".format(foo={"": 1})
|
||||
"{foo..}".format(foo=1)
|
||||
"{foo..bar}".format(foo=1)
|
||||
|
||||
# "{} {1}".format(1, 2) # F525
|
||||
# "{0} {}".format(1, 2) # F525
|
||||
# "{}".format(1, 2) # F523
|
||||
# "{}".format(1, bar=2) # F522
|
||||
# "{} {}".format(1) # F524
|
||||
# "{2}".format() # F524
|
||||
# "{bar}".format() # F524
|
||||
|
||||
# The following are all "good" uses of .format
|
||||
"{.__class__}".format("")
|
||||
"{foo[bar]}".format(foo={"bar": "barv"})
|
||||
"{[bar]}".format({"bar": "barv"})
|
||||
"{:{}} {}".format(1, 15, 2)
|
||||
"{:2}".format(1)
|
||||
"{foo}-{}".format(1, foo=2)
|
||||
a = ()
|
||||
"{}".format(*a)
|
||||
k = {}
|
||||
"{foo}".format(**k)
|
||||
13
resources/test/fixtures/F841.py
vendored
13
resources/test/fixtures/F841.py
vendored
@@ -52,3 +52,16 @@ def f5():
|
||||
|
||||
def f7():
|
||||
nonlocal b
|
||||
|
||||
|
||||
def f6():
|
||||
annotations = []
|
||||
assert len([annotations for annotations in annotations])
|
||||
|
||||
|
||||
def f7():
|
||||
def connect():
|
||||
return None, None
|
||||
|
||||
with connect() as (connection, cursor):
|
||||
cursor.execute("SELECT * FROM users")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.135"
|
||||
version = "0.0.138"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -8,7 +8,7 @@ anyhow = { version = "1.0.66" }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
codegen = { version = "0.2.0" }
|
||||
itertools = { version = "0.10.5" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
|
||||
ruff = { path = ".." }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "f885db8c61514f069979861f6b3bd83292086231" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "f885db8c61514f069979861f6b3bd83292086231" }
|
||||
|
||||
@@ -19,8 +19,8 @@ pub trait Visitor<'a> {
|
||||
fn visit_constant(&mut self, constant: &'a Constant) {
|
||||
walk_constant(self, constant);
|
||||
}
|
||||
fn visit_expr_context(&mut self, expr_content: &'a ExprContext) {
|
||||
walk_expr_context(self, expr_content);
|
||||
fn visit_expr_context(&mut self, expr_context: &'a ExprContext) {
|
||||
walk_expr_context(self, expr_context);
|
||||
}
|
||||
fn visit_boolop(&mut self, boolop: &'a Boolop) {
|
||||
walk_boolop(self, boolop);
|
||||
@@ -456,8 +456,8 @@ pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
comprehension: &'a Comprehension,
|
||||
) {
|
||||
visitor.visit_expr(&comprehension.target);
|
||||
visitor.visit_expr(&comprehension.iter);
|
||||
visitor.visit_expr(&comprehension.target);
|
||||
for expr in &comprehension.ifs {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn leading_space(line: &str) -> String {
|
||||
}
|
||||
|
||||
/// Extract the leading indentation from a line.
|
||||
pub fn indentation<'a, T>(checker: &'a Checker, located: &Located<T>) -> String {
|
||||
pub fn indentation<T>(checker: &Checker, located: &Located<T>) -> String {
|
||||
let range = Range::from_located(located);
|
||||
checker
|
||||
.locator
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::Path;
|
||||
use itertools::Itertools;
|
||||
use log::error;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustpython_ast::Withitem;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
|
||||
KeywordData, Operator, Stmt, StmtKind, Suite,
|
||||
@@ -21,7 +22,7 @@ use crate::ast::types::{
|
||||
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Range, Scope,
|
||||
ScopeKind,
|
||||
};
|
||||
use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||
use crate::ast::visitor::{walk_excepthandler, walk_withitem, Visitor};
|
||||
use crate::ast::{helpers, operations, visitor};
|
||||
use crate::checks::{Check, CheckCode, CheckKind};
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
||||
@@ -77,6 +78,7 @@ pub struct Checker<'a> {
|
||||
in_deferred_string_annotation: bool,
|
||||
in_literal: bool,
|
||||
in_subscript: bool,
|
||||
in_withitem: bool,
|
||||
seen_import_boundary: bool,
|
||||
futures_allowed: bool,
|
||||
annotations_future_enabled: bool,
|
||||
@@ -120,6 +122,7 @@ impl<'a> Checker<'a> {
|
||||
in_deferred_string_annotation: false,
|
||||
in_literal: false,
|
||||
in_subscript: false,
|
||||
in_withitem: false,
|
||||
seen_import_boundary: false,
|
||||
futures_allowed: true,
|
||||
annotations_future_enabled: false,
|
||||
@@ -1240,6 +1243,28 @@ where
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
// pyflakes
|
||||
if let ExprKind::Attribute { value, attr, .. } = &func.node {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(value),
|
||||
..
|
||||
} = &value.node
|
||||
{
|
||||
if attr == "format" {
|
||||
// "...".format(...) call
|
||||
if self.settings.enabled.contains(&CheckCode::F521) {
|
||||
let location = Range::from_located(expr);
|
||||
if let Some(check) =
|
||||
pyflakes::checks::string_dot_format_invalid(value, location)
|
||||
{
|
||||
self.add_check(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pyupgrade
|
||||
if self.settings.enabled.contains(&CheckCode::U005) {
|
||||
pyupgrade::plugins::deprecated_unittest_alias(self, func);
|
||||
}
|
||||
@@ -1926,13 +1951,20 @@ where
|
||||
|
||||
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
|
||||
match &excepthandler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, name, .. } => {
|
||||
ExcepthandlerKind::ExceptHandler {
|
||||
type_, name, body, ..
|
||||
} => {
|
||||
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
|
||||
self.add_check(Check::new(
|
||||
CheckKind::DoNotUseBareExcept,
|
||||
Range::from_located(excepthandler),
|
||||
));
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::B904) {
|
||||
{
|
||||
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
|
||||
}
|
||||
}
|
||||
match name {
|
||||
Some(name) => {
|
||||
if self.settings.enabled.contains(&CheckCode::E741) {
|
||||
@@ -2080,6 +2112,13 @@ where
|
||||
|
||||
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
|
||||
}
|
||||
|
||||
fn visit_withitem(&mut self, withitem: &'b Withitem) {
|
||||
let prev_in_withitem = self.in_withitem;
|
||||
self.in_withitem = true;
|
||||
walk_withitem(self, withitem);
|
||||
self.in_withitem = prev_in_withitem;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> bool {
|
||||
@@ -2386,7 +2425,7 @@ impl<'a> Checker<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
if operations::is_unpacking_assignment(parent) {
|
||||
if self.in_withitem || operations::is_unpacking_assignment(parent) {
|
||||
self.add_binding(
|
||||
id,
|
||||
Binding {
|
||||
|
||||
@@ -52,6 +52,7 @@ pub enum CheckCode {
|
||||
F405,
|
||||
F406,
|
||||
F407,
|
||||
F521,
|
||||
F541,
|
||||
F601,
|
||||
F602,
|
||||
@@ -103,6 +104,7 @@ pub enum CheckCode {
|
||||
B025,
|
||||
B026,
|
||||
B027,
|
||||
B904,
|
||||
// flake8-blind-except
|
||||
BLE001,
|
||||
// flake8-comprehensions
|
||||
@@ -402,6 +404,7 @@ pub enum CheckKind {
|
||||
MultiValueRepeatedKeyVariable(String),
|
||||
RaiseNotImplemented,
|
||||
ReturnOutsideFunction,
|
||||
StringDotFormatInvalidFormat(String),
|
||||
TwoStarredExpressions,
|
||||
UndefinedExport(String),
|
||||
UndefinedLocal(String),
|
||||
@@ -441,6 +444,7 @@ pub enum CheckKind {
|
||||
DuplicateTryBlockException(String),
|
||||
StarArgUnpackingAfterKeywordArg,
|
||||
EmptyMethodWithoutAbstractDecorator(String),
|
||||
RaiseWithoutFromInsideExcept,
|
||||
// flake8-comprehensions
|
||||
UnnecessaryGeneratorList,
|
||||
UnnecessaryGeneratorSet,
|
||||
@@ -644,6 +648,7 @@ impl CheckCode {
|
||||
}
|
||||
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
|
||||
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
|
||||
CheckCode::F521 => CheckKind::StringDotFormatInvalidFormat("...".to_string()),
|
||||
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
|
||||
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
|
||||
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
|
||||
@@ -699,6 +704,7 @@ impl CheckCode {
|
||||
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
|
||||
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
|
||||
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
|
||||
CheckCode::B904 => CheckKind::RaiseWithoutFromInsideExcept,
|
||||
// flake8-comprehensions
|
||||
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
|
||||
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
|
||||
@@ -909,6 +915,7 @@ impl CheckCode {
|
||||
CheckCode::F405 => CheckCategory::Pyflakes,
|
||||
CheckCode::F406 => CheckCategory::Pyflakes,
|
||||
CheckCode::F407 => CheckCategory::Pyflakes,
|
||||
CheckCode::F521 => CheckCategory::Pyflakes,
|
||||
CheckCode::F541 => CheckCategory::Pyflakes,
|
||||
CheckCode::F601 => CheckCategory::Pyflakes,
|
||||
CheckCode::F602 => CheckCategory::Pyflakes,
|
||||
@@ -958,6 +965,7 @@ impl CheckCode {
|
||||
CheckCode::B025 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B026 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B027 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::B904 => CheckCategory::Flake8Bugbear,
|
||||
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
|
||||
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
|
||||
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
|
||||
@@ -1132,6 +1140,7 @@ impl CheckKind {
|
||||
CheckKind::NotIsTest => &CheckCode::E714,
|
||||
CheckKind::RaiseNotImplemented => &CheckCode::F901,
|
||||
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
|
||||
CheckKind::StringDotFormatInvalidFormat(_) => &CheckCode::F521,
|
||||
CheckKind::SyntaxError(_) => &CheckCode::E999,
|
||||
CheckKind::ExpressionsInStarAssignment => &CheckCode::F621,
|
||||
CheckKind::TrueFalseComparison(..) => &CheckCode::E712,
|
||||
@@ -1176,6 +1185,7 @@ impl CheckKind {
|
||||
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
|
||||
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
|
||||
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
|
||||
CheckKind::RaiseWithoutFromInsideExcept => &CheckCode::B904,
|
||||
// flake8-blind-except
|
||||
CheckKind::BlindExcept => &CheckCode::BLE001,
|
||||
// flake8-comprehensions
|
||||
@@ -1417,6 +1427,9 @@ impl CheckKind {
|
||||
CheckKind::ReturnOutsideFunction => {
|
||||
"`return` statement outside of a function/method".to_string()
|
||||
}
|
||||
CheckKind::StringDotFormatInvalidFormat(message) => {
|
||||
format!("'...'.format(...) has invalid format string: {message}")
|
||||
}
|
||||
CheckKind::SyntaxError(message) => format!("SyntaxError: {message}"),
|
||||
CheckKind::ExpressionsInStarAssignment => {
|
||||
"Too many expressions in star-unpacking assignment".to_string()
|
||||
@@ -1580,6 +1593,11 @@ impl CheckKind {
|
||||
decorator"
|
||||
)
|
||||
}
|
||||
CheckKind::RaiseWithoutFromInsideExcept => {
|
||||
"Within an except clause, raise exceptions with raise ... from err or raise ... \
|
||||
from None to distinguish them from errors in exception handling"
|
||||
.to_string()
|
||||
}
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => {
|
||||
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
|
||||
|
||||
@@ -63,6 +63,9 @@ pub enum CheckCodePrefix {
|
||||
B025,
|
||||
B026,
|
||||
B027,
|
||||
B9,
|
||||
B90,
|
||||
B904,
|
||||
BLE,
|
||||
BLE0,
|
||||
BLE00,
|
||||
@@ -416,6 +419,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::B025,
|
||||
CheckCode::B026,
|
||||
CheckCode::B027,
|
||||
CheckCode::B904,
|
||||
],
|
||||
CheckCodePrefix::B0 => vec![
|
||||
CheckCode::B002,
|
||||
@@ -500,6 +504,9 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B025 => vec![CheckCode::B025],
|
||||
CheckCodePrefix::B026 => vec![CheckCode::B026],
|
||||
CheckCodePrefix::B027 => vec![CheckCode::B027],
|
||||
CheckCodePrefix::B9 => vec![CheckCode::B904],
|
||||
CheckCodePrefix::B90 => vec![CheckCode::B904],
|
||||
CheckCodePrefix::B904 => vec![CheckCode::B904],
|
||||
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
|
||||
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
|
||||
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
|
||||
@@ -840,6 +847,7 @@ impl CheckCodePrefix {
|
||||
CheckCode::F405,
|
||||
CheckCode::F406,
|
||||
CheckCode::F407,
|
||||
CheckCode::F521,
|
||||
CheckCode::F541,
|
||||
CheckCode::F601,
|
||||
CheckCode::F602,
|
||||
@@ -1285,6 +1293,9 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::B9 => PrefixSpecificity::Hundreds,
|
||||
CheckCodePrefix::B90 => PrefixSpecificity::Tens,
|
||||
CheckCodePrefix::B904 => PrefixSpecificity::Explicit,
|
||||
CheckCodePrefix::BLE => PrefixSpecificity::Category,
|
||||
CheckCodePrefix::BLE0 => PrefixSpecificity::Hundreds,
|
||||
CheckCodePrefix::BLE00 => PrefixSpecificity::Tens,
|
||||
|
||||
22
src/cli.rs
22
src/cli.rs
@@ -1,14 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{command, Parser};
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::checks_gen::CheckCodePrefix;
|
||||
use crate::logging::LogLevel;
|
||||
use crate::printer::SerializationFormat;
|
||||
use crate::settings::types::{PatternPrefixPair, PerFileIgnore, PythonVersion};
|
||||
use crate::settings::types::{FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, about = "Ruff: An extremely fast Python linter.")]
|
||||
@@ -60,11 +60,11 @@ pub struct Cli {
|
||||
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>,
|
||||
pub exclude: Vec<FilePattern>,
|
||||
/// Like --exclude, but adds additional files and directories on top of the
|
||||
/// excluded ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_exclude: Vec<String>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
/// List of error codes to treat as eligible for autofix. Only applicable
|
||||
/// when autofix itself is enabled (e.g., via `--fix`).
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
@@ -77,7 +77,7 @@ pub struct Cli {
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub per_file_ignores: Vec<PatternPrefixPair>,
|
||||
/// Output serialization format for error messages.
|
||||
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
|
||||
#[arg(long, value_enum, default_value_t = SerializationFormat::Text)]
|
||||
pub format: SerializationFormat,
|
||||
/// Show violations with source code.
|
||||
#[arg(long)]
|
||||
@@ -111,6 +111,9 @@ pub struct Cli {
|
||||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long)]
|
||||
pub stdin_filename: Option<String>,
|
||||
/// Explain a rule.
|
||||
#[arg(long)]
|
||||
pub explain: Option<CheckCode>,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
@@ -145,10 +148,7 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
|
||||
}
|
||||
|
||||
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
|
||||
pub fn collect_per_file_ignores(
|
||||
pairs: Vec<PatternPrefixPair>,
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<Vec<PerFileIgnore>> {
|
||||
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<CheckCodePrefix>> = FxHashMap::default();
|
||||
for pair in pairs {
|
||||
per_file_ignores
|
||||
@@ -157,7 +157,7 @@ pub fn collect_per_file_ignores(
|
||||
.push(pair.prefix);
|
||||
}
|
||||
per_file_ignores
|
||||
.iter()
|
||||
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, prefixes, project_root))
|
||||
.into_iter()
|
||||
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
|
||||
.collect()
|
||||
}
|
||||
|
||||
66
src/commands.rs
Normal file
66
src/commands.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::fs::iter_python_files;
|
||||
use crate::printer::SerializationFormat;
|
||||
use crate::{Configuration, Settings};
|
||||
|
||||
/// Print the user-facing configuration settings.
|
||||
pub fn show_settings(
|
||||
configuration: &Configuration,
|
||||
project_root: Option<&PathBuf>,
|
||||
pyproject: Option<&PathBuf>,
|
||||
) {
|
||||
println!("Resolved configuration: {configuration:#?}");
|
||||
println!("Found project root at: {project_root:?}");
|
||||
println!("Found pyproject.toml at: {pyproject:?}");
|
||||
}
|
||||
|
||||
/// Show the list of files to be checked based on current settings.
|
||||
pub fn show_files(files: &[PathBuf], settings: &Settings) {
|
||||
let mut entries: Vec<DirEntry> = files
|
||||
.iter()
|
||||
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
|
||||
.flatten()
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.path().cmp(b.path()));
|
||||
for entry in entries {
|
||||
println!("{}", entry.path().to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Explanation<'a> {
|
||||
code: &'a str,
|
||||
category: &'a str,
|
||||
summary: &'a str,
|
||||
}
|
||||
|
||||
/// Explain a `CheckCode` to the user.
|
||||
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
|
||||
match format {
|
||||
SerializationFormat::Text => {
|
||||
println!(
|
||||
"{} ({}): {}",
|
||||
code.as_ref(),
|
||||
code.category().title(),
|
||||
code.kind().summary()
|
||||
);
|
||||
}
|
||||
SerializationFormat::Json => {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&Explanation {
|
||||
code: code.as_ref(),
|
||||
category: code.category().title(),
|
||||
summary: &code.kind().summary(),
|
||||
})?
|
||||
);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub use getattr_with_constant::getattr_with_constant;
|
||||
pub use jump_statement_in_finally::jump_statement_in_finally;
|
||||
pub use loop_variable_overrides_iterator::loop_variable_overrides_iterator;
|
||||
pub use mutable_argument_default::mutable_argument_default;
|
||||
pub use raise_without_from_inside_except::raise_without_from_inside_except;
|
||||
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
|
||||
pub use setattr_with_constant::setattr_with_constant;
|
||||
pub use star_arg_unpacking_after_keyword_arg::star_arg_unpacking_after_keyword_arg;
|
||||
@@ -35,6 +36,7 @@ mod getattr_with_constant;
|
||||
mod jump_statement_in_finally;
|
||||
mod loop_variable_overrides_iterator;
|
||||
mod mutable_argument_default;
|
||||
mod raise_without_from_inside_except;
|
||||
mod redundant_tuple_in_exception_handler;
|
||||
mod setattr_with_constant;
|
||||
mod star_arg_unpacking_after_keyword_arg;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use rustpython_ast::{ExprKind, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::check_ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::python::string::is_lower;
|
||||
|
||||
struct RaiseVisitor {
|
||||
checks: Vec<Check>,
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> for RaiseVisitor {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::Raise { exc, cause } => {
|
||||
if cause.is_none() {
|
||||
if let Some(exc) = exc {
|
||||
match &exc.node {
|
||||
ExprKind::Name { id, .. } if is_lower(id) => {}
|
||||
_ => {
|
||||
self.checks.push(Check::new(
|
||||
CheckKind::RaiseWithoutFromInsideExcept,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::ClassDef { .. }
|
||||
| StmtKind::FunctionDef { .. }
|
||||
| StmtKind::AsyncFunctionDef { .. }
|
||||
| StmtKind::Try { .. } => {}
|
||||
StmtKind::If { body, .. }
|
||||
| StmtKind::While { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. }
|
||||
| StmtKind::For { body, .. }
|
||||
| StmtKind::AsyncFor { body, .. } => {
|
||||
for stmt in body {
|
||||
self.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raise_without_from_inside_except(checker: &mut Checker, body: &[Stmt]) {
|
||||
let mut visitor = RaiseVisitor { checks: vec![] };
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
checker.add_checks(visitor.checks.into_iter());
|
||||
}
|
||||
152
src/fs.rs
152
src/fs.rs
@@ -1,16 +1,16 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use globset::GlobMatcher;
|
||||
use log::debug;
|
||||
use path_absolutize::{path_dedot, Absolutize};
|
||||
use rustc_hash::FxHashSet;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
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)> {
|
||||
@@ -25,32 +25,8 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
|
||||
Ok((file_path, file_basename))
|
||||
}
|
||||
|
||||
fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool
|
||||
where
|
||||
T: Iterator<Item = &'a FilePattern>,
|
||||
{
|
||||
for pattern in exclude {
|
||||
match pattern {
|
||||
FilePattern::Simple(basename) => {
|
||||
if *basename == file_basename {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
FilePattern::Complex(absolute, basename) => {
|
||||
if absolute.matches(file_path) {
|
||||
return true;
|
||||
}
|
||||
if basename
|
||||
.as_ref()
|
||||
.map(|pattern| pattern.matches(file_basename))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
false
|
||||
fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool {
|
||||
exclude.is_match(file_path) || exclude.is_match(file_basename)
|
||||
}
|
||||
|
||||
fn is_included(path: &Path) -> bool {
|
||||
@@ -60,18 +36,12 @@ fn is_included(path: &Path) -> bool {
|
||||
|
||||
pub fn iter_python_files<'a>(
|
||||
path: &'a Path,
|
||||
exclude: &'a [FilePattern],
|
||||
extend_exclude: &'a [FilePattern],
|
||||
exclude: &'a globset::GlobSet,
|
||||
extend_exclude: &'a globset::GlobSet,
|
||||
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
|
||||
// Run some checks over the provided patterns, to enable optimizations below.
|
||||
let has_exclude = !exclude.is_empty();
|
||||
let has_extend_exclude = !extend_exclude.is_empty();
|
||||
let exclude_simple = exclude
|
||||
.iter()
|
||||
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
|
||||
let extend_exclude_simple = extend_exclude
|
||||
.iter()
|
||||
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
|
||||
|
||||
WalkDir::new(normalize_path(path))
|
||||
.into_iter()
|
||||
@@ -83,17 +53,11 @@ pub fn iter_python_files<'a>(
|
||||
let path = entry.path();
|
||||
match extract_path_names(path) {
|
||||
Ok((file_path, file_basename)) => {
|
||||
let file_type = entry.file_type();
|
||||
|
||||
if has_exclude
|
||||
&& (!exclude_simple || file_type.is_dir())
|
||||
&& is_excluded(file_path, file_basename, exclude.iter())
|
||||
{
|
||||
if has_exclude && is_excluded(file_path, file_basename, exclude) {
|
||||
debug!("Ignored path via `exclude`: {:?}", path);
|
||||
false
|
||||
} else if has_extend_exclude
|
||||
&& (!extend_exclude_simple || file_type.is_dir())
|
||||
&& is_excluded(file_path, file_basename, extend_exclude.iter())
|
||||
&& is_excluded(file_path, file_basename, extend_exclude)
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
false
|
||||
@@ -119,19 +83,15 @@ pub fn iter_python_files<'a>(
|
||||
/// Create tree set with codes matching the pattern/code pairs.
|
||||
pub(crate) fn ignores_from_path<'a>(
|
||||
path: &Path,
|
||||
pattern_code_pairs: &'a [PerFileIgnore],
|
||||
) -> Result<FxHashSet<&'a CheckCode>> {
|
||||
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)],
|
||||
) -> Result<BTreeSet<&'a CheckCode>> {
|
||||
let (file_path, file_basename) = extract_path_names(path)?;
|
||||
Ok(pattern_code_pairs
|
||||
.iter()
|
||||
.filter(|pattern_code_pair| {
|
||||
is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
[&pattern_code_pair.pattern].into_iter(),
|
||||
)
|
||||
.filter(|(absolute, basename, _)| {
|
||||
basename.is_match(file_basename) || absolute.is_match(file_path)
|
||||
})
|
||||
.flat_map(|pattern_code_pair| &pattern_code_pair.codes)
|
||||
.flat_map(|(_, _, codes)| codes)
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -171,9 +131,10 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::GlobSet;
|
||||
use path_absolutize::Absolutize;
|
||||
|
||||
use crate::fs::{extract_path_names, is_excluded, is_included};
|
||||
@@ -194,73 +155,86 @@ mod tests {
|
||||
assert!(!is_included(&path));
|
||||
}
|
||||
|
||||
fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet {
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
file_pattern.add_to(&mut builder, project_root).unwrap();
|
||||
builder.build().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclusions() -> Result<()> {
|
||||
let project_root = Path::new("/tmp/");
|
||||
|
||||
let path = Path::new("foo").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("foo".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"bar",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("bar".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"baz.py",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("baz.py".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("foo/bar".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar/baz.py",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("foo/bar/baz.py".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"foo/bar/*.py",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("foo/bar/*.py".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
.unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
"baz",
|
||||
Some(&project_root.to_path_buf()),
|
||||
)?];
|
||||
let exclude = FilePattern::User("baz".to_string());
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(!is_excluded(file_path, file_basename, exclude.iter()));
|
||||
assert!(!is_excluded(
|
||||
file_path,
|
||||
file_basename,
|
||||
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ pub mod checks;
|
||||
pub mod checks_gen;
|
||||
pub mod cli;
|
||||
pub mod code_gen;
|
||||
pub mod commands;
|
||||
mod cst;
|
||||
mod directives;
|
||||
mod docstrings;
|
||||
@@ -70,6 +71,7 @@ pub mod settings;
|
||||
pub mod source_code_locator;
|
||||
#[cfg(feature = "update-informer")]
|
||||
pub mod updates;
|
||||
mod vendored;
|
||||
pub mod visibility;
|
||||
|
||||
/// Run Ruff over Python source code directly.
|
||||
@@ -86,10 +88,10 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
|
||||
None => debug!("Unable to find pyproject.toml; using default settings..."),
|
||||
};
|
||||
|
||||
let settings = Settings::from_configuration(Configuration::from_pyproject(
|
||||
pyproject.as_ref(),
|
||||
let settings = Settings::from_configuration(
|
||||
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?,
|
||||
project_root.as_ref(),
|
||||
)?);
|
||||
)?;
|
||||
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = tokenize(contents);
|
||||
|
||||
@@ -417,6 +417,7 @@ mod tests {
|
||||
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
|
||||
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
|
||||
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
|
||||
#[test_case(CheckCode::B904, Path::new("B904.py"); "B904")]
|
||||
#[test_case(CheckCode::BLE001, Path::new("BLE.py"); "BLE001")]
|
||||
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
|
||||
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
|
||||
@@ -505,6 +506,7 @@ mod tests {
|
||||
#[test_case(CheckCode::F405, Path::new("F405.py"); "F405")]
|
||||
#[test_case(CheckCode::F406, Path::new("F406.py"); "F406")]
|
||||
#[test_case(CheckCode::F407, Path::new("F407.py"); "F407")]
|
||||
#[test_case(CheckCode::F521, Path::new("F521.py"); "F521")]
|
||||
#[test_case(CheckCode::F541, Path::new("F541.py"); "F541")]
|
||||
#[test_case(CheckCode::F601, Path::new("F601.py"); "F601")]
|
||||
#[test_case(CheckCode::F602, Path::new("F602.py"); "F602")]
|
||||
|
||||
63
src/main.rs
63
src/main.rs
@@ -17,20 +17,18 @@ use std::process::ExitCode;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
|
||||
use ::ruff::cache;
|
||||
use ::ruff::checks::{CheckCode, CheckKind};
|
||||
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, Cli};
|
||||
use ::ruff::fs::iter_python_files;
|
||||
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin};
|
||||
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
|
||||
use ::ruff::logging::{set_up_logging, LogLevel};
|
||||
use ::ruff::message::Message;
|
||||
use ::ruff::printer::{Printer, SerializationFormat};
|
||||
use ::ruff::settings::configuration::Configuration;
|
||||
use ::ruff::settings::types::FilePattern;
|
||||
use ::ruff::settings::user::UserConfiguration;
|
||||
use ::ruff::settings::{pyproject, Settings};
|
||||
#[cfg(feature = "update-informer")]
|
||||
use ::ruff::updates;
|
||||
use ::ruff::{cache, commands};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
@@ -38,7 +36,6 @@ use log::{debug, error};
|
||||
use notify::{raw_watcher, RecursiveMode, Watcher};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use rayon::prelude::*;
|
||||
use ruff::linter::Diagnostics;
|
||||
use rustpython_ast::Location;
|
||||
use walkdir::DirEntry;
|
||||
|
||||
@@ -56,29 +53,6 @@ fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
|
||||
iterable.iter()
|
||||
}
|
||||
|
||||
fn show_settings(
|
||||
configuration: Configuration,
|
||||
project_root: Option<PathBuf>,
|
||||
pyproject: Option<PathBuf>,
|
||||
) {
|
||||
println!(
|
||||
"{:#?}",
|
||||
UserConfiguration::from_configuration(configuration, project_root, pyproject)
|
||||
);
|
||||
}
|
||||
|
||||
fn show_files(files: &[PathBuf], settings: &Settings) {
|
||||
let mut entries: Vec<DirEntry> = files
|
||||
.iter()
|
||||
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
|
||||
.flatten()
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.path().cmp(b.path()));
|
||||
for entry in entries {
|
||||
println!("{}", entry.path().to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_stdin() -> Result<String> {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().lock().read_to_string(&mut buffer)?;
|
||||
@@ -234,28 +208,16 @@ fn inner_main() -> Result<ExitCode> {
|
||||
};
|
||||
|
||||
// Reconcile configuration from pyproject.toml and command-line arguments.
|
||||
let exclude: Vec<FilePattern> = cli
|
||||
.exclude
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
|
||||
.collect::<Result<_>>()?;
|
||||
let extend_exclude: Vec<FilePattern> = cli
|
||||
.extend_exclude
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let mut configuration =
|
||||
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
|
||||
if !exclude.is_empty() {
|
||||
configuration.exclude = exclude;
|
||||
if !cli.exclude.is_empty() {
|
||||
configuration.exclude = cli.exclude;
|
||||
}
|
||||
if !extend_exclude.is_empty() {
|
||||
configuration.extend_exclude = extend_exclude;
|
||||
if !cli.extend_exclude.is_empty() {
|
||||
configuration.extend_exclude = cli.extend_exclude;
|
||||
}
|
||||
if !cli.per_file_ignores.is_empty() {
|
||||
configuration.per_file_ignores =
|
||||
collect_per_file_ignores(cli.per_file_ignores, project_root.as_ref())?;
|
||||
configuration.per_file_ignores = collect_per_file_ignores(cli.per_file_ignores);
|
||||
}
|
||||
if !cli.select.is_empty() {
|
||||
configuration.select = cli.select;
|
||||
@@ -294,21 +256,26 @@ fn inner_main() -> Result<ExitCode> {
|
||||
configuration.show_source = true;
|
||||
}
|
||||
|
||||
if let Some(code) = cli.explain {
|
||||
commands::explain(&code, cli.format)?;
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
if cli.show_settings && cli.show_files {
|
||||
eprintln!("Error: specify --show-settings or show-files (not both).");
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
if cli.show_settings {
|
||||
show_settings(configuration, project_root, pyproject);
|
||||
commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref());
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
// Extract settings for internal use.
|
||||
let fix_enabled: bool = configuration.fix;
|
||||
let settings = Settings::from_configuration(configuration);
|
||||
let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
|
||||
|
||||
if cli.show_files {
|
||||
show_files(&cli.files, &settings);
|
||||
commands::show_files(&cli.files, &settings);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,30 @@ use rustpython_parser::ast::{
|
||||
|
||||
use crate::ast::types::{BindingKind, FunctionScope, Range, Scope, ScopeKind};
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::vendored::format::{FieldName, FormatPart, FormatString, FromTemplate};
|
||||
|
||||
// F521
|
||||
pub fn string_dot_format_invalid(literal: &str, location: Range) -> Option<Check> {
|
||||
match FormatString::from_str(literal) {
|
||||
Err(e) => Some(Check::new(
|
||||
CheckKind::StringDotFormatInvalidFormat(e.to_string()),
|
||||
location,
|
||||
)),
|
||||
Ok(format_string) => {
|
||||
for part in format_string.format_parts {
|
||||
if let FormatPart::Field { field_name, .. } = &part {
|
||||
if let Err(e) = FieldName::parse(field_name) {
|
||||
return Some(Check::new(
|
||||
CheckKind::StringDotFormatInvalidFormat(e.to_string()),
|
||||
location,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// F631
|
||||
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {
|
||||
|
||||
22
src/pyflakes/format.rs
Normal file
22
src/pyflakes/format.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
//! Implements helper functions for using vendored/format.rs
|
||||
use std::fmt;
|
||||
|
||||
use crate::vendored::format::FormatParseError;
|
||||
|
||||
impl fmt::Display for FormatParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = match self {
|
||||
FormatParseError::EmptyAttribute => "Empty attribute in format string",
|
||||
FormatParseError::InvalidCharacterAfterRightBracket => {
|
||||
"Only '.' or '[' may follow ']' in format field specifier"
|
||||
}
|
||||
FormatParseError::InvalidFormatSpecifier => "Max string recursion exceeded",
|
||||
FormatParseError::MissingStartBracket => "Single '}' encountered in format string",
|
||||
FormatParseError::MissingRightBracket => "Expected '}' before end of string",
|
||||
FormatParseError::UnmatchedBracket => "Single '{' encountered in format string",
|
||||
_ => "Unexpected error parsing format string",
|
||||
};
|
||||
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod checks;
|
||||
pub mod fixes;
|
||||
mod format;
|
||||
pub mod plugins;
|
||||
|
||||
@@ -46,25 +46,25 @@ pub struct Configuration {
|
||||
|
||||
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"),
|
||||
FilePattern::Builtin(".bzr"),
|
||||
FilePattern::Builtin(".direnv"),
|
||||
FilePattern::Builtin(".eggs"),
|
||||
FilePattern::Builtin(".git"),
|
||||
FilePattern::Builtin(".hg"),
|
||||
FilePattern::Builtin(".mypy_cache"),
|
||||
FilePattern::Builtin(".nox"),
|
||||
FilePattern::Builtin(".pants.d"),
|
||||
FilePattern::Builtin(".ruff_cache"),
|
||||
FilePattern::Builtin(".svn"),
|
||||
FilePattern::Builtin(".tox"),
|
||||
FilePattern::Builtin(".venv"),
|
||||
FilePattern::Builtin("__pypackages__"),
|
||||
FilePattern::Builtin("_build"),
|
||||
FilePattern::Builtin("buck-out"),
|
||||
FilePattern::Builtin("build"),
|
||||
FilePattern::Builtin("dist"),
|
||||
FilePattern::Builtin("node_modules"),
|
||||
FilePattern::Builtin("venv"),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -103,22 +103,14 @@ impl Configuration {
|
||||
},
|
||||
),
|
||||
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()
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
|
||||
exclude: options.exclude.map_or_else(
|
||||
|| DEFAULT_EXCLUDE.clone(),
|
||||
|paths| paths.into_iter().map(FilePattern::User).collect(),
|
||||
),
|
||||
extend_exclude: options
|
||||
.extend_exclude
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, project_root))
|
||||
.collect::<Result<_>>()?,
|
||||
.map(|paths| paths.into_iter().map(FilePattern::User).collect())
|
||||
.unwrap_or_default(),
|
||||
extend_ignore: options.extend_ignore.unwrap_or_default(),
|
||||
select: options
|
||||
.select
|
||||
@@ -154,13 +146,10 @@ impl Configuration {
|
||||
.per_file_ignores
|
||||
.map(|per_file_ignores| {
|
||||
per_file_ignores
|
||||
.iter()
|
||||
.map(|(pattern, prefixes)| {
|
||||
PerFileIgnore::new(pattern, prefixes, project_root)
|
||||
})
|
||||
.into_iter()
|
||||
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
|
||||
.collect()
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
show_source: options.show_source.unwrap_or_default(),
|
||||
// Plugins
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
//! 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 std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use globset::{Glob, GlobMatcher, GlobSet};
|
||||
use path_absolutize::path_dedot;
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashSet;
|
||||
@@ -14,7 +17,7 @@ use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
|
||||
use crate::settings::configuration::Configuration;
|
||||
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
|
||||
use crate::{
|
||||
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe,
|
||||
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe,
|
||||
pep8_naming,
|
||||
};
|
||||
|
||||
@@ -22,17 +25,16 @@ 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: FxHashSet<CheckCode>,
|
||||
pub exclude: Vec<FilePattern>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub exclude: GlobSet,
|
||||
pub extend_exclude: GlobSet,
|
||||
pub fixable: FxHashSet<CheckCode>,
|
||||
pub line_length: usize,
|
||||
pub per_file_ignores: Vec<PerFileIgnore>,
|
||||
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)>,
|
||||
pub show_source: bool,
|
||||
pub src: Vec<PathBuf>,
|
||||
pub target_version: PythonVersion,
|
||||
@@ -47,8 +49,11 @@ pub struct Settings {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn from_configuration(config: Configuration) -> Self {
|
||||
Self {
|
||||
pub fn from_configuration(
|
||||
config: Configuration,
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
dummy_variable_rgx: config.dummy_variable_rgx,
|
||||
enabled: resolve_codes(
|
||||
&config
|
||||
@@ -62,8 +67,8 @@ impl Settings {
|
||||
.chain(config.extend_ignore.into_iter())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
exclude: config.exclude,
|
||||
extend_exclude: config.extend_exclude,
|
||||
exclude: resolve_globset(config.exclude, project_root)?,
|
||||
extend_exclude: resolve_globset(config.extend_exclude, project_root)?,
|
||||
fixable: resolve_codes(&config.fixable, &config.unfixable),
|
||||
flake8_annotations: config.flake8_annotations,
|
||||
flake8_bugbear: config.flake8_bugbear,
|
||||
@@ -73,11 +78,11 @@ impl Settings {
|
||||
mccabe: config.mccabe,
|
||||
line_length: config.line_length,
|
||||
pep8_naming: config.pep8_naming,
|
||||
per_file_ignores: config.per_file_ignores,
|
||||
per_file_ignores: resolve_per_file_ignores(config.per_file_ignores, project_root)?,
|
||||
src: config.src,
|
||||
target_version: config.target_version,
|
||||
show_source: config.show_source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn for_rule(check_code: CheckCode) -> Self {
|
||||
@@ -85,8 +90,8 @@ impl Settings {
|
||||
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
|
||||
enabled: FxHashSet::from_iter([check_code.clone()]),
|
||||
fixable: FxHashSet::from_iter([check_code]),
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
exclude: GlobSet::empty(),
|
||||
extend_exclude: GlobSet::empty(),
|
||||
line_length: 88,
|
||||
per_file_ignores: vec![],
|
||||
src: vec![path_dedot::CWD.clone()],
|
||||
@@ -107,8 +112,8 @@ impl Settings {
|
||||
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
|
||||
enabled: FxHashSet::from_iter(check_codes.clone()),
|
||||
fixable: FxHashSet::from_iter(check_codes),
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
exclude: GlobSet::empty(),
|
||||
extend_exclude: GlobSet::empty(),
|
||||
line_length: 88,
|
||||
per_file_ignores: vec![],
|
||||
src: vec![path_dedot::CWD.clone()],
|
||||
@@ -136,8 +141,10 @@ impl Hash for Settings {
|
||||
value.hash(state);
|
||||
}
|
||||
self.line_length.hash(state);
|
||||
for value in &self.per_file_ignores {
|
||||
value.hash(state);
|
||||
for (absolute, basename, codes) in &self.per_file_ignores {
|
||||
absolute.glob().hash(state);
|
||||
basename.glob().hash(state);
|
||||
codes.hash(state);
|
||||
}
|
||||
self.show_source.hash(state);
|
||||
self.target_version.hash(state);
|
||||
@@ -152,6 +159,42 @@ impl Hash for Settings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a list of patterns, create a `GlobSet`.
|
||||
pub fn resolve_globset(
|
||||
patterns: Vec<FilePattern>,
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<GlobSet> {
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
for pattern in patterns {
|
||||
pattern.add_to(&mut builder, project_root)?;
|
||||
}
|
||||
builder.build().map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
/// Given a list of patterns, create a `GlobSet`.
|
||||
pub fn resolve_per_file_ignores(
|
||||
per_file_ignores: Vec<PerFileIgnore>,
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<Vec<(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)>> {
|
||||
per_file_ignores
|
||||
.into_iter()
|
||||
.map(|per_file_ignore| {
|
||||
// Construct absolute path matcher.
|
||||
let path = Path::new(&per_file_ignore.pattern);
|
||||
let absolute_path = match project_root {
|
||||
Some(project_root) => fs::normalize_path_to(path, project_root),
|
||||
None => fs::normalize_path(path),
|
||||
};
|
||||
let absolute = Glob::new(&absolute_path.to_string_lossy())?.compile_matcher();
|
||||
|
||||
// Construct basename matcher.
|
||||
let basename = Glob::new(&per_file_ignore.pattern)?.compile_matcher();
|
||||
|
||||
Ok((absolute, basename, per_file_ignore.codes))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Given a set of selected and ignored prefixes, resolve the set of enabled
|
||||
/// error codes.
|
||||
fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use glob::Pattern;
|
||||
use globset::{Glob, GlobSetBuilder};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
@@ -44,46 +44,59 @@ impl FromStr for PythonVersion {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FilePattern {
|
||||
Simple(&'static str),
|
||||
Complex(Pattern, Option<Pattern>),
|
||||
Builtin(&'static str),
|
||||
User(String),
|
||||
}
|
||||
|
||||
impl FilePattern {
|
||||
pub fn from_user(pattern: &str, project_root: Option<&PathBuf>) -> Result<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),
|
||||
};
|
||||
pub fn add_to(
|
||||
self,
|
||||
builder: &mut GlobSetBuilder,
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
FilePattern::Builtin(pattern) => {
|
||||
builder.add(Glob::from_str(pattern)?);
|
||||
}
|
||||
FilePattern::User(pattern) => {
|
||||
// Add absolute path.
|
||||
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),
|
||||
};
|
||||
builder.add(Glob::new(&absolute_path.to_string_lossy())?);
|
||||
|
||||
let absolute = Pattern::new(&absolute_path.to_string_lossy())?;
|
||||
let basename = if pattern.contains(std::path::MAIN_SEPARATOR) {
|
||||
None
|
||||
} else {
|
||||
Some(Pattern::new(pattern)?)
|
||||
};
|
||||
// Add basename path.
|
||||
if !pattern.contains(std::path::MAIN_SEPARATOR) {
|
||||
builder.add(Glob::from_str(&pattern)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FilePattern::Complex(absolute, basename))
|
||||
impl FromStr for FilePattern {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::User(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct PerFileIgnore {
|
||||
pub pattern: FilePattern,
|
||||
pub pattern: String,
|
||||
pub codes: BTreeSet<CheckCode>,
|
||||
}
|
||||
|
||||
impl PerFileIgnore {
|
||||
pub fn new(
|
||||
pattern: &str,
|
||||
prefixes: &[CheckCodePrefix],
|
||||
project_root: Option<&PathBuf>,
|
||||
) -> Result<Self> {
|
||||
let pattern = FilePattern::from_user(pattern, project_root)?;
|
||||
pub fn new(pattern: String, prefixes: &[CheckCodePrefix]) -> Self {
|
||||
let codes = prefixes.iter().flat_map(CheckCodePrefix::codes).collect();
|
||||
Ok(Self { pattern, codes })
|
||||
Self { pattern, codes }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +128,9 @@ impl<'de> Deserialize<'de> for PatternPrefixPair {
|
||||
impl FromStr for PatternPrefixPair {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, Self::Err> {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (pattern_str, code_string) = {
|
||||
let tokens = string.split(':').collect::<Vec<_>>();
|
||||
let tokens = s.split(':').collect::<Vec<_>>();
|
||||
if tokens.len() != 2 {
|
||||
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
//! Structs to render user-facing settings.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::checks_gen::CheckCodePrefix;
|
||||
use crate::settings::types::{FilePattern, PythonVersion};
|
||||
use crate::{
|
||||
flake8_annotations, flake8_quotes, flake8_tidy_imports, isort, pep8_naming, 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 fix: bool,
|
||||
pub fixable: Vec<CheckCodePrefix>,
|
||||
pub ignore: Vec<CheckCodePrefix>,
|
||||
pub line_length: usize,
|
||||
pub per_file_ignores: Vec<(Exclusion, Vec<CheckCode>)>,
|
||||
pub select: Vec<CheckCodePrefix>,
|
||||
pub show_source: bool,
|
||||
pub src: Vec<PathBuf>,
|
||||
pub target_version: PythonVersion,
|
||||
pub unfixable: Vec<CheckCodePrefix>,
|
||||
// Plugins
|
||||
pub flake8_annotations: flake8_annotations::settings::Settings,
|
||||
pub flake8_quotes: flake8_quotes::settings::Settings,
|
||||
pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings,
|
||||
pub isort: isort::settings::Settings,
|
||||
pub pep8_naming: pep8_naming::settings::Settings,
|
||||
// Non-settings exposed to the user
|
||||
pub project_root: Option<PathBuf>,
|
||||
pub pyproject: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl UserConfiguration {
|
||||
pub fn from_configuration(
|
||||
configuration: Configuration,
|
||||
project_root: Option<PathBuf>,
|
||||
pyproject: Option<PathBuf>,
|
||||
) -> Self {
|
||||
Self {
|
||||
dummy_variable_rgx: configuration.dummy_variable_rgx,
|
||||
exclude: configuration
|
||||
.exclude
|
||||
.into_iter()
|
||||
.map(Exclusion::from_file_pattern)
|
||||
.collect(),
|
||||
extend_exclude: configuration
|
||||
.extend_exclude
|
||||
.into_iter()
|
||||
.map(Exclusion::from_file_pattern)
|
||||
.collect(),
|
||||
extend_ignore: configuration.extend_ignore,
|
||||
extend_select: configuration.extend_select,
|
||||
fix: configuration.fix,
|
||||
fixable: configuration.fixable,
|
||||
unfixable: configuration.unfixable,
|
||||
ignore: configuration.ignore,
|
||||
line_length: configuration.line_length,
|
||||
per_file_ignores: configuration
|
||||
.per_file_ignores
|
||||
.into_iter()
|
||||
.map(|per_file_ignore| {
|
||||
(
|
||||
Exclusion::from_file_pattern(per_file_ignore.pattern),
|
||||
Vec::from_iter(per_file_ignore.codes),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
select: configuration.select,
|
||||
src: configuration.src,
|
||||
target_version: configuration.target_version,
|
||||
show_source: configuration.show_source,
|
||||
flake8_annotations: configuration.flake8_annotations,
|
||||
flake8_quotes: configuration.flake8_quotes,
|
||||
flake8_tidy_imports: configuration.flake8_tidy_imports,
|
||||
isort: configuration.isort,
|
||||
pep8_naming: configuration.pep8_naming,
|
||||
project_root,
|
||||
pyproject,
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/snapshots/ruff__linter__tests__B904_B904.py.snap
Normal file
29
src/snapshots/ruff__linter__tests__B904_B904.py.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: RaiseWithoutFromInsideExcept
|
||||
location:
|
||||
row: 10
|
||||
column: 8
|
||||
end_location:
|
||||
row: 10
|
||||
column: 23
|
||||
fix: ~
|
||||
- kind: RaiseWithoutFromInsideExcept
|
||||
location:
|
||||
row: 11
|
||||
column: 4
|
||||
end_location:
|
||||
row: 11
|
||||
column: 21
|
||||
fix: ~
|
||||
- kind: RaiseWithoutFromInsideExcept
|
||||
location:
|
||||
row: 16
|
||||
column: 4
|
||||
end_location:
|
||||
row: 16
|
||||
column: 39
|
||||
fix: ~
|
||||
|
||||
68
src/snapshots/ruff__linter__tests__F521_F521.py.snap
Normal file
68
src/snapshots/ruff__linter__tests__F521_F521.py.snap
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: "Single '{' encountered in format string"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 1
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: "Single '}' encountered in format string"
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
end_location:
|
||||
row: 2
|
||||
column: 13
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: "Expected '}' before end of string"
|
||||
location:
|
||||
row: 3
|
||||
column: 0
|
||||
end_location:
|
||||
row: 3
|
||||
column: 22
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: Max string recursion exceeded
|
||||
location:
|
||||
row: 5
|
||||
column: 0
|
||||
end_location:
|
||||
row: 5
|
||||
column: 26
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: Empty attribute in format string
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 29
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: Empty attribute in format string
|
||||
location:
|
||||
row: 8
|
||||
column: 0
|
||||
end_location:
|
||||
row: 8
|
||||
column: 23
|
||||
fix: ~
|
||||
- kind:
|
||||
StringDotFormatInvalidFormat: Empty attribute in format string
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 9
|
||||
column: 26
|
||||
fix: ~
|
||||
|
||||
360
src/vendored/format.rs
Normal file
360
src/vendored/format.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
//! Vendored from [format.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/format.rs).
|
||||
//! The only changes we make are to remove dead code and code involving the vm.
|
||||
use itertools::{Itertools, PeekingNext};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FormatParseError {
|
||||
UnmatchedBracket,
|
||||
MissingStartBracket,
|
||||
UnescapedStartBracketInLiteral,
|
||||
InvalidFormatSpecifier,
|
||||
UnknownConversion,
|
||||
EmptyAttribute,
|
||||
MissingRightBracket,
|
||||
InvalidCharacterAfterRightBracket,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FieldNamePart {
|
||||
Attribute(String),
|
||||
Index(usize),
|
||||
StringIndex(String),
|
||||
}
|
||||
|
||||
impl FieldNamePart {
|
||||
fn parse_part(
|
||||
chars: &mut impl PeekingNext<Item = char>,
|
||||
) -> Result<Option<FieldNamePart>, FormatParseError> {
|
||||
chars
|
||||
.next()
|
||||
.map(|ch| match ch {
|
||||
'.' => {
|
||||
let mut attribute = String::new();
|
||||
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
|
||||
attribute.push(ch);
|
||||
}
|
||||
if attribute.is_empty() {
|
||||
Err(FormatParseError::EmptyAttribute)
|
||||
} else {
|
||||
Ok(FieldNamePart::Attribute(attribute))
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
let mut index = String::new();
|
||||
for ch in chars {
|
||||
if ch == ']' {
|
||||
return if index.is_empty() {
|
||||
Err(FormatParseError::EmptyAttribute)
|
||||
} else if let Ok(index) = index.parse::<usize>() {
|
||||
Ok(FieldNamePart::Index(index))
|
||||
} else {
|
||||
Ok(FieldNamePart::StringIndex(index))
|
||||
};
|
||||
}
|
||||
index.push(ch);
|
||||
}
|
||||
Err(FormatParseError::MissingRightBracket)
|
||||
}
|
||||
_ => Err(FormatParseError::InvalidCharacterAfterRightBracket),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FieldType {
|
||||
Auto,
|
||||
Index(usize),
|
||||
Keyword(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct FieldName {
|
||||
pub field_type: FieldType,
|
||||
pub parts: Vec<FieldNamePart>,
|
||||
}
|
||||
|
||||
impl FieldName {
|
||||
pub(crate) fn parse(text: &str) -> Result<FieldName, FormatParseError> {
|
||||
let mut chars = text.chars().peekable();
|
||||
let mut first = String::new();
|
||||
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
|
||||
first.push(ch);
|
||||
}
|
||||
|
||||
let field_type = if first.is_empty() {
|
||||
FieldType::Auto
|
||||
} else if let Ok(index) = first.parse::<usize>() {
|
||||
FieldType::Index(index)
|
||||
} else {
|
||||
FieldType::Keyword(first)
|
||||
};
|
||||
|
||||
let mut parts = Vec::new();
|
||||
while let Some(part) = FieldNamePart::parse_part(&mut chars)? {
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
Ok(FieldName { field_type, parts })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum FormatPart {
|
||||
Field {
|
||||
field_name: String,
|
||||
preconversion_spec: Option<char>,
|
||||
format_spec: String,
|
||||
},
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct FormatString {
|
||||
pub format_parts: Vec<FormatPart>,
|
||||
}
|
||||
|
||||
impl FormatString {
|
||||
fn parse_literal_single(text: &str) -> Result<(char, &str), FormatParseError> {
|
||||
let mut chars = text.chars();
|
||||
// This should never be called with an empty str
|
||||
let first_char = chars.next().unwrap();
|
||||
// isn't this detectable only with bytes operation?
|
||||
if first_char == '{' || first_char == '}' {
|
||||
let maybe_next_char = chars.next();
|
||||
// if we see a bracket, it has to be escaped by doubling up to be in a literal
|
||||
return if maybe_next_char.is_none() || maybe_next_char.unwrap() != first_char {
|
||||
Err(FormatParseError::UnescapedStartBracketInLiteral)
|
||||
} else {
|
||||
Ok((first_char, chars.as_str()))
|
||||
};
|
||||
}
|
||||
Ok((first_char, chars.as_str()))
|
||||
}
|
||||
|
||||
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
|
||||
let mut cur_text = text;
|
||||
let mut result_string = String::new();
|
||||
while !cur_text.is_empty() {
|
||||
match FormatString::parse_literal_single(cur_text) {
|
||||
Ok((next_char, remaining)) => {
|
||||
result_string.push(next_char);
|
||||
cur_text = remaining;
|
||||
}
|
||||
Err(err) => {
|
||||
return if result_string.is_empty() {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok((FormatPart::Literal(result_string), cur_text))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((FormatPart::Literal(result_string), ""))
|
||||
}
|
||||
|
||||
fn parse_part_in_brackets(text: &str) -> Result<FormatPart, FormatParseError> {
|
||||
let parts: Vec<&str> = text.splitn(2, ':').collect();
|
||||
// before the comma is a keyword or arg index, after the comma is maybe a spec.
|
||||
let arg_part = parts[0];
|
||||
|
||||
let format_spec = if parts.len() > 1 {
|
||||
parts[1].to_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// On parts[0] can still be the preconversor (!r, !s, !a)
|
||||
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
|
||||
// before the bang is a keyword or arg index, after the comma is maybe a
|
||||
// conversor spec.
|
||||
let arg_part = parts[0];
|
||||
|
||||
let preconversion_spec = parts
|
||||
.get(1)
|
||||
.map(|conversion| {
|
||||
// conversions are only every one character
|
||||
conversion
|
||||
.chars()
|
||||
.exactly_one()
|
||||
.map_err(|_| FormatParseError::UnknownConversion)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(FormatPart::Field {
|
||||
field_name: arg_part.to_owned(),
|
||||
preconversion_spec,
|
||||
format_spec,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_spec(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
|
||||
let mut nested = false;
|
||||
let mut end_bracket_pos = None;
|
||||
let mut left = String::new();
|
||||
|
||||
// There may be one layer nesting brackets in spec
|
||||
for (idx, c) in text.chars().enumerate() {
|
||||
if idx == 0 {
|
||||
if c != '{' {
|
||||
return Err(FormatParseError::MissingStartBracket);
|
||||
}
|
||||
} else if c == '{' {
|
||||
if nested {
|
||||
return Err(FormatParseError::InvalidFormatSpecifier);
|
||||
}
|
||||
nested = true;
|
||||
left.push(c);
|
||||
continue;
|
||||
} else if c == '}' {
|
||||
if nested {
|
||||
nested = false;
|
||||
left.push(c);
|
||||
continue;
|
||||
}
|
||||
end_bracket_pos = Some(idx);
|
||||
break;
|
||||
} else {
|
||||
left.push(c);
|
||||
}
|
||||
}
|
||||
if let Some(pos) = end_bracket_pos {
|
||||
let (_, right) = text.split_at(pos);
|
||||
let format_part = FormatString::parse_part_in_brackets(&left)?;
|
||||
Ok((format_part, &right[1..]))
|
||||
} else {
|
||||
Err(FormatParseError::UnmatchedBracket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait FromTemplate<'a>: Sized {
|
||||
type Err;
|
||||
fn from_str(s: &'a str) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
impl<'a> FromTemplate<'a> for FormatString {
|
||||
type Err = FormatParseError;
|
||||
|
||||
fn from_str(text: &'a str) -> Result<Self, Self::Err> {
|
||||
let mut cur_text: &str = text;
|
||||
let mut parts: Vec<FormatPart> = Vec::new();
|
||||
while !cur_text.is_empty() {
|
||||
// Try to parse both literals and bracketed format parts until we
|
||||
// run out of text
|
||||
cur_text = FormatString::parse_literal(cur_text)
|
||||
.or_else(|_| FormatString::parse_spec(cur_text))
|
||||
.map(|(part, new_text)| {
|
||||
parts.push(part);
|
||||
new_text
|
||||
})?;
|
||||
}
|
||||
Ok(FormatString {
|
||||
format_parts: parts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_parse() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("abcd".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "1".to_owned(),
|
||||
preconversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
FormatPart::Literal(":".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "key".to_owned(),
|
||||
preconversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("abcd{1}:{key}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_parse_fail() {
|
||||
assert_eq!(
|
||||
FormatString::from_str("{s"),
|
||||
Err(FormatParseError::UnmatchedBracket)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_parse_escape() {
|
||||
let expected = Ok(FormatString {
|
||||
format_parts: vec![
|
||||
FormatPart::Literal("{".to_owned()),
|
||||
FormatPart::Field {
|
||||
field_name: "key".to_owned(),
|
||||
preconversion_spec: None,
|
||||
format_spec: String::new(),
|
||||
},
|
||||
FormatPart::Literal("}ddfe".to_owned()),
|
||||
],
|
||||
});
|
||||
|
||||
assert_eq!(FormatString::from_str("{{{key}}}ddfe"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_field_name() {
|
||||
assert_eq!(
|
||||
FieldName::parse(""),
|
||||
Ok(FieldName {
|
||||
field_type: FieldType::Auto,
|
||||
parts: Vec::new(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("0"),
|
||||
Ok(FieldName {
|
||||
field_type: FieldType::Index(0),
|
||||
parts: Vec::new(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key"),
|
||||
Ok(FieldName {
|
||||
field_type: FieldType::Keyword("key".to_owned()),
|
||||
parts: Vec::new(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key.attr[0][string]"),
|
||||
Ok(FieldName {
|
||||
field_type: FieldType::Keyword("key".to_owned()),
|
||||
parts: vec![
|
||||
FieldNamePart::Attribute("attr".to_owned()),
|
||||
FieldNamePart::Index(0),
|
||||
FieldNamePart::StringIndex("string".to_owned())
|
||||
],
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key.."),
|
||||
Err(FormatParseError::EmptyAttribute)
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key[]"),
|
||||
Err(FormatParseError::EmptyAttribute)
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key["),
|
||||
Err(FormatParseError::MissingRightBracket)
|
||||
);
|
||||
assert_eq!(
|
||||
FieldName::parse("key[0]after"),
|
||||
Err(FormatParseError::InvalidCharacterAfterRightBracket)
|
||||
);
|
||||
}
|
||||
}
|
||||
1
src/vendored/mod.rs
Normal file
1
src/vendored/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod format;
|
||||
Reference in New Issue
Block a user