use super::{Rule, Violation};
use crate::column::ColumnIndex;
use crate::nodes::{
child_token, has_child_token, is_block_scalar, is_quoted_scalar, starts_a_flow_collection,
};
use crate::yaml::SyntaxNode;
pub(super) fn check_tab_usage(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::SyntaxKind;
let tab = match node.kind() {
SyntaxKind::SCALAR => {
block_scalar_tab_indent(node).or_else(|| quoted_scalar_tab_indent(node))
}
SyntaxKind::DOCUMENT
| SyntaxKind::KEY
| SyntaxKind::VALUE
| SyntaxKind::MAPPING
| SyntaxKind::SEQUENCE
| SyntaxKind::MAPPING_ENTRY
| SyntaxKind::SEQUENCE_ENTRY => block_tab_indent(node),
_ => None,
};
if let Some(token) = tab {
violations.push(Violation::error_at(
Rule::InvalidTabUsage,
token.text_range(),
"Tabs are not allowed for indentation in YAML",
));
}
}
pub(super) fn check_block_mapping_entries_on_same_line(
node: &SyntaxNode,
violations: &mut Vec<Violation>,
) {
let first_token = node.first_token();
let is_flow_mapping = first_token.as_ref().is_some_and(|t| t.text() == "{");
if is_flow_mapping {
return; }
let entries: Vec<SyntaxNode> = node
.children()
.filter(|c| c.kind() == crate::SyntaxKind::MAPPING_ENTRY)
.collect();
if let Some(pair) = entries.windows(2).find(|w| !newline_between(&w[0], &w[1])) {
violations.push(Violation::error_at(
Rule::Other,
pair[1].text_range(),
"Block mapping entries must be on separate lines",
));
}
}
fn newline_between(prev: &SyntaxNode, next: &SyntaxNode) -> bool {
let prev_ends_with_newline = prev
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.filter(|t| {
!t.text().is_empty()
&& !matches!(
t.kind(),
crate::SyntaxKind::INDENT | crate::SyntaxKind::WHITESPACE
)
})
.last()
.is_some_and(|t| t.kind() == crate::SyntaxKind::NEWLINE);
prev_ends_with_newline
|| prev
.siblings_with_tokens(rowan::Direction::Next)
.skip(1)
.take_while(|el| el.as_node() != Some(next))
.any(|el| el.kind() == crate::SyntaxKind::NEWLINE)
}
pub(super) fn check_mapping_entry_indentation(
node: &SyntaxNode,
columns: &ColumnIndex,
violations: &mut Vec<Violation>,
) {
if starts_a_flow_collection(node) {
return;
}
let expected_indent = expected_entry_indent(node, columns);
let mut seen_entry = false;
let mut pending: Option<rowan::SyntaxToken<crate::Lang>> = None;
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Token(t) if t.kind() == crate::SyntaxKind::INDENT => {
pending = Some(t);
}
rowan::NodeOrToken::Node(n) if n.kind() == crate::SyntaxKind::MAPPING_ENTRY => {
if let Some(t) = pending.take() {
if seen_entry && t.text() != expected_indent {
violations.push(Violation::error_at(
Rule::Other,
t.text_range(),
format!(
"Sibling block mapping entries have inconsistent indentation (expected {expected_indent:?}, found {:?})",
t.text()
),
));
}
}
seen_entry = true;
}
_ => {
pending = None;
}
}
}
}
fn expected_entry_indent(node: &SyntaxNode, columns: &ColumnIndex) -> String {
node.parent()
.filter(|p| {
matches!(
p.kind(),
crate::SyntaxKind::VALUE
| crate::SyntaxKind::SEQUENCE_ENTRY
| crate::SyntaxKind::TAGGED_NODE
)
})
.and_then(|parent_value| {
let mut last_indent: Option<String> = None;
for el in parent_value.children_with_tokens() {
match el {
rowan::NodeOrToken::Node(n) if &n == node => break,
rowan::NodeOrToken::Token(t) if t.kind() == crate::SyntaxKind::INDENT => {
last_indent = Some(t.text().to_string());
}
_ => {}
}
}
last_indent
})
.or_else(|| {
let parent = node.parent()?;
if !matches!(
parent.kind(),
crate::SyntaxKind::SEQUENCE_ENTRY
| crate::SyntaxKind::KEY
| crate::SyntaxKind::VALUE
| crate::SyntaxKind::DOCUMENT
) {
return None;
}
Some(" ".repeat(columns.column(&node.first_token()?)))
})
.unwrap_or_default()
}
pub(super) fn check_comment_token_whitespace(
token: &rowan::SyntaxToken<crate::Lang>,
violations: &mut Vec<Violation>,
) {
let Some(prev) = std::iter::successors(token.prev_token(), |t| t.prev_token())
.find(|t| !t.text().is_empty())
else {
return;
};
if !matches!(
prev.kind(),
crate::SyntaxKind::WHITESPACE
| crate::SyntaxKind::NEWLINE
| crate::SyntaxKind::INDENT
| crate::SyntaxKind::BOM
) {
violations.push(Violation::error_at(
Rule::Other,
token.text_range(),
"Comment without whitespace separation",
));
}
}
pub(super) fn check_block_mapping_after_properties(
node: &SyntaxNode,
violations: &mut Vec<Violation>,
) {
if starts_a_flow_collection(node) {
return;
}
let mut prev = node.prev_sibling_or_token();
while let Some(rowan::NodeOrToken::Token(token)) = &prev {
if token.kind() != crate::SyntaxKind::WHITESPACE {
break;
}
prev = token.prev_sibling_or_token();
}
let Some(rowan::NodeOrToken::Token(property)) = prev else {
return;
};
if matches!(
property.kind(),
crate::SyntaxKind::TAG | crate::SyntaxKind::ANCHOR
) {
violations.push(Violation::error_at(
Rule::Other,
node.text_range(),
"A block mapping cannot start on the line of its tag or anchor",
));
}
}
pub(super) fn check_implicit_key_multiline(
entry_node: &SyntaxNode,
columns: &ColumnIndex,
violations: &mut Vec<Violation>,
) {
let is_explicit = has_child_token(entry_node, |k| k == crate::SyntaxKind::QUESTION)
|| entry_node
.children()
.filter(|c| c.kind() == crate::SyntaxKind::KEY)
.any(|key| has_child_token(&key, |k| k == crate::SyntaxKind::QUESTION));
if is_explicit {
return;
}
let in_flow = entry_node
.parent()
.is_some_and(|m| starts_a_flow_collection(&m));
if !in_flow {
let multiline_key = entry_node
.children()
.filter(|c| c.kind() == crate::SyntaxKind::KEY)
.find(|key| columns.has_line_break(key.text_range()));
if let Some(key) = multiline_key {
violations.push(Violation::error_at(
Rule::Other,
key.text_range(),
"Implicit key cannot span multiple lines",
));
return;
}
}
if entry_node
.parent()
.is_some_and(|m| m.kind() == crate::SyntaxKind::MAPPING && starts_a_flow_collection(&m))
{
return;
}
if line_break_before_colon(entry_node) {
violations.push(Violation::error_at(
Rule::Other,
entry_node.text_range(),
"Implicit key cannot span multiple lines",
));
}
}
fn line_break_before_colon(entry: &SyntaxNode) -> bool {
entry
.children_with_tokens()
.skip_while(|el| el.kind() != crate::SyntaxKind::KEY)
.filter_map(|el| el.into_token())
.find(|t| {
matches!(
t.kind(),
crate::SyntaxKind::NEWLINE | crate::SyntaxKind::COLON
)
})
.is_some_and(|t| t.kind() == crate::SyntaxKind::NEWLINE)
}
pub(super) fn check_collection_on_same_line_as_key(
entry_node: &SyntaxNode,
in_flow: bool,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
let key_node = entry_node
.children()
.filter(|c| c.kind() == SyntaxKind::KEY)
.last();
let value_node = entry_node
.children()
.filter(|c| c.kind() == SyntaxKind::VALUE)
.last();
let Some(value) = value_node else { return };
if has_child_token(entry_node, |k| k == SyntaxKind::QUESTION) {
return;
}
let Some(collection) = value
.children()
.find(|c| matches!(c.kind(), SyntaxKind::SEQUENCE | SyntaxKind::MAPPING))
else {
return;
};
if starts_a_flow_collection(&collection) || in_flow {
return;
}
if !line_break_before_collection(key_node.as_ref(), &value, &collection) {
let what = if collection.kind() == SyntaxKind::MAPPING {
"mapping"
} else {
"sequence"
};
violations.push(Violation::error_at(
Rule::Other,
collection.text_range(),
format!("Block {what} cannot start on same line as mapping key"),
));
}
}
fn line_break_before_collection(
key: Option<&SyntaxNode>,
value: &SyntaxNode,
collection: &SyntaxNode,
) -> bool {
use crate::SyntaxKind;
let in_value = value
.children_with_tokens()
.take_while(|el| el.as_node() != Some(collection))
.any(|el| el.kind() == SyntaxKind::NEWLINE);
in_value
|| key.is_some_and(|key| {
key.siblings_with_tokens(rowan::Direction::Next)
.filter_map(|el| el.into_token())
.skip_while(|t| t.kind() != SyntaxKind::COLON)
.any(|t| t.kind() == SyntaxKind::NEWLINE)
})
}
pub(super) fn check_sequence_indentation(
node: &SyntaxNode,
columns: &ColumnIndex,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
if node.kind() != SyntaxKind::SEQUENCE {
return;
}
fn collect_dashes(
node: &rowan::SyntaxNode<crate::Lang>,
dashes: &mut Vec<rowan::SyntaxToken<crate::Lang>>,
) {
for child in node.children_with_tokens() {
match child {
rowan::NodeOrToken::Token(token) if token.kind() == crate::SyntaxKind::DASH => {
dashes.push(token);
}
rowan::NodeOrToken::Node(n) if n.kind() == crate::SyntaxKind::SEQUENCE_ENTRY => {
collect_dashes(&n, dashes);
}
_ => {}
}
}
}
let mut dashes = Vec::new();
collect_dashes(node, &mut dashes);
let Some((first, rest)) = dashes.split_first() else {
return;
};
let first_col = columns.column(first);
if let Some(misplaced) = rest.iter().find(|t| columns.column(t) != first_col) {
violations.push(Violation::error_at(
Rule::InvalidIndentation,
misplaced.text_range(),
"Inconsistent sequence item indentation",
));
}
}
fn block_scalar_tab_indent(node: &SyntaxNode) -> Option<crate::nodes::SyntaxToken> {
if !is_block_scalar(node) {
return None;
}
child_token(node, crate::SyntaxKind::INDENT).filter(|t| t.text().starts_with('\t'))
}
fn quoted_scalar_tab_indent(node: &SyntaxNode) -> Option<crate::nodes::SyntaxToken> {
if !node.parent().is_some_and(|p| {
matches!(
p.kind(),
crate::SyntaxKind::VALUE | crate::SyntaxKind::SEQUENCE_ENTRY
)
}) {
return None;
}
node.children_with_tokens()
.filter_map(|c| c.into_token())
.find(|t| {
is_quoted_scalar(t)
&& t.text()
.split(['\n', '\r'])
.skip(1)
.any(|line| line.starts_with('\t') && !line.trim().is_empty())
})
}
fn block_tab_indent(node: &SyntaxNode) -> Option<crate::nodes::SyntaxToken> {
let is_flow = starts_a_flow_collection(node);
let tab = node
.children_with_tokens()
.filter_map(|c| c.into_token())
.find(|t| tab_before_block_collection(t) || tab_indent(t, is_flow))?;
let outermost = node
.ancestors()
.filter(|a| {
matches!(
a.kind(),
crate::SyntaxKind::MAPPING | crate::SyntaxKind::SEQUENCE
)
})
.last();
if outermost.is_some_and(|c| starts_a_flow_collection(&c)) {
return None;
}
Some(tab)
}
fn tab_before_block_collection(token: &crate::nodes::SyntaxToken) -> bool {
use crate::SyntaxKind;
if token.kind() != SyntaxKind::WHITESPACE || !token.text().contains('\t') {
return false;
}
let Some(next) = token.next_sibling_or_token().and_then(|n| n.into_node()) else {
return false;
};
let opens_a_block = |n: &SyntaxNode| {
matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE)
&& !starts_a_flow_collection(n)
};
let collection = if opens_a_block(&next) {
next
} else if matches!(next.kind(), SyntaxKind::VALUE | SyntaxKind::KEY) {
match next.children().find(opens_a_block) {
Some(c) => c,
None => return false,
}
} else {
return false;
};
let first_entry = collection
.children()
.find(|c| {
matches!(
c.kind(),
SyntaxKind::MAPPING_ENTRY | SyntaxKind::SEQUENCE_ENTRY
)
})
.map_or(collection.text_range().start(), |e| e.text_range().start());
!std::iter::successors(token.next_token(), |t| t.next_token())
.take_while(|t| t.text_range().start() < first_entry)
.any(|t| t.kind() == SyntaxKind::NEWLINE)
}
fn tab_indent(token: &crate::nodes::SyntaxToken, in_flow: bool) -> bool {
if token.kind() != crate::SyntaxKind::INDENT {
return false;
}
if token.next_token().map_or(true, |t| {
matches!(
t.kind(),
crate::SyntaxKind::NEWLINE | crate::SyntaxKind::COMMENT
)
}) {
return false;
}
let starts_block_entry = !in_flow
&& token
.next_sibling_or_token()
.is_some_and(|n| match n.kind() {
crate::SyntaxKind::MAPPING_ENTRY | crate::SyntaxKind::SEQUENCE_ENTRY => true,
crate::SyntaxKind::MAPPING | crate::SyntaxKind::SEQUENCE => {
!n.as_node().is_some_and(starts_a_flow_collection)
}
_ => false,
});
let at_stream_level = token
.parent()
.is_some_and(|p| p.kind() == crate::SyntaxKind::DOCUMENT);
if (token.text().starts_with(' ') || at_stream_level) && !starts_block_entry {
return false;
}
token.text().contains('\t')
}
pub(super) fn check_explicit_key_separation(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::SyntaxKind;
for question in node
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.filter(|t| t.kind() == SyntaxKind::QUESTION)
{
let separated = std::iter::successors(question.next_token(), |t| t.next_token())
.find(|t| !t.text().is_empty())
.map_or(true, |t| {
matches!(t.kind(), SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
});
if !separated {
violations.push(Violation::error_at(
Rule::Other,
question.text_range(),
"Explicit key indicator '?' must be followed by white space".to_string(),
));
}
}
}
pub(super) fn check_explicit_value_column(
entry: &SyntaxNode,
columns: &ColumnIndex,
in_flow: bool,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
if in_flow {
return;
}
let (Some(question), Some(colon)) = (
child_token(entry, SyntaxKind::QUESTION),
child_token(entry, SyntaxKind::COLON),
) else {
return;
};
let opens_line = std::iter::successors(colon.prev_token(), |t| t.prev_token())
.find(|t| {
!t.text().is_empty() && !matches!(t.kind(), SyntaxKind::INDENT | SyntaxKind::WHITESPACE)
})
.map_or(true, |t| t.kind() == SyntaxKind::NEWLINE);
if opens_line && columns.column(&colon) != columns.column(&question) {
violations.push(Violation::error_at(
Rule::Other,
colon.text_range(),
"Explicit value indicator ':' must be at the column of its '?'".to_string(),
));
}
}