ruff/fuzz/fuzz_targets/ruff_formatter_validity.rs
Dhruv Manilawala 72b6c26101 Simplify LinterResult, avoid cloning ParseError (#11903)
## Summary

Follow-up to #11902

This PR simplifies the `LinterResult` struct by avoiding the generic and
not store the `ParseError`.

This is possible because the callers already have access to the
`ParseError` via the `Parsed` output. This also means that we can
simplify the return type of `check_path` and avoid the generic `T` on
`LinterResult`.

## Test Plan

`cargo insta test`
2024-06-27 13:44:11 +02:00

96 lines
3.1 KiB
Rust

//! Fuzzer harness which actively tries to find testcases that cause Ruff to introduce errors into
//! the resulting file.
#![no_main]
use std::collections::HashMap;
use std::sync::OnceLock;
use libfuzzer_sys::{fuzz_target, Corpus};
use ruff_linter::linter::ParseSource;
use ruff_linter::settings::flags::Noqa;
use ruff_linter::settings::LinterSettings;
use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{format_module_source, PyFormatOptions};
use similar::TextDiff;
static SETTINGS: OnceLock<LinterSettings> = OnceLock::new();
fn do_fuzz(case: &[u8]) -> Corpus {
// throw away inputs which aren't utf-8
let Ok(code) = std::str::from_utf8(case) else {
return Corpus::Reject;
};
// the settings are immutable to test_snippet, so we avoid re-initialising here
let linter_settings = SETTINGS.get_or_init(LinterSettings::default);
let format_options = PyFormatOptions::default();
let linter_result = ruff_linter::linter::lint_only(
"fuzzed-source.py".as_ref(),
None,
linter_settings,
Noqa::Enabled,
&SourceKind::Python(code.to_string()),
PySourceType::Python,
ParseSource::None,
);
if linter_result.has_syntax_error {
return Corpus::Keep; // keep, but don't continue
}
let mut warnings = HashMap::new();
for msg in &linter_result.messages {
let count: &mut usize = warnings.entry(msg.name()).or_default();
*count += 1;
}
// format the code once
if let Ok(formatted) = format_module_source(code, format_options.clone()) {
let formatted = formatted.as_code().to_string();
let linter_result = ruff_linter::linter::lint_only(
"fuzzed-source.py".as_ref(),
None,
linter_settings,
Noqa::Enabled,
&SourceKind::Python(formatted.clone()),
PySourceType::Python,
ParseSource::None,
);
assert!(
!linter_result.has_syntax_error,
"formatter introduced a parse error"
);
for msg in &linter_result.messages {
if let Some(count) = warnings.get_mut(msg.name()) {
if let Some(decremented) = count.checked_sub(1) {
*count = decremented;
} else {
panic!(
"formatter introduced additional linter warning: {msg:?}\ndiff: {}",
TextDiff::from_lines(code, &formatted)
.unified_diff()
.header("Unformatted", "Formatted")
);
}
} else {
panic!(
"formatter introduced new linter warning that was not previously present: {msg:?}\ndiff: {}",
TextDiff::from_lines(code, &formatted)
.unified_diff()
.header("Unformatted", "Formatted")
);
}
}
}
Corpus::Keep
}
fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) });