Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949e4d4077 | ||
|
|
c8cb2eead2 | ||
|
|
02ae494a0e | ||
|
|
dce86e065b | ||
|
|
d77979429c | ||
|
|
a3a15d2eb2 | ||
|
|
5af95428ff | ||
|
|
6338cad4e6 | ||
|
|
485881877f |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1801,7 +1801,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.45"
|
||||
version = "0.0.46"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -86,7 +86,7 @@ ruff path/to/code/ --select F401 F403
|
||||
See `ruff --help` for more:
|
||||
|
||||
```shell
|
||||
ruff (v0.0.45) 0.0.45
|
||||
ruff (v0.0.46)
|
||||
An extremely fast Python linter.
|
||||
|
||||
USAGE:
|
||||
@@ -119,6 +119,8 @@ OPTIONS:
|
||||
Print help information
|
||||
-n, --no-cache
|
||||
Disable cache reads
|
||||
--per-file-ignores <PER_FILE_IGNORES>...
|
||||
List of mappings from file pattern to code to exclude
|
||||
-q, --quiet
|
||||
Disable all logging (but still exit with status code "1" upon detecting errors)
|
||||
--add-noqa
|
||||
@@ -210,8 +212,7 @@ variables.)
|
||||
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
|
||||
|
||||
1. Flake8 has a plugin architecture and supports writing custom lint rules.
|
||||
2. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
|
||||
3. ruff does not yet support parenthesized context managers.
|
||||
2. ruff does not yet support parenthesized context managers.
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
|
||||
12
resources/test/fixtures/F841.py
vendored
12
resources/test/fixtures/F841.py
vendored
@@ -10,13 +10,13 @@ except ValueError as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def f():
|
||||
def f1():
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + y
|
||||
|
||||
|
||||
def g():
|
||||
def f2():
|
||||
foo = (1, 2)
|
||||
(a, b) = (1, 2)
|
||||
|
||||
@@ -26,6 +26,12 @@ def g():
|
||||
(x, y) = baz = bar
|
||||
|
||||
|
||||
def h():
|
||||
def f3():
|
||||
locals()
|
||||
x = 1
|
||||
|
||||
|
||||
def f4():
|
||||
_ = 1
|
||||
__ = 1
|
||||
_discarded = 1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::izip;
|
||||
use regex::Regex;
|
||||
use rustpython_parser::ast::{
|
||||
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
|
||||
Location, Stmt, StmtKind, Unaryop,
|
||||
@@ -71,7 +72,11 @@ pub fn check_not_tests(
|
||||
}
|
||||
|
||||
/// Check UnusedVariable compliance.
|
||||
pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec<Check> {
|
||||
pub fn check_unused_variables(
|
||||
scope: &Scope,
|
||||
locator: &dyn CheckLocator,
|
||||
dummy_variable_rgx: &Regex,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
if matches!(
|
||||
@@ -82,13 +87,12 @@ pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec<
|
||||
}
|
||||
|
||||
for (name, binding) in scope.values.iter() {
|
||||
// TODO(charlie): Ignore if using `locals`.
|
||||
if binding.used.is_none()
|
||||
&& name != "_"
|
||||
&& matches!(binding.kind, BindingKind::Assignment)
|
||||
&& !dummy_variable_rgx.is_match(name)
|
||||
&& name != "__tracebackhide__"
|
||||
&& name != "__traceback_info__"
|
||||
&& name != "__traceback_supplement__"
|
||||
&& matches!(binding.kind, BindingKind::Assignment)
|
||||
{
|
||||
checks.push(Check::new(
|
||||
CheckKind::UnusedVariable(name.to_string()),
|
||||
|
||||
@@ -1415,8 +1415,11 @@ impl<'a> Checker<'a> {
|
||||
fn check_deferred_assignments(&mut self) {
|
||||
if self.settings.select.contains(&CheckCode::F841) {
|
||||
while let Some(index) = self.deferred_assignments.pop() {
|
||||
self.checks
|
||||
.extend(checks::check_unused_variables(&self.scopes[index], self));
|
||||
self.checks.extend(checks::check_unused_variables(
|
||||
&self.scopes[index],
|
||||
self,
|
||||
&self.settings.dummy_variable_rgx,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +180,11 @@ pub fn check_lines(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::settings;
|
||||
|
||||
use super::check_lines;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn e501_non_ascii_char() {
|
||||
@@ -191,19 +192,14 @@ mod tests {
|
||||
let noqa_line_for: Vec<usize> = vec![1];
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
let settings = Settings {
|
||||
pyproject: None,
|
||||
project_root: None,
|
||||
line_length,
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from_iter(vec![CheckCode::E501]),
|
||||
};
|
||||
check_lines(
|
||||
&mut checks,
|
||||
line,
|
||||
&noqa_line_for,
|
||||
&settings,
|
||||
&settings::Settings {
|
||||
line_length,
|
||||
..settings::Settings::for_rule(CheckCode::E501)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
);
|
||||
return checks;
|
||||
|
||||
50
src/fs.rs
50
src/fs.rs
@@ -1,4 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::ops::Deref;
|
||||
@@ -10,7 +11,8 @@ use path_absolutize::path_dedot;
|
||||
use path_absolutize::Absolutize;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::settings::FilePattern;
|
||||
use crate::checks::CheckCode;
|
||||
use crate::settings::{FilePattern, PerFileIgnore};
|
||||
|
||||
/// Extract the absolute path and basename (as strings) from a Path.
|
||||
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
|
||||
@@ -25,9 +27,12 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
|
||||
Ok((file_path, file_basename))
|
||||
}
|
||||
|
||||
fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) -> bool {
|
||||
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 {
|
||||
match pattern {
|
||||
FilePattern::Simple(basename) => {
|
||||
if *basename == file_basename {
|
||||
return true;
|
||||
@@ -45,7 +50,7 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) ->
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -85,13 +90,13 @@ pub fn iter_python_files<'a>(
|
||||
|
||||
if has_exclude
|
||||
&& (!exclude_simple || file_type.is_dir())
|
||||
&& is_excluded(file_path, file_basename, exclude)
|
||||
&& is_excluded(file_path, file_basename, exclude.iter())
|
||||
{
|
||||
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)
|
||||
&& is_excluded(file_path, file_basename, extend_exclude.iter())
|
||||
{
|
||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||
false
|
||||
@@ -112,6 +117,25 @@ pub fn iter_python_files<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
/// Create tree set with codes matching the pattern/code pairs.
|
||||
pub fn ignores_from_path<'a>(
|
||||
path: &Path,
|
||||
pattern_code_pairs: &'a [PerFileIgnore],
|
||||
) -> 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(),
|
||||
)
|
||||
})
|
||||
.map(|pattern_code_pair| &pattern_code_pair.code)
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Convert any path to an absolute path (based on the current working directory).
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
if let Ok(path) = path.absolutize() {
|
||||
@@ -180,7 +204,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
@@ -188,7 +212,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
@@ -198,7 +222,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
|
||||
let exclude = vec![FilePattern::from_user(
|
||||
@@ -206,7 +230,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
@@ -216,7 +240,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
@@ -226,7 +250,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py")
|
||||
.absolutize_from(project_root)
|
||||
@@ -236,7 +260,7 @@ mod tests {
|
||||
&Some(project_root.to_path_buf()),
|
||||
)];
|
||||
let (file_path, file_basename) = extract_path_names(&path)?;
|
||||
assert!(!is_excluded(file_path, file_basename, &exclude));
|
||||
assert!(!is_excluded(file_path, file_basename, exclude.iter()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ fn check_path(
|
||||
tokens: Vec<LexResult>,
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) -> Vec<Check> {
|
||||
) -> Result<Vec<Check>> {
|
||||
// Aggregate all checks.
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
|
||||
@@ -52,7 +52,18 @@ fn check_path(
|
||||
// Run the lines-based checks.
|
||||
check_lines(&mut checks, contents, &noqa_line_for, settings, autofix);
|
||||
|
||||
checks
|
||||
// Create path ignores.
|
||||
if !checks.is_empty() && !settings.per_file_ignores.is_empty() {
|
||||
let ignores = fs::ignores_from_path(path, &settings.per_file_ignores)?;
|
||||
if !ignores.is_empty() {
|
||||
return Ok(checks
|
||||
.into_iter()
|
||||
.filter(|check| !ignores.contains(check.kind.code()))
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(checks)
|
||||
}
|
||||
|
||||
pub fn lint_path(
|
||||
@@ -76,7 +87,7 @@ pub fn lint_path(
|
||||
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
|
||||
|
||||
// Generate checks.
|
||||
let mut checks = check_path(path, &contents, tokens, settings, autofix);
|
||||
let mut checks = check_path(path, &contents, tokens, settings, autofix)?;
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
@@ -109,7 +120,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
|
||||
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
|
||||
|
||||
// Generate checks.
|
||||
let checks = check_path(path, &contents, tokens, settings, &fixer::Mode::None);
|
||||
let checks = check_path(path, &contents, tokens, settings, &fixer::Mode::None)?;
|
||||
|
||||
add_noqa(&checks, &contents, &noqa_line_for, path)
|
||||
}
|
||||
@@ -119,6 +130,7 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
@@ -135,9 +147,7 @@ mod tests {
|
||||
) -> Result<Vec<Check>> {
|
||||
let contents = fs::read_file(path)?;
|
||||
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
|
||||
Ok(linter::check_path(
|
||||
path, &contents, tokens, settings, autofix,
|
||||
))
|
||||
linter::check_path(path, &contents, tokens, settings, autofix)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -596,6 +606,21 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f841_dummy_variable_rgx() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/F841.py"),
|
||||
&settings::Settings {
|
||||
dummy_variable_rgx: Regex::new(r"^z$").unwrap(),
|
||||
..settings::Settings::for_rule(CheckCode::F841)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f901() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
|
||||
39
src/main.rs
39
src/main.rs
@@ -1,5 +1,6 @@
|
||||
extern crate core;
|
||||
|
||||
use regex::Regex;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
@@ -18,14 +19,14 @@ use ::ruff::cache;
|
||||
use ::ruff::checks::CheckCode;
|
||||
use ::ruff::checks::CheckKind;
|
||||
use ::ruff::fs::iter_python_files;
|
||||
use ::ruff::linter::add_noqa_to_path;
|
||||
use ::ruff::linter::lint_path;
|
||||
use ::ruff::logging::set_up_logging;
|
||||
use ::ruff::message::Message;
|
||||
use ::ruff::printer::{Printer, SerializationFormat};
|
||||
use ::ruff::pyproject;
|
||||
use ::ruff::settings::{FilePattern, Settings};
|
||||
use ::ruff::pyproject::{self, StrCheckCodePair};
|
||||
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
|
||||
use ::ruff::tell_user;
|
||||
use ruff::linter::add_noqa_to_path;
|
||||
|
||||
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -73,6 +74,9 @@ struct Cli {
|
||||
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
|
||||
#[clap(long, multiple = true)]
|
||||
extend_exclude: Vec<String>,
|
||||
/// List of mappings from file pattern to code to exclude
|
||||
#[clap(long, multiple = true)]
|
||||
per_file_ignores: Vec<StrCheckCodePair>,
|
||||
/// Output serialization format for error messages.
|
||||
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
|
||||
format: SerializationFormat,
|
||||
@@ -85,6 +89,9 @@ struct Cli {
|
||||
/// Enable automatic additions of noqa directives to failing lines.
|
||||
#[clap(long, action)]
|
||||
add_noqa: bool,
|
||||
/// Regular expression matching the name of dummy variables.
|
||||
#[clap(long)]
|
||||
dummy_variable_rgx: Option<Regex>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "update-informer")]
|
||||
@@ -244,14 +251,22 @@ fn inner_main() -> Result<ExitCode> {
|
||||
.iter()
|
||||
.map(|path| FilePattern::from_user(path, &project_root))
|
||||
.collect();
|
||||
let per_file_ignores: Vec<PerFileIgnore> = cli
|
||||
.per_file_ignores
|
||||
.into_iter()
|
||||
.map(|pair| PerFileIgnore::new(pair, &project_root))
|
||||
.collect();
|
||||
|
||||
let mut settings = Settings::from_pyproject(pyproject, project_root);
|
||||
let mut settings = Settings::from_pyproject(pyproject, project_root)?;
|
||||
if !exclude.is_empty() {
|
||||
settings.exclude = exclude;
|
||||
}
|
||||
if !extend_exclude.is_empty() {
|
||||
settings.extend_exclude = extend_exclude;
|
||||
}
|
||||
if !per_file_ignores.is_empty() {
|
||||
settings.per_file_ignores = per_file_ignores;
|
||||
}
|
||||
if !cli.select.is_empty() {
|
||||
settings.clear();
|
||||
settings.select(cli.select);
|
||||
@@ -265,9 +280,12 @@ fn inner_main() -> Result<ExitCode> {
|
||||
if !cli.extend_ignore.is_empty() {
|
||||
settings.ignore(&cli.extend_ignore);
|
||||
}
|
||||
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
|
||||
settings.dummy_variable_rgx = dummy_variable_rgx;
|
||||
}
|
||||
|
||||
if cli.show_settings && cli.show_files {
|
||||
println!("Error: specify --show-settings or show-files (not both).");
|
||||
eprintln!("Error: specify --show-settings or show-files (not both).");
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
if cli.show_settings {
|
||||
@@ -284,15 +302,15 @@ fn inner_main() -> Result<ExitCode> {
|
||||
let mut printer = Printer::new(cli.format, cli.verbose);
|
||||
if cli.watch {
|
||||
if cli.fix {
|
||||
println!("Warning: --fix is not enabled in watch mode.");
|
||||
eprintln!("Warning: --fix is not enabled in watch mode.");
|
||||
}
|
||||
|
||||
if cli.add_noqa {
|
||||
println!("Warning: --no-qa is not enabled in watch mode.");
|
||||
eprintln!("Warning: --no-qa is not enabled in watch mode.");
|
||||
}
|
||||
|
||||
if cli.format != SerializationFormat::Text {
|
||||
println!("Warning: --format 'text' is used in watch mode.");
|
||||
eprintln!("Warning: --format 'text' is used in watch mode.");
|
||||
}
|
||||
|
||||
// Perform an initial run instantly.
|
||||
@@ -354,6 +372,9 @@ fn inner_main() -> Result<ExitCode> {
|
||||
fn main() -> ExitCode {
|
||||
match inner_main() {
|
||||
Ok(code) => code,
|
||||
Err(_) => ExitCode::FAILURE,
|
||||
Err(err) => {
|
||||
eprintln!("{} {:?}", "error".red().bold(), err);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use colored::Colorize;
|
||||
use rustpython_parser::ast::Location;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::checks::{CheckCode, CheckKind};
|
||||
use crate::message::Message;
|
||||
use crate::tell_user;
|
||||
|
||||
@@ -11,6 +14,16 @@ pub enum SerializationFormat {
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExpandedMessage<'a> {
|
||||
kind: &'a CheckKind,
|
||||
code: &'a CheckCode,
|
||||
message: String,
|
||||
fixed: bool,
|
||||
location: Location,
|
||||
filename: &'a String,
|
||||
}
|
||||
|
||||
pub struct Printer {
|
||||
format: SerializationFormat,
|
||||
verbose: bool,
|
||||
@@ -31,7 +44,22 @@ impl Printer {
|
||||
|
||||
match self.format {
|
||||
SerializationFormat::Json => {
|
||||
println!("{}", serde_json::to_string_pretty(&messages)?)
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&messages
|
||||
.iter()
|
||||
.map(|m| ExpandedMessage {
|
||||
kind: &m.kind,
|
||||
code: m.kind.code(),
|
||||
message: m.kind.body(),
|
||||
fixed: m.fixed,
|
||||
location: m.location,
|
||||
filename: &m.filename,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
)?
|
||||
)
|
||||
}
|
||||
SerializationFormat::Text => {
|
||||
if !fixed.is_empty() {
|
||||
|
||||
105
src/pyproject.rs
105
src/pyproject.rs
@@ -1,30 +1,25 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use common_path::common_path_all;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::Deserialize;
|
||||
use serde::de;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::fs;
|
||||
|
||||
pub fn load_config(pyproject: &Option<PathBuf>) -> Config {
|
||||
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
|
||||
match pyproject {
|
||||
Some(pyproject) => match parse_pyproject_toml(pyproject) {
|
||||
Ok(pyproject) => pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.unwrap_or_default(),
|
||||
Err(e) => {
|
||||
println!("Failed to load pyproject.toml: {:?}", e);
|
||||
println!("Falling back to default configuration...");
|
||||
Default::default()
|
||||
}
|
||||
},
|
||||
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
.unwrap_or_default()),
|
||||
None => {
|
||||
println!("No pyproject.toml found.");
|
||||
println!("Falling back to default configuration...");
|
||||
Default::default()
|
||||
eprintln!("No pyproject.toml found.");
|
||||
eprintln!("Falling back to default configuration...");
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +32,50 @@ pub struct Config {
|
||||
pub extend_exclude: Option<Vec<String>>,
|
||||
pub select: Option<Vec<CheckCode>>,
|
||||
pub ignore: Option<Vec<CheckCode>>,
|
||||
pub per_file_ignores: Option<Vec<StrCheckCodePair>>,
|
||||
pub dummy_variable_rgx: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct StrCheckCodePair {
|
||||
pub pattern: String,
|
||||
pub code: CheckCode,
|
||||
}
|
||||
|
||||
impl StrCheckCodePair {
|
||||
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for StrCheckCodePair {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let str_result = String::deserialize(deserializer)?;
|
||||
Self::from_str(str_result.as_str()).map_err(|_| {
|
||||
de::Error::invalid_value(
|
||||
de::Unexpected::Str(str_result.as_str()),
|
||||
&Self::EXPECTED_PATTERN,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for StrCheckCodePair {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, Self::Err> {
|
||||
let (pattern_str, code_string) = {
|
||||
let tokens = string.split(':').collect::<Vec<_>>();
|
||||
if tokens.len() != 2 {
|
||||
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
|
||||
}
|
||||
(tokens[0], tokens[1])
|
||||
};
|
||||
let code = CheckCode::from_str(code_string)?;
|
||||
let pattern = pattern_str.into();
|
||||
Ok(Self { pattern, code })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
@@ -102,9 +141,11 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
|
||||
mod tests {
|
||||
use std::env::current_dir;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::StrCheckCodePair;
|
||||
use crate::checks::CheckCode;
|
||||
use crate::pyproject::{
|
||||
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
|
||||
@@ -137,6 +178,8 @@ mod tests {
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -157,6 +200,8 @@ line-length = 79
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -177,6 +222,8 @@ exclude = ["foo.py"]
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: None,
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -197,6 +244,8 @@ select = ["E501"]
|
||||
extend_exclude: None,
|
||||
select: Some(vec![CheckCode::E501]),
|
||||
ignore: None,
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -217,6 +266,8 @@ ignore = ["E501"]
|
||||
extend_exclude: None,
|
||||
select: None,
|
||||
ignore: Some(vec![CheckCode::E501]),
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
})
|
||||
})
|
||||
);
|
||||
@@ -281,9 +332,29 @@ other-attribute = 1
|
||||
]),
|
||||
select: None,
|
||||
ignore: None,
|
||||
per_file_ignores: None,
|
||||
dummy_variable_rgx: None,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_check_code_pair_strings() {
|
||||
let result = StrCheckCodePair::from_str("foo:E501");
|
||||
assert!(result.is_ok());
|
||||
let result = StrCheckCodePair::from_str("E501:foo");
|
||||
assert!(result.is_err());
|
||||
let result = StrCheckCodePair::from_str("E501");
|
||||
assert!(result.is_err());
|
||||
let result = StrCheckCodePair::from_str("foo");
|
||||
assert!(result.is_err());
|
||||
let result = StrCheckCodePair::from_str("foo:E501:E402");
|
||||
assert!(result.is_err());
|
||||
let result = StrCheckCodePair::from_str("**/bar:E501");
|
||||
assert!(result.is_ok());
|
||||
let result = StrCheckCodePair::from_str("bar:E502");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ use std::collections::BTreeSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use glob::Pattern;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
|
||||
use crate::fs;
|
||||
use crate::pyproject::load_config;
|
||||
use crate::pyproject::{load_config, StrCheckCodePair};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum FilePattern {
|
||||
Simple(&'static str),
|
||||
Complex(Pattern, Option<Pattern>),
|
||||
@@ -34,6 +36,20 @@ impl FilePattern {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct PerFileIgnore {
|
||||
pub pattern: FilePattern,
|
||||
pub code: CheckCode,
|
||||
}
|
||||
|
||||
impl PerFileIgnore {
|
||||
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
|
||||
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
|
||||
let code = user_in.code;
|
||||
Self { pattern, code }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Settings {
|
||||
pub pyproject: Option<PathBuf>,
|
||||
@@ -42,6 +58,8 @@ pub struct Settings {
|
||||
pub exclude: Vec<FilePattern>,
|
||||
pub extend_exclude: Vec<FilePattern>,
|
||||
pub select: BTreeSet<CheckCode>,
|
||||
pub per_file_ignores: Vec<PerFileIgnore>,
|
||||
pub dummy_variable_rgx: Regex,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -53,6 +71,8 @@ impl Settings {
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from([check_code]),
|
||||
per_file_ignores: vec![],
|
||||
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +84,8 @@ impl Settings {
|
||||
exclude: vec![],
|
||||
extend_exclude: vec![],
|
||||
select: BTreeSet::from_iter(check_codes),
|
||||
per_file_ignores: vec![],
|
||||
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,9 +93,13 @@ impl Settings {
|
||||
impl Hash for Settings {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.line_length.hash(state);
|
||||
self.dummy_variable_rgx.as_str().hash(state);
|
||||
for value in self.select.iter() {
|
||||
value.hash(state);
|
||||
}
|
||||
for value in self.per_file_ignores.iter() {
|
||||
value.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +127,15 @@ static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
|
||||
]
|
||||
});
|
||||
|
||||
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
|
||||
|
||||
impl Settings {
|
||||
pub fn from_pyproject(pyproject: Option<PathBuf>, project_root: Option<PathBuf>) -> Self {
|
||||
let config = load_config(&pyproject);
|
||||
pub fn from_pyproject(
|
||||
pyproject: Option<PathBuf>,
|
||||
project_root: Option<PathBuf>,
|
||||
) -> Result<Self> {
|
||||
let config = load_config(&pyproject)?;
|
||||
let mut settings = Settings {
|
||||
line_length: config.line_length.unwrap_or(88),
|
||||
exclude: config
|
||||
@@ -129,13 +161,27 @@ impl Settings {
|
||||
} else {
|
||||
BTreeSet::from_iter(DEFAULT_CHECK_CODES)
|
||||
},
|
||||
per_file_ignores: config
|
||||
.per_file_ignores
|
||||
.map(|ignore_strings| {
|
||||
ignore_strings
|
||||
.into_iter()
|
||||
.map(|pair| PerFileIgnore::new(pair, &project_root))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
dummy_variable_rgx: match config.dummy_variable_rgx {
|
||||
Some(pattern) => Regex::new(&pattern)
|
||||
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
|
||||
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
|
||||
},
|
||||
pyproject,
|
||||
project_root,
|
||||
};
|
||||
if let Some(ignore) = &config.ignore {
|
||||
settings.ignore(ignore);
|
||||
}
|
||||
settings
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: src/linter.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind:
|
||||
UnusedVariable: e
|
||||
location:
|
||||
row: 3
|
||||
column: 1
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: foo
|
||||
location:
|
||||
row: 20
|
||||
column: 5
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: a
|
||||
location:
|
||||
row: 21
|
||||
column: 6
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: b
|
||||
location:
|
||||
row: 21
|
||||
column: 9
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: _
|
||||
location:
|
||||
row: 35
|
||||
column: 5
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: __
|
||||
location:
|
||||
row: 36
|
||||
column: 5
|
||||
fix: ~
|
||||
- kind:
|
||||
UnusedVariable: _discarded
|
||||
location:
|
||||
row: 37
|
||||
column: 5
|
||||
fix: ~
|
||||
|
||||
Reference in New Issue
Block a user