Compare commits

..

13 Commits

Author SHA1 Message Date
Charlie Marsh
3937885f37 Bump version to 0.0.40 2022-09-16 04:57:21 -04:00
Charlie Marsh
24de97d951 Create cache directory prior to writing .gitignore 2022-09-16 04:56:58 -04:00
Charlie Marsh
06e5b3e457 Bump version to 0.0.39 2022-09-15 21:41:14 -04:00
Charlie Marsh
68a0e6dc19 Remove erroneous test dir 2022-09-15 21:41:00 -04:00
Charlie Marsh
9d4a4478f7 Improve exclusion syntax to match exact files (#209) 2022-09-15 21:40:49 -04:00
Charlie Marsh
6bbf3f46c4 Add .gitignore to .ruff_cache (#208) 2022-09-15 20:40:06 -04:00
Charlie Marsh
4ac4e8c991 Exclude .ruff_cache by default (#207) 2022-09-15 20:39:39 -04:00
Dmitry Dygalo
0091a3ae5f chore: Do not read the same file twice (#206) 2022-09-15 16:05:29 -04:00
Patrick Haller
17b3109a8b Update docs with --format flag (#205) 2022-09-15 16:04:07 -04:00
Charlie Marsh
71520213c1 Allow __path__ in __init__.py (#201) 2022-09-15 09:44:03 -04:00
Charlie Marsh
f24e7a0052 Add trailing period to help message 2022-09-15 09:43:51 -04:00
Patrick Haller
507e9f7ec3 Fix: Structured output Issue Fix (#186) 2022-09-15 09:43:10 -04:00
Charlie Marsh
592c53c8bf Use binding location when reporting F821 errors (#200) 2022-09-14 22:51:07 -04:00
20 changed files with 343 additions and 138 deletions

View File

@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.38
rev: v0.0.40
hooks:
- id: lint

21
Cargo.lock generated
View File

@@ -1432,6 +1432,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "path-absolutize"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b"
dependencies = [
"once_cell",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -1783,7 +1801,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.38"
version = "0.0.40"
dependencies = [
"anyhow",
"bincode",
@@ -1802,6 +1820,7 @@ dependencies = [
"log",
"notify",
"once_cell",
"path-absolutize",
"rayon",
"regex",
"rustpython-parser",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.38"
version = "0.0.40"
edition = "2021"
[lib]
@@ -23,6 +23,7 @@ itertools = "0.10.3"
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
path-absolutize = "3.0.13"
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }

View File

@@ -57,7 +57,7 @@ ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on sys
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.38
rev: v0.0.40
hooks:
- id: lint
```
@@ -86,7 +86,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff (v0.0.38)
ruff (v0.0.40)
An extremely fast Python linter.
USAGE:
@@ -96,19 +96,42 @@ ARGS:
<FILES>...
OPTIONS:
-e, --exit-zero Exit with status code "0", even upon detecting errors
--exclude <EXCLUDE>... List of file and/or directory patterns to exclude from checks
-f, --fix Attempt to automatically fix lint errors
-h, --help Print help information
--ignore <IGNORE>... List of error codes to ignore
-n, --no-cache Disable cache reads
-q, --quiet Disable all logging (but still exit with status code "1" upon
detecting errors)
--select <SELECT>... List of error codes to enable
-v, --verbose Enable verbose logging
-w, --watch Run in watch mode by re-running whenever files change
-e, --exit-zero
Exit with status code "0", even upon detecting errors
--exclude <EXCLUDE>...
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>...
Like --exclude, but adds additional files and directories on top of the excluded ones
-f, --fix
Attempt to automatically fix lint errors
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text,
json]
-h, --help
Print help information
--ignore <IGNORE>...
List of error codes to ignore
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--select <SELECT>...
List of error codes to enable
-v, --verbose
Enable verbose logging
-w, --watch
Run in watch mode by re-running whenever files change
```
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `./directory/foo.py` (to exclude that specific file) or `./directory/*.py`
(to exclude any Python files in `./directory`). Note that these paths are relative to the
directory from which you execute `ruff`, and _not_ the directory of the `pyproject.toml`.
### Compatibility with Black
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
@@ -221,28 +244,28 @@ Add this `pyproject.toml` to the CPython directory:
[tool.ruff]
line-length = 88
exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/test_fstring.py",
"Lib/test/test_grammar.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_source_encoding.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
"Tools/test2to3/setup.py",
"Tools/test2to3/test/test_foo.py",
"Tools/test2to3/test2to3/hello.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/bom.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/crlf.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/different_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/false_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/py2_test_grammar.py",
"./resources/test/cpython/Lib/test/bad_coding2.py",
"./resources/test/cpython/Lib/test/badsyntax_3131.py",
"./resources/test/cpython/Lib/test/badsyntax_pep3120.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_iso_8859_1.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_koi8_r.py",
"./resources/test/cpython/Lib/test/test_fstring.py",
"./resources/test/cpython/Lib/test/test_grammar.py",
"./resources/test/cpython/Lib/test/test_importlib/test_util.py",
"./resources/test/cpython/Lib/test/test_named_expressions.py",
"./resources/test/cpython/Lib/test/test_patma.py",
"./resources/test/cpython/Lib/test/test_source_encoding.py",
"./resources/test/cpython/Tools/c-analyzer/c_parser/parser/_delim.py",
"./resources/test/cpython/Tools/i18n/pygettext.py",
"./resources/test/cpython/Tools/test2to3/maintest.py",
"./resources/test/cpython/Tools/test2to3/setup.py",
"./resources/test/cpython/Tools/test2to3/test/test_foo.py",
"./resources/test/cpython/Tools/test2to3/test2to3/hello.py",
]
```

View File

@@ -77,3 +77,8 @@ class Ticket:
def set_status(self, status: Status):
self.status = status
def update_tomato():
print(TOMATO)
TOMATO = "cherry tomato"

View File

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

View File

@@ -0,0 +1,9 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

@@ -1,6 +1,10 @@
[tool.ruff]
line-length = 88
extend-exclude = ["excluded.py", "migrations"]
extend-exclude = [
"excluded.py",
"migrations",
"./resources/test/fixtures/directory/also_excluded.py",
]
select = [
"E402",
"E501",

View File

@@ -53,5 +53,7 @@ pub enum BindingKind {
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
pub used: Option<usize>,
/// Tuple of (scope index, location) indicating the scope and location at which the binding was
/// last used.
pub used: Option<(usize, Location)>,
}

View File

@@ -1,8 +1,10 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::Metadata;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
use anyhow::Result;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
@@ -83,6 +85,16 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
)
}
pub fn init() -> Result<()> {
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
if gitignore_path.exists() {
return Ok(());
}
create_dir_all(cache_dir())?;
let mut file = File::create(gitignore_path)?;
file.write_all(b"*").map_err(|e| e.into())
}
pub fn get(
path: &Path,
metadata: &Metadata,

View File

@@ -26,7 +26,7 @@ struct Checker<'a> {
locator: SourceCodeLocator<'a>,
settings: &'a Settings,
autofix: &'a fixer::Mode,
path: &'a str,
path: &'a Path,
// Computed checks.
checks: Vec<Check>,
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
@@ -55,7 +55,7 @@ impl<'a> Checker<'a> {
pub fn new(
settings: &'a Settings,
autofix: &'a fixer::Mode,
path: &'a str,
path: &'a Path,
content: &'a str,
) -> Checker<'a> {
Checker {
@@ -184,7 +184,7 @@ where
name.to_string(),
Binding {
kind: BindingKind::Assignment,
used: Some(global_scope_id),
used: Some((global_scope_id, stmt.location)),
location: stmt.location,
},
);
@@ -416,13 +416,14 @@ where
name,
Binding {
kind: BindingKind::FutureImportation,
used: Some(
used: Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found."))]
.id,
),
stmt.location,
)),
location: stmt.location,
},
);
@@ -1108,7 +1109,7 @@ impl<'a> Checker<'a> {
}
}
if let Some(binding) = scope.values.get_mut(id) {
binding.used = Some(scope_id);
binding.used = Some((scope_id, expr.location));
return;
}
@@ -1117,6 +1118,10 @@ impl<'a> Checker<'a> {
}
if self.settings.select.contains(&CheckCode::F821) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
@@ -1136,17 +1141,14 @@ impl<'a> Checker<'a> {
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function | ScopeKind::Module) {
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
expr.location,
));
if let Some(binding) = scope.values.get(id) {
if let Some((scope_id, location)) = binding.used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
location,
));
}
}
}
}
@@ -1240,12 +1242,12 @@ impl<'a> Checker<'a> {
}
}
fn check_deferred_string_annotations<'b>(&mut self, path: &str, allocator: &'b mut Vec<Expr>)
fn check_deferred_string_annotations<'b>(&mut self, allocator: &'b mut Vec<Expr>)
where
'b: 'a,
{
while let Some((location, expression)) = self.deferred_string_annotations.pop() {
if let Ok(mut expr) = parser::parse_expression(expression, path) {
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, location);
allocator.push(expr);
} else if self.settings.select.contains(&CheckCode::F722) {
@@ -1328,7 +1330,7 @@ impl<'a> Checker<'a> {
});
if self.settings.select.contains(&CheckCode::F822)
&& !Path::new(self.path).ends_with("__init__.py")
&& !self.path.ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(names) = all_names {
@@ -1374,7 +1376,7 @@ pub fn check_ast(
content: &str,
settings: &Settings,
autofix: &fixer::Mode,
path: &str,
path: &Path,
) -> Vec<Check> {
let mut checker = Checker::new(settings, autofix, path, content);
checker.push_scope(Scope::new(ScopeKind::Module));
@@ -1391,7 +1393,7 @@ pub fn check_ast(
checker.check_deferred_assignments();
checker.check_deferred_annotations();
let mut allocator = vec![];
checker.check_deferred_string_annotations(path, &mut allocator);
checker.check_deferred_string_annotations(&mut allocator);
// Reset the scope to module-level, and check all consumed scopes.
checker.scope_stack = vec![GLOBAL_SCOPE_INDEX];

View File

@@ -1,3 +1,4 @@
use std::env;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
@@ -5,9 +6,11 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use log::debug;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool {
// Check the basename.
if let Some(file_name) = path.file_name() {
if let Some(file_name) = file_name.to_str() {
for pattern in exclude {
@@ -15,13 +18,19 @@ fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool {
return true;
}
}
false
} else {
false
}
} else {
false
}
// Check the complete path.
if let Some(file_name) = path.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
return true;
}
}
}
false
}
fn is_included(path: &Path) -> bool {
@@ -34,7 +43,7 @@ pub fn iter_python_files<'a>(
exclude: &'a [Pattern],
extend_exclude: &'a [Pattern],
) -> impl Iterator<Item = DirEntry> + 'a {
WalkDir::new(path)
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(|entry| {
@@ -60,6 +69,20 @@ pub fn iter_python_files<'a>(
})
}
pub fn normalize_path(path: &PathBuf) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.clone();
}
if let Ok(path) = path.absolutize() {
if let Ok(root) = env::current_dir() {
if let Ok(path) = path.strip_prefix(root) {
return Path::new(".").join(path);
}
}
}
path.clone()
}
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
@@ -105,6 +128,18 @@ mod tests {
let exclude = vec![Pattern::new("baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("foo/bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("foo/bar/*.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz").unwrap()];
assert!(!is_excluded(path, &exclude));

View File

@@ -2,7 +2,7 @@ extern crate core;
mod ast;
mod autofix;
mod cache;
pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
@@ -10,6 +10,7 @@ pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
pub mod printer;
mod pyproject;
mod python;
pub mod settings;

View File

@@ -13,10 +13,12 @@ use crate::message::Message;
use crate::settings::Settings;
use crate::{cache, fs};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
// Read the file from disk.
let contents = fs::read_file(path)?;
fn check_path(
path: &Path,
contents: &str,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
@@ -26,13 +28,12 @@ fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
let path = path.to_string_lossy();
let python_ast = parser::parse_program(&contents, &path)?;
checks.extend(check_ast(&python_ast, &contents, settings, autofix, &path));
let python_ast = parser::parse_program(contents, "<filename>")?;
checks.extend(check_ast(&python_ast, contents, settings, autofix, path));
}
// Run the lines-based checks.
check_lines(&mut checks, &contents, settings);
check_lines(&mut checks, contents, settings);
Ok(checks)
}
@@ -55,7 +56,7 @@ pub fn lint_path(
let contents = fs::read_file(path)?;
// Generate checks.
let mut checks = check_path(path, settings, autofix)?;
let mut checks = check_path(path, &contents, settings, autofix)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
@@ -85,10 +86,20 @@ mod tests {
use anyhow::Result;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::linter::check_path;
use crate::checks::{Check, CheckCode};
use crate::fs;
use crate::linter;
use crate::settings;
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
linter::check_path(path, &contents, settings, autofix)
}
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
@@ -752,6 +763,23 @@ mod tests {
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821, CheckCode::F822]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(

View File

@@ -12,12 +12,14 @@ use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::settings::Settings;
use ::ruff::tell_user;
@@ -60,6 +62,9 @@ struct Cli {
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
extend_exclude: Vec<Pattern>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
}
#[cfg(feature = "update-informer")]
@@ -142,60 +147,15 @@ fn run_once(
Ok(messages)
}
fn report_once(messages: &[Message]) -> Result<()> {
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
messages.iter().partition(|message| message.fixed);
let num_fixable = outstanding
.iter()
.filter(|message| message.kind.fixable())
.count();
if !outstanding.is_empty() {
for message in &outstanding {
println!("{}", message);
}
println!();
}
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
);
} else {
println!("Found {} error(s).", outstanding.len());
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.");
}
Ok(())
}
fn report_continuously(messages: &[Message]) -> Result<()> {
tell_user!(
"Found {} error(s). Watching for file changes.",
messages.len(),
);
if !messages.is_empty() {
println!();
for message in messages {
println!("{}", message);
}
}
Ok(())
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
let mut settings = Settings::from_paths(&cli.files);
let mut printer = Printer::new(cli.format);
if !cli.select.is_empty() {
settings.select(cli.select);
}
@@ -209,18 +169,24 @@ fn inner_main() -> Result<ExitCode> {
settings.extend_exclude = cli.extend_exclude;
}
cache::init()?;
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.")
println!("Warning: --fix is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly.
clearscreen::clear()?;
printer.clear_screen()?;
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
printer.write_continuously(&messages)?;
}
// Configure the file watcher.
@@ -235,12 +201,12 @@ fn inner_main() -> Result<ExitCode> {
Ok(e) => {
if let Some(path) = e.path {
if path.to_string_lossy().ends_with(".py") {
clearscreen::clear()?;
printer.clear_screen()?;
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
printer.write_continuously(&messages)?;
}
}
}
@@ -251,7 +217,7 @@ fn inner_main() -> Result<ExitCode> {
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
report_once(&messages)?;
printer.write_once(&messages)?;
}
#[cfg(feature = "update-informer")]

80
src/printer.rs Normal file
View File

@@ -0,0 +1,80 @@
use colored::Colorize;
use anyhow::Result;
use clap::ValueEnum;
use crate::message::Message;
use crate::tell_user;
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
pub enum SerializationFormat {
Text,
Json,
}
pub struct Printer {
format: SerializationFormat,
}
impl Printer {
pub fn new(format: SerializationFormat) -> Self {
Self { format }
}
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
messages.iter().partition(|message| message.fixed);
let num_fixable = outstanding
.iter()
.filter(|message| message.kind.fixable())
.count();
match self.format {
SerializationFormat::Json => {
println!("{}", serde_json::to_string_pretty(&messages)?)
}
SerializationFormat::Text => {
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
)
} else {
println!("Found {} error(s).", outstanding.len())
}
for message in outstanding {
println!("{}", message)
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.")
}
}
}
Ok(())
}
pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> {
tell_user!(
"Found {} error(s). Watching for file changes.",
messages.len(),
);
if !messages.is_empty() {
println!();
for message in messages {
println!("{}", message)
}
}
Ok(())
}
pub fn clear_screen(&mut self) -> Result<()> {
clearscreen::clear()?;
Ok(())
}
}

View File

@@ -261,7 +261,8 @@ other-attribute = 1
exclude: None,
extend_exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("migrations").to_path_buf()
Path::new("migrations").to_path_buf(),
Path::new("./resources/test/fixtures/directory/also_excluded.py").to_path_buf()
]),
select: Some(vec![
CheckCode::E402,

View File

@@ -24,6 +24,7 @@ impl Hash for Settings {
}
}
}
static DEFAULT_EXCLUDE: Lazy<Vec<Pattern>> = Lazy::new(|| {
vec![
Pattern::new(".bzr").unwrap(),
@@ -34,6 +35,7 @@ static DEFAULT_EXCLUDE: Lazy<Vec<Pattern>> = Lazy::new(|| {
Pattern::new(".mypy_cache").unwrap(),
Pattern::new(".nox").unwrap(),
Pattern::new(".pants.d").unwrap(),
Pattern::new(".ruff_cache").unwrap(),
Pattern::new(".svn").unwrap(),
Pattern::new(".tox").unwrap(),
Pattern::new(".venv").unwrap(),
@@ -56,7 +58,7 @@ impl Settings {
.exclude
.map(|paths| {
paths
.into_iter()
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
@@ -67,7 +69,7 @@ impl Settings {
.extend_exclude
.map(|paths| {
paths
.into_iter()
.iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})

View File

@@ -32,4 +32,10 @@ expression: checks
row: 58
column: 5
fix: ~
- kind:
UndefinedName: TOMATO
location:
row: 83
column: 11
fix: ~

View File

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