use super::{Rule, Scope, Violation};
use crate::column::{space_indent, ColumnIndex};
use crate::nodes::{child_token, is_block_scalar, is_quoted_scalar};
use crate::yaml::SyntaxNode;
pub(super) fn check_escape_sequences(node: &SyntaxNode, violations: &mut Vec<Violation>) {
let first_char = node.first_token().and_then(|t| t.text().chars().next());
if first_char != Some('"') {
return;
}
let text = node.text().to_string();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&next) = chars.peek() {
let valid_escapes = [
'0', 'a', 'b', 't', 'n', 'v', 'f', 'r', 'e', ' ', '"', '/', '\\', 'N', '_',
'L', 'P', 'x', 'u', 'U', '\n', '\r', '\t',
];
if !valid_escapes.contains(&next) {
violations.push(Violation::error_at(
Rule::InvalidEscape,
node.text_range(),
format!("Invalid escape sequence: \\{next}"),
));
return; }
chars.next();
let digits = match next {
'x' => 2,
'u' => 4,
'U' => 8,
_ => continue,
};
let code: String = chars.by_ref().take(digits).collect();
if code.len() != digits || !code.chars().all(|c| c.is_ascii_hexdigit()) {
violations.push(Violation::error_at(
Rule::InvalidEscape,
node.text_range(),
format!("Invalid escape sequence: \\{next}{code}"),
));
return;
}
}
}
}
}
pub(super) fn check_block_scalar_indicator(node: &SyntaxNode, violations: &mut Vec<Violation>) {
if !is_block_scalar(node) {
return;
}
let mut found_indicator = false;
let mut found_newline = false;
let mut in_header = true;
let mut chomping_indicators = 0;
for child in node.children_with_tokens() {
if let rowan::NodeOrToken::Token(token) = child {
if matches!(
token.kind(),
crate::SyntaxKind::GREATER | crate::SyntaxKind::PIPE
) {
found_indicator = true;
continue;
}
if found_indicator && !found_newline {
match token.kind() {
crate::SyntaxKind::NEWLINE => {
found_newline = true;
}
crate::SyntaxKind::STRING
if token.text().chars().all(|c| matches!(c, '+' | '-'))
|| (token.text().len() == 2
&& token.text().chars().any(|c| c.is_ascii_digit())
&& token.text().chars().any(|c| matches!(c, '+' | '-'))) => {}
crate::SyntaxKind::STRING => {
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Block scalar content cannot appear on same line as indicator",
));
return;
}
crate::SyntaxKind::INT
if !matches!(
token.text().trim_matches(['+', '-']),
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
) =>
{
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Block scalar indentation indicator must be a digit 1-9",
));
return;
}
crate::SyntaxKind::WHITESPACE | crate::SyntaxKind::COMMENT => {
in_header = false;
}
_ => {}
}
if in_header
&& matches!(
token.kind(),
crate::SyntaxKind::PLUS
| crate::SyntaxKind::STRING
| crate::SyntaxKind::INT
)
{
chomping_indicators += token.text().matches(['+', '-']).count();
if chomping_indicators > 1 {
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Block scalar header has more than one chomping indicator",
));
return;
}
}
}
}
}
}
pub(super) fn check_block_scalar_header_indent(
node: &SyntaxNode,
columns: &ColumnIndex,
scope: Scope,
violations: &mut Vec<Violation>,
) {
if !is_block_scalar(node) {
return;
}
let Some(header) = node.first_token() else {
return;
};
let Some(min_indent) = scope.continuation_indent else {
return;
};
if columns.column(&header) < min_indent {
violations.push(Violation::error_at(
Rule::Other,
header.text_range(),
"Block scalar header must be indented past its parent node",
));
}
}
pub(super) fn check_block_scalar_content_indent(
node: &SyntaxNode,
violations: &mut Vec<Violation>,
) {
if !is_block_scalar(node) {
return;
}
if crate::nodes::scalar_node::block_scalar_indicators(node)
.1
.is_some()
{
return;
}
let mut past_first_newline = false;
let mut max_blank_indent = 0usize;
let mut body_indent: Option<usize> = None;
let mut pending_indent: Option<rowan::SyntaxToken<crate::Lang>> = None;
for child in node.children_with_tokens() {
let Some(token) = child.as_token().cloned() else {
continue;
};
if !past_first_newline {
if token.kind() == crate::SyntaxKind::NEWLINE {
past_first_newline = true;
}
continue;
}
match token.kind() {
crate::SyntaxKind::INDENT => {
pending_indent = Some(token);
}
crate::SyntaxKind::NEWLINE => {
if let Some(ind) = pending_indent.take() {
if !ind.text().contains('\t') {
max_blank_indent = max_blank_indent.max(space_indent(ind.text()));
}
}
}
_ => {
if let Some(ind) = pending_indent.take() {
if body_indent.is_none() {
let indent = space_indent(ind.text());
body_indent = Some(indent);
if indent < max_blank_indent {
violations.push(Violation::error_at(Rule::Other, ind.text_range(), format!( "Block scalar content under-indented ({indent} spaces) relative to preceding blank line ({max_blank_indent} spaces)" )));
return;
}
}
}
}
}
}
}
pub(super) fn check_legacy_yaml_1_1(node: &SyntaxNode, violations: &mut Vec<Violation>) {
let Some(token) = node.children_with_tokens().find_map(|c| {
c.into_token().filter(|t| {
matches!(
t.kind(),
crate::SyntaxKind::INT
| crate::SyntaxKind::FLOAT
| crate::SyntaxKind::STRING
| crate::SyntaxKind::BOOL
)
})
}) else {
return;
};
if is_quoted_scalar(&token) {
return;
}
if let Some(message) = legacy_yaml_1_1_reading(token.text()) {
violations.push(Violation::warning_at(
Rule::LegacyYaml11,
token.text_range(),
message,
));
}
}
fn legacy_yaml_1_1_reading(text: &str) -> Option<String> {
if crate::ScalarValue::is_legacy_octal(text) {
let sign = if text.starts_with('-') { "-" } else { "" };
let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
let as_octal = crate::scalar::digits_to_decimal(digits, 8);
let as_decimal = crate::scalar::digits_to_decimal(digits, 10);
return Some(format!(
"`{text}` is YAML 1.1 octal, read as {sign}{as_octal}; YAML 1.2 reads it as {sign}{as_decimal}. Write `{sign}{as_octal}` to keep the octal meaning",
));
}
if let Some((after, kind)) = crate::migrate::yaml_1_1_number(text) {
let (value, suggestion, kind) = if kind == crate::SyntaxKind::FLOAT {
(after.clone(), after, "float")
} else if let Some(hex) = after.strip_prefix("0x") {
(crate::scalar::digits_to_decimal(hex, 16), after, "integer")
} else if let Some(octal) = after.strip_prefix("0o") {
let value = crate::scalar::digits_to_decimal(octal, 8);
(value.clone(), value, "integer")
} else {
(after.clone(), after, "integer")
};
return Some(format!(
"`{text}` is the {kind} {value} in YAML 1.1; YAML 1.2 reads it as a string. Write `{suggestion}` to keep the {kind} meaning",
));
}
if crate::ScalarValue::is_leading_zero_decimal(text) {
let minus = if text.starts_with('-') { "-" } else { "" };
let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
let as_decimal = crate::scalar::digits_to_decimal(digits, 10);
return Some(format!(
"`{text}` is a string in YAML 1.1; YAML 1.2 reads it as the integer {minus}{as_decimal}. Write `'{text}'` to keep the string meaning",
));
}
if crate::migrate::yaml_1_1_string(text) {
let reading = match text.strip_prefix("0o") {
Some(octal) => {
format!("the integer {}", crate::scalar::digits_to_decimal(octal, 8))
}
None => "a float".to_string(),
};
return Some(format!(
"`{text}` is a string in YAML 1.1; YAML 1.2 reads it as {reading}. Write `'{text}'` to keep the string meaning",
));
}
if matches!(
text,
"yes" | "no" | "on" | "off" | "Yes" | "No" | "On" | "Off" | "YES" | "NO" | "ON" | "OFF"
) {
return Some(format!("`{text}` is the string \"{text}\" in YAML 1.2, but a boolean to a YAML 1.1 reader; quote it or write true/false to be unambiguous"));
}
None
}
pub(super) fn check_trailing_content_after_quoted(
node: &SyntaxNode,
violations: &mut Vec<Violation>,
) {
let mut found_quoted = false;
let mut found_quote_end = false;
let mut found_newline = false;
for child in node.children_with_tokens() {
if let rowan::NodeOrToken::Token(token) = child {
match token.kind() {
crate::SyntaxKind::STRING => {
let text = token.text();
if !found_quoted && is_quoted_scalar(&token) {
found_quoted = true;
if text.len() > 1 && (text.ends_with('"') || text.ends_with('\'')) {
found_quote_end = true;
}
} else if found_quoted && !found_quote_end {
if text.ends_with('"') || text.ends_with('\'') {
found_quote_end = true;
}
} else if found_quote_end && !found_newline {
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Trailing content after quoted string",
));
return;
}
}
crate::SyntaxKind::NEWLINE => {
found_newline = true;
}
crate::SyntaxKind::WHITESPACE
| crate::SyntaxKind::INDENT
| crate::SyntaxKind::COMMENT => {}
_ if found_quote_end && !found_newline => {
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Trailing content after quoted string",
));
return;
}
_ => {}
}
}
}
}
pub(super) fn check_colon_in_plain_scalar(node: &SyntaxNode, violations: &mut Vec<Violation>) {
let Some(colon) = child_token(node, crate::SyntaxKind::COLON) else {
return;
};
if node.first_token().is_some_and(|t| is_quoted_scalar(&t)) || is_block_scalar(node) {
return;
}
let parent_is_value = node
.parent()
.is_some_and(|p| p.kind() == crate::SyntaxKind::VALUE);
if parent_is_value {
violations.push(Violation::error_at(
Rule::Other,
colon.text_range(),
"Plain scalar value cannot contain mapping syntax (colon)",
));
}
}
pub(super) fn check_plain_scalar_start(
node: &SyntaxNode,
in_flow: bool,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
let Some(first) = node.first_token() else {
return;
};
let text = first.text();
let unsafe_after_dash: &[char] = if in_flow {
&[' ', '\t', '\r', '\n', ',', '[', ']', '{', '}']
} else {
&[' ', '\t', '\r', '\n']
};
let invalid = text.starts_with([',', ']', '}'])
|| (first.kind() == SyntaxKind::STRING && text.starts_with(['%', '@', '`']))
|| text.strip_prefix('-').is_some_and(|rest| {
(rest.is_empty() && first.next_token().map(|t| t.kind()) != Some(SyntaxKind::COLON))
|| rest.starts_with(unsafe_after_dash)
});
if invalid {
let start = text.chars().next().unwrap_or_default();
violations.push(Violation::error_at(
Rule::Other,
first.text_range(),
format!("Plain scalar cannot start with {start:?}"),
));
}
}
pub(super) fn check_flow_indicator_in_plain_scalar(
node: &SyntaxNode,
in_flow: bool,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
if !in_flow {
return;
}
if let Some(open) = node
.children_with_tokens()
.filter_map(|c| c.into_token())
.find(|t| matches!(t.kind(), SyntaxKind::LEFT_BRACE | SyntaxKind::LEFT_BRACKET))
{
violations.push(Violation::error_at(
Rule::Other,
open.text_range(),
format!(
"Plain scalar in a flow collection cannot contain {:?}",
open.text()
),
));
}
}
pub(super) fn check_quoted_string_indentation(
node: &SyntaxNode,
scope: Scope,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
if node.kind() != SyntaxKind::SCALAR {
return;
}
let text = node.text().to_string();
if !text.starts_with('"') && !text.starts_with('\'') {
return; }
if !text.contains(['\n', '\r']) {
return; }
if text.split(['\n', '\r']).skip(1).any(|line| {
(line.starts_with("---") || line.starts_with("..."))
&& line[3..]
.chars()
.next()
.map_or(true, |c| matches!(c, ' ' | '\t'))
}) {
violations.push(Violation::error_at(
Rule::InvalidDocumentMarker,
node.text_range(),
"Document marker inside a quoted scalar",
));
return;
}
let Some(required) = scope.continuation_indent else {
return;
};
let lines: Vec<&str> = text.split(['\n', '\r']).collect();
if lines.len() > 1 {
for (i, line) in lines.iter().enumerate().skip(1) {
if i == lines.len() - 1 && line.trim().is_empty() {
continue;
}
let leading_spaces = space_indent(line);
if leading_spaces < required && !line.trim().is_empty() && !line.starts_with('\t') {
violations.push(Violation::error_at(
Rule::InvalidIndentation,
node.text_range(),
"Wrong indented multiline quoted scalar",
));
return;
}
}
}
}