mirror of
https://github.com/astral-sh/ruff
synced 2026-01-22 22:10:48 -05:00
269 lines
10 KiB
Rust
269 lines
10 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use rustpython_parser::ast::Location;
|
|
|
|
use crate::ast::types::Range;
|
|
use crate::autofix::fixer;
|
|
use crate::checks::{Check, CheckCode, CheckKind, Fix};
|
|
use crate::noqa;
|
|
use crate::noqa::Directive;
|
|
use crate::settings::Settings;
|
|
|
|
/// Whether the given line is too long and should be reported.
|
|
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
|
|
if length > limit {
|
|
let mut chunks = line.split_whitespace();
|
|
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
|
|
// Do not enforce the line length for commented lines with a single word
|
|
!(first == "#" && chunks.next().is_none())
|
|
} else {
|
|
// Single word / no printable chars - no way to make the line shorter
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn check_lines(
|
|
checks: &mut Vec<Check>,
|
|
contents: &str,
|
|
noqa_line_for: &[usize],
|
|
settings: &Settings,
|
|
autofix: &fixer::Mode,
|
|
) {
|
|
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
|
|
let enforce_noqa = settings.enabled.contains(&CheckCode::M001);
|
|
|
|
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
|
|
|
|
let mut line_checks = vec![];
|
|
let mut ignored = vec![];
|
|
|
|
let lines: Vec<&str> = contents.lines().collect();
|
|
for (lineno, line) in lines.iter().enumerate() {
|
|
// Grab the noqa (logical) line number for the current (physical) line.
|
|
// If there are newlines at the end of the file, they won't be represented in
|
|
// `noqa_line_for`, so fallback to the current line.
|
|
let noqa_lineno = noqa_line_for
|
|
.get(lineno)
|
|
.map(|lineno| lineno - 1)
|
|
.unwrap_or(lineno);
|
|
|
|
if enforce_noqa {
|
|
noqa_directives
|
|
.entry(noqa_lineno)
|
|
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
|
}
|
|
|
|
// Remove any ignored checks.
|
|
// TODO(charlie): Only validate checks for the current line.
|
|
for (index, check) in checks.iter().enumerate() {
|
|
if check.location.row() == lineno + 1 {
|
|
let noqa = noqa_directives
|
|
.entry(noqa_lineno)
|
|
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
|
|
|
match noqa {
|
|
(Directive::All(_, _), matches) => {
|
|
matches.push(check.kind.code().as_ref());
|
|
ignored.push(index)
|
|
}
|
|
(Directive::Codes(_, _, codes), matches) => {
|
|
if codes.contains(&check.kind.code().as_ref()) {
|
|
matches.push(check.kind.code().as_ref());
|
|
ignored.push(index);
|
|
}
|
|
}
|
|
(Directive::None, _) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enforce line length.
|
|
if enforce_line_too_long {
|
|
let line_length = line.chars().count();
|
|
if should_enforce_line_length(line, line_length, settings.line_length) {
|
|
let noqa = noqa_directives
|
|
.entry(noqa_lineno)
|
|
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
|
|
|
let check = Check::new(
|
|
CheckKind::LineTooLong(line_length, settings.line_length),
|
|
Range {
|
|
location: Location::new(lineno + 1, 1),
|
|
end_location: Location::new(lineno + 1, line_length + 1),
|
|
},
|
|
);
|
|
|
|
match noqa {
|
|
(Directive::All(_, _), matches) => {
|
|
matches.push(check.kind.code().as_ref());
|
|
}
|
|
(Directive::Codes(_, _, codes), matches) => {
|
|
if codes.contains(&check.kind.code().as_ref()) {
|
|
matches.push(check.kind.code().as_ref());
|
|
} else {
|
|
line_checks.push(check);
|
|
}
|
|
}
|
|
(Directive::None, _) => line_checks.push(check),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enforce newlines at end of files.
|
|
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
|
|
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't want to
|
|
// raise W292 anyway).
|
|
if let Some(line) = lines.last() {
|
|
let lineno = lines.len() - 1;
|
|
let noqa_lineno = noqa_line_for
|
|
.get(lineno)
|
|
.map(|lineno| lineno - 1)
|
|
.unwrap_or(lineno);
|
|
|
|
let noqa = noqa_directives
|
|
.entry(noqa_lineno)
|
|
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
|
|
|
let check = Check::new(
|
|
CheckKind::NoNewLineAtEndOfFile,
|
|
Range {
|
|
location: Location::new(lines.len(), line.len() + 1),
|
|
end_location: Location::new(lines.len(), line.len() + 1),
|
|
},
|
|
);
|
|
|
|
match noqa {
|
|
(Directive::All(_, _), matches) => {
|
|
matches.push(check.kind.code().as_ref());
|
|
}
|
|
(Directive::Codes(_, _, codes), matches) => {
|
|
if codes.contains(&check.kind.code().as_ref()) {
|
|
matches.push(check.kind.code().as_ref());
|
|
} else {
|
|
line_checks.push(check);
|
|
}
|
|
}
|
|
(Directive::None, _) => line_checks.push(check),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enforce that the noqa directive was actually used.
|
|
if enforce_noqa {
|
|
for (row, (directive, matches)) in noqa_directives {
|
|
match directive {
|
|
Directive::All(start, end) => {
|
|
if matches.is_empty() {
|
|
let mut check = Check::new(
|
|
CheckKind::UnusedNOQA(None),
|
|
Range {
|
|
location: Location::new(row + 1, start + 1),
|
|
end_location: Location::new(row + 1, end + 1),
|
|
},
|
|
);
|
|
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
|
check.amend(Fix {
|
|
content: "".to_string(),
|
|
location: Location::new(row + 1, start + 1),
|
|
end_location: Location::new(
|
|
row + 1,
|
|
lines[row].chars().count() + 1,
|
|
),
|
|
applied: false,
|
|
});
|
|
}
|
|
line_checks.push(check);
|
|
}
|
|
}
|
|
Directive::Codes(start, end, codes) => {
|
|
let mut invalid_codes = vec![];
|
|
let mut valid_codes = vec![];
|
|
for code in codes {
|
|
if !matches.contains(&code) {
|
|
invalid_codes.push(code);
|
|
} else {
|
|
valid_codes.push(code);
|
|
}
|
|
}
|
|
|
|
if !invalid_codes.is_empty() {
|
|
let mut check = Check::new(
|
|
CheckKind::UnusedNOQA(Some(invalid_codes.join(", "))),
|
|
Range {
|
|
location: Location::new(row + 1, start + 1),
|
|
end_location: Location::new(row + 1, end + 1),
|
|
},
|
|
);
|
|
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
|
if valid_codes.is_empty() {
|
|
check.amend(Fix {
|
|
content: "".to_string(),
|
|
location: Location::new(row + 1, start + 1),
|
|
end_location: Location::new(
|
|
row + 1,
|
|
lines[row].chars().count() + 1,
|
|
),
|
|
applied: false,
|
|
});
|
|
} else {
|
|
check.amend(Fix {
|
|
content: format!(" # noqa: {}", valid_codes.join(", ")),
|
|
location: Location::new(row + 1, start + 1),
|
|
end_location: Location::new(
|
|
row + 1,
|
|
lines[row].chars().count() + 1,
|
|
),
|
|
applied: false,
|
|
});
|
|
}
|
|
}
|
|
line_checks.push(check);
|
|
}
|
|
}
|
|
Directive::None => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
ignored.sort();
|
|
for index in ignored.iter().rev() {
|
|
checks.swap_remove(*index);
|
|
}
|
|
checks.extend(line_checks);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::autofix::fixer;
|
|
use crate::checks::{Check, CheckCode};
|
|
use crate::settings;
|
|
|
|
use super::check_lines;
|
|
|
|
#[test]
|
|
fn e501_non_ascii_char() {
|
|
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
|
|
let noqa_line_for: Vec<usize> = vec![1];
|
|
let check_with_max_line_length = |line_length: usize| {
|
|
let mut checks: Vec<Check> = vec![];
|
|
check_lines(
|
|
&mut checks,
|
|
line,
|
|
&noqa_line_for,
|
|
&settings::Settings {
|
|
line_length,
|
|
..settings::Settings::for_rule(CheckCode::E501)
|
|
},
|
|
&fixer::Mode::Generate,
|
|
);
|
|
return checks;
|
|
};
|
|
assert!(!check_with_max_line_length(6).is_empty());
|
|
assert!(check_with_max_line_length(7).is_empty());
|
|
}
|
|
}
|