use libyaml_safer::{Scanner, TokenData};
use super::super::Problem;
use super::super::config::RuleConf;
use super::super::parser::{Stream, is_python_whitespace};
use super::{Alt, Check, Default, OptKind, OptSpec, RuleSpec};
fn check_empty_lines(conf: &RuleConf, stream: &Stream<'_>, idx: usize, out: &mut Vec<Problem>) {
let line = stream.lines[idx];
let buf = stream.bytes();
if !(line.start == line.end && line.end < buf.len()) {
return;
}
if buf[line.end..].starts_with(b"\n\n") || buf[line.end..].starts_with(b"\r\n\r\n") {
return;
}
let mut blank_lines = 0;
let mut start = line.start;
while start >= 2 && &buf[start - 2..start] == b"\r\n" {
blank_lines += 1;
start -= 2;
}
while start >= 1 && buf[start - 1] == b'\n' {
blank_lines += 1;
start -= 1;
}
let at_start = start == 0;
if at_start {
blank_lines += 1; }
let at_end = (line.end + 1 == buf.len() && buf[line.end] == b'\n')
|| (line.end + 2 == buf.len() && &buf[line.end..] == b"\r\n");
if at_end && line.end == 0 {
return;
}
let max = if at_end {
conf.int_opt("max-end")
} else if at_start {
conf.int_opt("max-start")
} else {
conf.int_opt("max")
};
if i64::from(blank_lines) > max {
out.push(Problem::new(
line.number,
1,
format!("too many blank lines ({blank_lines} > {max})"),
));
}
}
pub const EMPTY_LINES: RuleSpec = RuleSpec {
id: "empty-lines",
options: &[
OptSpec {
name: "max",
kind: OptKind::Int,
default: Default::Int(2),
},
OptSpec {
name: "max-start",
kind: OptKind::Int,
default: Default::Int(0),
},
OptSpec {
name: "max-end",
kind: OptKind::Int,
default: Default::Int(0),
},
],
validate: None,
check: Check::Line(check_empty_lines),
};
fn check_inline_mapping(content: &str) -> bool {
let mut scanner: Scanner<&mut &[u8]> = Scanner::new();
let mut input = content.as_bytes();
scanner.set_input_string(&mut input);
let mut in_mapping = false;
loop {
let Ok(token) = Scanner::scan(&mut scanner) else {
return false;
};
match token.data {
TokenData::StreamEnd => return false,
TokenData::BlockMappingStart => in_mapping = true,
TokenData::Value if in_mapping => {
let Ok(next) = Scanner::scan(&mut scanner) else {
return false;
};
match next.data {
TokenData::Scalar { .. } => {
let column = next.start_mark.column as usize;
return !content.chars().skip(column).any(|c| c == ' ');
}
TokenData::StreamEnd => return false,
_ => {}
}
}
_ => {}
}
}
}
fn check_line_length(conf: &RuleConf, stream: &Stream<'_>, idx: usize, out: &mut Vec<Problem>) {
let line = stream.lines[idx];
let buf = stream.bytes();
let max_length = conf.int_opt("max");
let length = line.content(stream.buffer).chars().count() as i64;
if length <= max_length {
return;
}
let allow_inline = conf.bool_opt("allow-non-breakable-inline-mappings");
let allow_words = conf.bool_opt("allow-non-breakable-words") || allow_inline;
if allow_words {
let mut start = line.start;
while start < line.end && buf[start] == b' ' {
start += 1;
}
if start != line.end {
if buf[start] == b'#' {
while start < line.end && buf[start] == b'#' {
start += 1;
}
start += 1;
} else if buf[start] == b'-' {
start += 2;
}
let start = start.min(line.end);
if !buf[start..line.end].contains(&b' ') {
return;
}
if allow_inline && check_inline_mapping(line.content(stream.buffer)) {
return;
}
}
}
out.push(Problem::new(
line.number,
(max_length + 1) as usize,
format!("line too long ({length} > {max_length} characters)"),
));
}
pub const LINE_LENGTH: RuleSpec = RuleSpec {
id: "line-length",
options: &[
OptSpec {
name: "max",
kind: OptKind::Int,
default: Default::Int(80),
},
OptSpec {
name: "allow-non-breakable-words",
kind: OptKind::Bool,
default: Default::Bool(true),
},
OptSpec {
name: "allow-non-breakable-inline-mappings",
kind: OptKind::Bool,
default: Default::Bool(false),
},
],
validate: None,
check: Check::Line(check_line_length),
};
fn check_new_line_at_end_of_file(
_: &RuleConf,
stream: &Stream<'_>,
idx: usize,
out: &mut Vec<Problem>,
) {
let line = stream.lines[idx];
if line.end == stream.buffer.len() && line.end > line.start {
out.push(Problem::new(
line.number,
line.content(stream.buffer).chars().count() + 1,
"no new line character at the end of file",
));
}
}
pub const NEW_LINE_AT_END_OF_FILE: RuleSpec = RuleSpec {
id: "new-line-at-end-of-file",
options: &[],
validate: None,
check: Check::Line(check_new_line_at_end_of_file),
};
const NEW_LINE_TYPES: &[Alt] = &[Alt::Str("unix"), Alt::Str("dos"), Alt::Str("platform")];
fn check_new_lines(conf: &RuleConf, stream: &Stream<'_>, idx: usize, out: &mut Vec<Problem>) {
let line = stream.lines[idx];
let buf = stream.bytes();
let newline: &[u8] = match conf.str_opt("type") {
"dos" => b"\r\n",
_ => b"\n",
};
if line.start == 0 && buf.len() > line.end && !buf[line.end..].starts_with(newline) {
let shown = if newline == b"\n" { "\\n" } else { "\\r\\n" };
out.push(Problem::new(
1,
line.content(stream.buffer).chars().count() + 1,
format!("wrong new line character: expected {shown}"),
));
}
}
pub const NEW_LINES: RuleSpec = RuleSpec {
id: "new-lines",
options: &[OptSpec {
name: "type",
kind: OptKind::OneOf(NEW_LINE_TYPES),
default: Default::Str("unix"),
}],
validate: None,
check: Check::Line(check_new_lines),
};
fn check_trailing_spaces(_: &RuleConf, stream: &Stream<'_>, idx: usize, out: &mut Vec<Problem>) {
let line = stream.lines[idx];
if line.end == 0 {
return;
}
let buf = stream.bytes();
let mut pos = line.end;
while pos > line.start && is_python_whitespace(buf[pos - 1]) {
pos -= 1;
}
if pos != line.end && matches!(buf[pos], b' ' | b'\t') {
let column = stream.buffer[line.start..pos].chars().count() + 1;
out.push(Problem::new(line.number, column, "trailing spaces"));
}
}
pub const TRAILING_SPACES: RuleSpec = RuleSpec {
id: "trailing-spaces",
options: &[],
validate: None,
check: Check::Line(check_trailing_spaces),
};