Include fixes in JSON API output (#988)

This commit is contained in:
Charlie Marsh 2022-12-01 16:30:56 -05:00 committed by GitHub
parent af40e64d6c
commit 46f5053c73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 19 deletions

View File

@ -37,6 +37,7 @@ use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher}; use notify::{raw_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use rayon::prelude::*; use rayon::prelude::*;
use ruff::autofix::fixer;
use rustpython_ast::Location; use rustpython_ast::Location;
use walkdir::DirEntry; use walkdir::DirEntry;
@ -60,14 +61,23 @@ fn read_from_stdin() -> Result<String> {
Ok(buffer) Ok(buffer)
} }
fn run_once_stdin(settings: &Settings, filename: &Path, autofix: bool) -> Result<Diagnostics> { fn run_once_stdin(
settings: &Settings,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?; let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, &stdin, settings, &autofix.into())?; let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable(); diagnostics.messages.sort_unstable();
Ok(diagnostics) Ok(diagnostics)
} }
fn run_once(files: &[PathBuf], settings: &Settings, cache: bool, autofix: bool) -> Diagnostics { fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: &fixer::Mode,
) -> Diagnostics {
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files let paths: Vec<Result<DirEntry, walkdir::Error>> = files
@ -83,7 +93,7 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool, autofix: bool)
match entry { match entry {
Ok(entry) => { Ok(entry) => {
let path = entry.path(); let path = entry.path();
lint_path(path, settings, &cache.into(), &autofix.into()) lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string())) .map_err(|e| (Some(path.to_owned()), e.to_string()))
} }
Err(e) => Err(( Err(e) => Err((
@ -99,6 +109,7 @@ fn run_once(files: &[PathBuf], settings: &Settings, cache: bool, autofix: bool)
kind: CheckKind::IOError(message), kind: CheckKind::IOError(message),
location: Location::default(), location: Location::default(),
end_location: Location::default(), end_location: Location::default(),
fix: None,
filename: path.to_string_lossy().to_string(), filename: path.to_string_lossy().to_string(),
source: None, source: None,
}]) }])
@ -266,7 +277,13 @@ fn inner_main() -> Result<ExitCode> {
} }
// Extract settings for internal use. // Extract settings for internal use.
let fix_enabled: bool = configuration.fix; let autofix = if configuration.fix {
fixer::Mode::Apply
} else if matches!(configuration.format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let settings = Settings::from_configuration(configuration, project_root.as_ref())?; let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
// Now that we've inferred the appropriate log level, add some debug // Now that we've inferred the appropriate log level, add some debug
@ -299,10 +316,7 @@ fn inner_main() -> Result<ExitCode> {
let printer = Printer::new(&settings.format, &log_level); let printer = Printer::new(&settings.format, &log_level);
if cli.watch { if cli.watch {
if settings.format != SerializationFormat::Text { if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
eprintln!("Warning: --format 'text' is used in watch mode.");
}
if fix_enabled {
eprintln!("Warning: --fix is not enabled in watch mode."); eprintln!("Warning: --fix is not enabled in watch mode.");
} }
if cli.add_noqa { if cli.add_noqa {
@ -311,12 +325,15 @@ fn inner_main() -> Result<ExitCode> {
if cli.autoformat { if cli.autoformat {
eprintln!("Warning: --autoformat is not enabled in watch mode."); eprintln!("Warning: --autoformat is not enabled in watch mode.");
} }
if settings.format != SerializationFormat::Text {
eprintln!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly. // Perform an initial run instantly.
printer.clear_screen()?; printer.clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n"); printer.write_to_user("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, cache_enabled, false); let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
printer.write_continuously(&messages)?; printer.write_continuously(&messages)?;
// Configure the file watcher. // Configure the file watcher.
@ -334,7 +351,8 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?; printer.clear_screen()?;
printer.write_to_user("File change detected...\n"); printer.write_to_user("File change detected...\n");
let messages = run_once(&cli.files, &settings, cache_enabled, false); let messages =
run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
printer.write_continuously(&messages)?; printer.write_continuously(&messages)?;
} }
} }
@ -359,15 +377,15 @@ fn inner_main() -> Result<ExitCode> {
let diagnostics = if is_stdin { let diagnostics = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename); let path = Path::new(&filename);
run_once_stdin(&settings, path, fix_enabled)? run_once_stdin(&settings, path, &autofix)?
} else { } else {
run_once(&cli.files, &settings, cache_enabled, fix_enabled) run_once(&cli.files, &settings, cache_enabled, &autofix)
}; };
// Always try to print violations (the printer itself may suppress output), // Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed // unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout). // source code goes to stdout).
if !(is_stdin && fix_enabled) { if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics)?; printer.write_once(&diagnostics)?;
} }

View File

@ -4,6 +4,7 @@ use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::ast::types::Range; use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{Check, CheckKind}; use crate::checks::{Check, CheckKind};
use crate::source_code_locator::SourceCodeLocator; use crate::source_code_locator::SourceCodeLocator;
@ -12,6 +13,7 @@ pub struct Message {
pub kind: CheckKind, pub kind: CheckKind,
pub location: Location, pub location: Location,
pub end_location: Location, pub end_location: Location,
pub fix: Option<Fix>,
pub filename: String, pub filename: String,
pub source: Option<Source>, pub source: Option<Source>,
} }
@ -22,6 +24,7 @@ impl Message {
kind: check.kind, kind: check.kind,
location: Location::new(check.location.row(), check.location.column() + 1), location: Location::new(check.location.row(), check.location.column() + 1),
end_location: Location::new(check.end_location.row(), check.end_location.column() + 1), end_location: Location::new(check.end_location.row(), check.end_location.column() + 1),
fix: check.fix,
filename, filename,
source, source,
} }

View File

@ -9,7 +9,8 @@ use itertools::iterate;
use rustpython_parser::ast::Location; use rustpython_parser::ast::Location;
use serde::Serialize; use serde::Serialize;
use crate::checks::{CheckCode, CheckKind}; use crate::autofix::Fix;
use crate::checks::CheckCode;
use crate::fs::relativize_path; use crate::fs::relativize_path;
use crate::linter::Diagnostics; use crate::linter::Diagnostics;
use crate::logging::LogLevel; use crate::logging::LogLevel;
@ -19,9 +20,9 @@ use crate::tell_user;
#[derive(Serialize)] #[derive(Serialize)]
struct ExpandedMessage<'a> { struct ExpandedMessage<'a> {
kind: &'a CheckKind,
code: &'a CheckCode, code: &'a CheckCode,
message: String, message: String,
fix: Option<&'a Fix>,
location: Location, location: Location,
end_location: Location, end_location: Location,
filename: &'a str, filename: &'a str,
@ -85,9 +86,9 @@ impl<'a> Printer<'a> {
.messages .messages
.iter() .iter()
.map(|message| ExpandedMessage { .map(|message| ExpandedMessage {
kind: &message.kind,
code: message.kind.code(), code: message.kind.code(),
message: message.kind.body(), message: message.kind.body(),
fix: message.fix.as_ref(),
location: message.location, location: message.location,
end_location: message.end_location, end_location: message.end_location,
filename: &message.filename, filename: &message.filename,

View File

@ -21,7 +21,11 @@ fn test_stdin_error() -> Result<()> {
.write_stdin("import os\n") .write_stdin("import os\n")
.assert() .assert()
.failure(); .failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("-:1:8: F401")); assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"Found 1 error(s).\n-:1:8: F401 `os` imported but unused\n1 potentially fixable with the \
--fix option.\n"
);
Ok(()) Ok(())
} }
@ -33,7 +37,31 @@ fn test_stdin_filename() -> Result<()> {
.write_stdin("import os\n") .write_stdin("import os\n")
.assert() .assert()
.failure(); .failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("F401.py:1:8: F401")); assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"Found 1 error(s).\nF401.py:1:8: F401 `os` imported but unused\n1 potentially fixable \
with the --fix option.\n"
);
Ok(())
}
#[test]
fn test_stdin_json() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;
let output = cmd
.args(["-", "--format", "json", "--stdin-filename", "F401.py"])
.write_stdin("import os\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"[\n {\n \"code\": \"F401\",\n \"message\": \"`os` imported but unused\",\n \
\"fix\": {\n \"content\": \"\",\n \"location\": {\n \"row\": 1,\n \
\"column\": 0\n },\n \"end_location\": {\n \"row\": 2,\n \
\"column\": 0\n }\n },\n \"location\": {\n \"row\": 1,\n \
\"column\": 8\n },\n \"end_location\": {\n \"row\": 1,\n \"column\": \
10\n },\n \"filename\": \"F401.py\"\n }\n]\n"
);
Ok(()) Ok(())
} }