use super::{directive_groups, find_root, Rule, Violation};
use crate::nodes::SyntaxToken;
use crate::nodes::{has_child_token, is_block_scalar, is_quoted_scalar};
use crate::yaml::SyntaxNode;
fn tag_handle(directive: &SyntaxNode) -> Option<String> {
let text = directive.text().to_string();
let mut words = text.split_whitespace();
if words.next() != Some("%TAG") {
return None;
}
words.next().map(str::to_string)
}
fn has_content(node: &SyntaxNode) -> bool {
node.descendants().any(|n| {
matches!(
n.kind(),
crate::SyntaxKind::MAPPING
| crate::SyntaxKind::SEQUENCE
| crate::SyntaxKind::SCALAR
| crate::SyntaxKind::STRING
| crate::SyntaxKind::TAGGED_NODE
)
})
}
fn named_handle(tag: &str) -> Option<&str> {
let rest = tag.strip_prefix('!')?;
if rest.starts_with('<') {
return None;
}
let end = rest.find('!').filter(|&i| i > 0)?;
Some(&tag[..end + 2])
}
fn named_tags(document: &SyntaxNode) -> impl Iterator<Item = (SyntaxToken, String)> {
document
.descendants_with_tokens()
.filter_map(|c| c.into_token())
.filter(|t| t.kind() == crate::SyntaxKind::TAG)
.filter_map(|tag| {
let handle = named_handle(tag.text())?.to_string();
Some((tag, handle))
})
}
fn undeclared_handle(tag: &SyntaxToken, handle: &str) -> Violation {
Violation::error_at(
Rule::Other,
tag.text_range(),
format!("Tag handle {handle} is not declared by a %TAG directive"),
)
}
pub(super) fn check_tag_shorthand_scope(node: &SyntaxNode, violations: &mut Vec<Violation>) {
let mut declared_earlier: Vec<String> = Vec::new();
for (directives, document) in directive_groups(&find_root(node)) {
let own: Vec<String> = directives.iter().filter_map(tag_handle).collect();
if let Some(document) = document {
for (tag, handle) in named_tags(&document) {
if own.contains(&handle) {
continue;
}
violations.push(if declared_earlier.contains(&handle) {
Violation::error_at(
Rule::Other,
tag.text_range(),
format!(
"Tag shorthand {handle} is only defined in the document that declares it"
),
)
} else {
undeclared_handle(&tag, &handle)
});
}
}
declared_earlier.extend(own);
}
}
pub(super) fn check_tag_handles_declared(
directives: &[SyntaxNode],
document: &SyntaxNode,
violations: &mut Vec<Violation>,
) {
let own: Vec<String> = directives.iter().filter_map(tag_handle).collect();
for (tag, handle) in named_tags(document) {
if !own.contains(&handle) {
violations.push(undeclared_handle(&tag, &handle));
}
}
}
pub(super) fn check_directives(
directives: &[SyntaxNode],
document: Option<&SyntaxNode>,
violations: &mut Vec<Violation>,
) {
let Some(last) = directives.last() else {
return;
};
if !document.is_some_and(|d| has_child_token(d, |k| k == crate::SyntaxKind::DOC_START)) {
violations.push(Violation::error_at(
Rule::Other,
last.text_range(),
"Directive without document start marker (---)",
));
}
let mut seen_yaml = false;
let mut tag_handles: Vec<String> = Vec::new();
for directive in directives {
let text = directive.text().to_string();
let mut words = text.split_whitespace();
match words.next() {
Some("%YAML") => {
if seen_yaml {
violations.push(Violation::error_at(
Rule::Other,
directive.text_range(),
"Duplicate %YAML directive",
));
}
seen_yaml = true;
let is_version = |v: &str| {
v.split_once('.').is_some_and(|(major, minor)| {
[major, minor]
.iter()
.all(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()))
})
};
if !words.next().is_some_and(is_version)
|| words.next().is_some_and(|w| !w.starts_with('#'))
{
violations.push(Violation::error_at(
Rule::Other,
directive.text_range(),
"Malformed %YAML directive",
));
}
}
Some("%TAG") => {
if let Some(handle) = words.next() {
if tag_handles.iter().any(|h| h == handle) {
violations.push(Violation::error_at(
Rule::Other,
directive.text_range(),
format!("Duplicate %TAG directive for {handle}"),
));
}
tag_handles.push(handle.to_string());
}
}
_ => {}
}
}
}
pub(super) fn check_directive_after_document(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::SyntaxKind;
let check_node = find_root(node);
let mut seen_document_with_content = false;
for child in check_node.children() {
match child.kind() {
SyntaxKind::DOCUMENT => {
if has_content(&child) {
seen_document_with_content = true;
}
}
SyntaxKind::DIRECTIVE if seen_document_with_content => {
let mut found_doc_with_end = false;
for prev in child.siblings_with_tokens(rowan::Direction::Prev).skip(1) {
if prev.kind() == SyntaxKind::DOC_END {
found_doc_with_end = true;
break;
}
if let Some(prev) = prev.as_node().filter(|n| n.kind() == SyntaxKind::DOCUMENT)
{
found_doc_with_end = has_child_token(prev, |k| k == SyntaxKind::DOC_END);
break;
}
}
if !found_doc_with_end {
violations.push(Violation::error_at(
Rule::Other,
child.text_range(),
"Directive after document requires document end marker (...)",
));
}
}
_ => {}
}
}
}
pub(super) fn check_directive_in_content(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::SyntaxKind;
if is_block_scalar(node) {
return;
}
let mut prev: Option<crate::nodes::SyntaxToken> = None;
for token in node.children_with_tokens().filter_map(|c| c.into_token()) {
if token.kind() == SyntaxKind::DIRECTIVE {
let continues_plain = prev
.as_ref()
.is_some_and(|p| p.kind() == SyntaxKind::STRING && !is_quoted_scalar(p));
if !continues_plain {
violations.push(Violation::error_at(Rule::Other, node.text_range(), "Directive in document content (missing document end marker `...` before directive)".to_string()));
return;
}
}
if !matches!(
token.kind(),
SyntaxKind::NEWLINE | SyntaxKind::WHITESPACE | SyntaxKind::INDENT
) {
prev = Some(token);
}
}
}