use super::{Rule, Violation};
use crate::anchor_resolution::{
document_scope, is_merge_key, merge_operands, merge_source, AnchorRegistry, MergedMapping,
};
use crate::as_yaml::SharedIdentities;
use crate::tag;
use crate::yaml::SyntaxNode;
use rowan::ast::AstNode;
#[derive(Default)]
pub(super) struct DocumentRegistry(Option<(SyntaxNode, AnchorRegistry)>);
impl DocumentRegistry {
fn get(&mut self, node: &SyntaxNode) -> &AnchorRegistry {
let scope = document_scope(node);
if self.0.as_ref().map_or(true, |(cached, _)| *cached != scope) {
let registry = AnchorRegistry::from_tree(&scope);
self.0 = Some((scope, registry));
}
&self.0.as_ref().expect("just filled").1
}
}
pub(super) fn check_merge_operands(
node: &SyntaxNode,
registry: &mut DocumentRegistry,
violations: &mut Vec<Violation>,
) {
let Some(mapping) = crate::yaml::Mapping::cast(node.clone()) else {
return;
};
for (key, value) in mapping.iter() {
if !is_merge_key(&key) {
continue;
}
let registry = registry.get(node);
let Ok(operands) = merge_operands(value.clone(), registry) else {
continue;
};
let value_range = value.syntax().text_range();
for operand in operands {
if !matches!(merge_source(&operand, registry), Ok(None)) {
continue;
}
let range = operand.syntax().text_range();
let range = if value_range.contains_range(range) {
range
} else {
value_range
};
violations.push(Violation::warning_at(
Rule::Other,
range,
format!(
"Merge key operand {:?} is not a mapping",
operand.to_string()
),
));
}
}
}
pub(super) fn check_set_values(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::as_yaml::YamlNode;
let Some(mapping) = crate::yaml::TaggedNode::cast(node.clone())
.filter(|tagged| tagged.as_set().is_some())
.and_then(|tagged| tagged.as_mapping())
else {
return;
};
for entry in mapping.entries() {
let Some(value) = entry.value_node() else {
continue;
};
if matches!(value, YamlNode::Alias(_)) {
continue;
}
let is_null =
crate::as_yaml::scalar_canonical(value.syntax()).is_some_and(|(t, _)| t == tag::NULL);
if is_null {
continue;
}
let member = entry
.key_node()
.map_or_else(String::new, |key| key.to_string());
violations.push(Violation::warning_at(
Rule::InvalidTag,
value.syntax().text_range(),
format!(
"!!set member {member:?} has non-null value {:?}",
value.to_string()
),
));
}
}
pub(super) fn check_pairs_items(node: &SyntaxNode, violations: &mut Vec<Violation>) {
use crate::as_yaml::YamlNode;
let Some(tagged) = crate::yaml::TaggedNode::cast(node.clone()) else {
return;
};
if !matches!(
tagged.resolved_tag().as_deref(),
Some(tag::OMAP | tag::PAIRS)
) {
return;
}
let Some(written_tag) = tagged.tag() else {
return;
};
let Some(sequence) = tagged.as_sequence() else {
return;
};
let registry = std::cell::OnceCell::new();
for item in sequence.values() {
if matches!(item, YamlNode::Alias(_)) {
continue;
}
if let Some(mapping) = pairs_item_mapping(&item) {
match pairs_item_keys(&mapping, ®istry) {
Some(keys) if keys.len() != 1 => {}
_ => continue,
}
}
violations.push(Violation::warning_at(
Rule::InvalidTag,
item.syntax().text_range(),
format!(
"{written_tag} item {:?} is not a mapping with a single entry",
item.to_string()
),
));
}
}
pub(super) fn check_omap_keys(
node: &SyntaxNode,
identities: &mut SharedIdentities,
violations: &mut Vec<Violation>,
) {
let Some(tagged) = crate::yaml::TaggedNode::cast(node.clone()) else {
return;
};
if tagged.resolved_tag().as_deref() != Some(tag::OMAP) {
return;
}
let Some(sequence) = tagged.as_sequence() else {
return;
};
let registry = std::cell::OnceCell::new();
let keys = sequence.values().filter_map(|item| {
let mapping = pairs_item_mapping(&item)?;
let key = pairs_item_keys(&mapping, ®istry)?.into_iter().next()?;
Some((key, item.syntax().text_range()))
});
let mut seen: Vec<(usize, SyntaxNode)> = Vec::new();
for (key, range) in keys {
let Some(identity) = key_identity(&key, identities) else {
continue;
};
if let Some((_, first)) = seen.iter().find(|(k, _)| *k == identity) {
violations.push(Violation::warning_at(
Rule::DuplicateKeys,
range,
format!(
"Duplicate !!omap key: {} (semantically equal to {})",
format_key(&key),
format_key(first)
),
));
}
seen.push((identity, key));
}
}
fn pairs_item_mapping(item: &crate::as_yaml::YamlNode) -> Option<crate::yaml::Mapping> {
use crate::as_yaml::YamlNode;
match item {
YamlNode::Mapping(mapping) => Some(mapping.clone()),
YamlNode::TaggedNode(tagged) if tagged.resolved_tag().as_deref() == Some(tag::MAP) => {
tagged.as_mapping()
}
_ => None,
}
}
fn pairs_item_keys(
mapping: &crate::yaml::Mapping,
registry: &std::cell::OnceCell<AnchorRegistry>,
) -> Option<Vec<SyntaxNode>> {
let key_node = |key: crate::as_yaml::YamlNode| {
key.syntax()
.ancestors()
.find(|n| n.kind() == crate::SyntaxKind::KEY)
};
if !mapping.keys().any(|key| is_merge_key(&key)) {
return Some(mapping.keys().filter_map(key_node).collect());
}
let registry =
registry.get_or_init(|| AnchorRegistry::from_tree(&document_scope(mapping.syntax())));
let merged = MergedMapping::build(mapping.clone(), registry).ok()?;
Some(merged.keys().filter_map(key_node).collect())
}
pub(super) fn check_duplicate_keys(
node: &SyntaxNode,
identities: &mut SharedIdentities,
violations: &mut Vec<Violation>,
) {
use crate::SyntaxKind;
use std::collections::HashMap;
let keys: Vec<(SyntaxNode, rowan::TextRange)> = node
.children()
.filter(|child| child.kind() == SyntaxKind::MAPPING_ENTRY)
.filter_map(|child| {
let key = child.children().find(|n| n.kind() == SyntaxKind::KEY)?;
Some((key, child.text_range()))
})
.collect();
let kinds: Vec<Option<SyntaxKind>> = keys.iter().map(|(key, _)| identity_kind(key)).collect();
let mut kind_counts: HashMap<SyntaxKind, usize> = HashMap::new();
for kind in kinds.iter().flatten() {
*kind_counts.entry(*kind).or_default() += 1;
}
let mut seen: HashMap<usize, usize> = HashMap::new();
for (i, (key, range)) in keys.iter().enumerate() {
if !kinds[i].is_some_and(|kind| kind_counts[&kind] > 1) {
continue;
}
let Some(identity) = key_identity(key, identities) else {
continue;
};
match seen.entry(identity) {
std::collections::hash_map::Entry::Occupied(first) => {
violations.push(Violation::error_at(
Rule::DuplicateKeys,
*range,
format!(
"Duplicate key: {} (semantically equal to {})",
format_key(key),
format_key(&keys[*first.get()].0)
),
));
}
std::collections::hash_map::Entry::Vacant(slot) => {
slot.insert(i);
}
}
}
}
fn format_key(key: &SyntaxNode) -> String {
match key.text().to_string().trim() {
"" => "null".to_string(),
text => format!("{text:?}"),
}
}
fn key_identity(key: &SyntaxNode, identities: &mut SharedIdentities) -> Option<usize> {
identities.identity(key).ok().flatten()
}
fn identity_kind(node: &SyntaxNode) -> Option<crate::SyntaxKind> {
use crate::SyntaxKind;
match node.kind() {
SyntaxKind::KEY => identity_kind(&node.first_child()?),
SyntaxKind::TAGGED_NODE => node
.first_child()
.map_or(Some(SyntaxKind::SCALAR), |content| identity_kind(&content)),
kind => Some(kind),
}
}