use std::collections::HashSet;
use tree_sitter::Node;
use crate::cop::CopConfig;
use crate::parse::source::SourceFile;
pub fn matches_invertible_unless_condition(
source: &SourceFile,
node: Node<'_>,
config: &CopConfig,
) -> bool {
let Some(condition) = node.child_by_field_name("condition") else {
return false;
};
is_invertible(condition, source, &inverse_methods(config))
}
fn inverse_methods(config: &CopConfig) -> HashSet<&str> {
config
.options
.get("InverseMethods")
.and_then(serde_yml::Value::as_mapping)
.map(|map| {
map.keys()
.filter_map(serde_yml::Value::as_str)
.map(|key| key.trim_start_matches(':'))
.collect()
})
.unwrap_or_default()
}
fn is_invertible(node: Node<'_>, source: &SourceFile, inverses: &HashSet<&str>) -> bool {
match node.kind() {
"begin" | "parenthesized_statements" => node
.children(&mut node.walk())
.find(|child| child.is_named())
.is_some_and(|first| is_invertible(first, source, inverses)),
"call" => invertible_call(node, source, inverses),
"unary" => matches!(operator(node, source), "!" | "not"),
"binary" => invertible_binary(node, source, inverses),
_ => false,
}
}
fn invertible_binary(node: Node<'_>, source: &SourceFile, inverses: &HashSet<&str>) -> bool {
match operator(node, source) {
"&&" | "and" | "||" | "or" => {
node.child_by_field_name("left")
.is_some_and(|l| is_invertible(l, source, inverses))
&& node
.child_by_field_name("right")
.is_some_and(|r| is_invertible(r, source, inverses))
}
op => !inheritance_check(node, source) && inverses.contains(op),
}
}
fn invertible_call(node: Node<'_>, source: &SourceFile, inverses: &HashSet<&str>) -> bool {
if node.child_by_field_name("block").is_some() {
return false;
}
if operator(node, source) == "&." {
return false;
}
match node.child_by_field_name("method") {
Some(method) => match text(method, source) {
"!" | "not" => true,
name => inverses.contains(name),
},
None => false,
}
}
fn inheritance_check(node: Node<'_>, source: &SourceFile) -> bool {
if operator(node, source) != "<" {
return false;
}
let Some(arg) = node.child_by_field_name("right") else {
return false;
};
let Some(short) = short_constant_name(arg, source) else {
return false;
};
short.to_uppercase() != short
}
fn short_constant_name<'a>(node: Node<'_>, source: &'a SourceFile) -> Option<&'a str> {
match node.kind() {
"constant" => Some(text(node, source)),
"scope_resolution" => {
let name = node.child_by_field_name("name")?;
Some(text(name, source))
}
_ => None,
}
}
fn operator<'a>(node: Node<'_>, source: &'a SourceFile) -> &'a str {
node.child_by_field_name("operator")
.map(|op| text(op, source))
.unwrap_or("")
}
fn text<'a>(node: Node<'_>, source: &'a SourceFile) -> &'a str {
node.utf8_text(source.as_bytes()).unwrap_or("")
}