use tree_sitter::Node;
use super::rs_paths::path_segments;
use crate::parse;
pub(super) type PathFact = Option<Option<String>>;
struct Attr {
path: Vec<String>,
form: Form,
carried: bool,
}
enum Form {
Bare,
Args,
Value(Option<String>),
Malformed,
}
pub(super) fn is_test(node: Node, src: &[u8]) -> bool {
attributes(node, src).iter().any(|a| {
a.path.last().is_some_and(|s| s == "test")
&& (!a.carried || matches!(a.form, Form::Bare | Form::Args))
})
}
pub(super) fn path_fact(node: Node, src: &[u8]) -> PathFact {
let attr = attributes(node, src)
.into_iter()
.find(|a| a.path.len() == 1 && a.path[0] == "path")?;
match attr.form {
Form::Value(Some(literal)) if !attr.carried => Some(Some(literal)),
_ => Some(None),
}
}
fn attributes(node: Node, src: &[u8]) -> Vec<Attr> {
let mut items = Vec::new();
let mut current = node.prev_sibling();
while let Some(prev) = current {
match prev.kind() {
"attribute_item" => items.extend(prev.named_child(0)),
"line_comment" | "block_comment" => {}
_ => break,
}
current = prev.prev_sibling();
}
items.reverse();
let mut out = Vec::new();
for attribute in items {
plain(attribute, src, &mut out);
}
out
}
fn plain(attribute: Node, src: &[u8], out: &mut Vec<Attr>) {
let Some(path) = attribute
.named_child(0)
.and_then(|p| path_segments(p, src))
.map(|segs| segs.into_iter().map(|s| s.text).collect::<Vec<_>>())
else {
return;
};
let args = attribute.child_by_field_name("arguments");
if path == ["cfg_attr"] {
if let Some(tree) = args {
expand(tree, src, out);
}
return;
}
let form = if args.is_some() {
Form::Args
} else if let Some(value) = attribute.child_by_field_name("value") {
Form::Value(string_literal(value, src))
} else {
Form::Bare
};
out.push(Attr {
path,
form,
carried: false,
});
}
fn expand(tree: Node, src: &[u8], out: &mut Vec<Attr>) {
let mut stack = vec![tree];
let mut found = Vec::new();
while let Some(tree) = stack.pop() {
let Some(parts) = split(tree) else {
continue;
};
for part in parts.into_iter().skip(1).rev() {
let attr = read_carried(&part, src);
match (&attr.form, part.last()) {
(Form::Args, Some(inner)) if attr.path == ["cfg_attr"] => stack.push(*inner),
_ => found.push((part[0].start_byte(), attr)),
}
}
}
found.sort_by_key(|(start, _)| *start);
out.extend(found.into_iter().map(|(_, a)| a));
}
fn split(tree: Node) -> Option<Vec<Vec<Node>>> {
let mut cursor = tree.walk();
let tokens: Vec<Node> = tree.children(&mut cursor).collect();
if tokens.len() < 2 || tokens[0].kind() != "(" || tokens[tokens.len() - 1].kind() != ")" {
return None;
}
let mut parts: Vec<Vec<Node>> = vec![Vec::new()];
for token in &tokens[1..tokens.len() - 1] {
if token.kind() == "," {
parts.push(Vec::new());
} else {
parts.last_mut()?.push(*token);
}
}
if parts.len() > 2 && parts.last().is_some_and(Vec::is_empty) {
parts.pop();
}
(parts.len() >= 2 && parts.iter().all(|p| !p.is_empty())).then_some(parts)
}
fn read_carried(tokens: &[Node], src: &[u8]) -> Attr {
let mut path = Vec::new();
let mut k = usize::from(tokens.first().is_some_and(|t| t.kind() == "::"));
while let Some(token) = tokens.get(k).filter(|t| t.kind() == "identifier") {
path.push(parse::node_text(*token, src));
if tokens.get(k + 1).is_some_and(|t| t.kind() == "::") {
k += 2;
} else {
k += 1;
break;
}
}
let rest = tokens.get(k..).unwrap_or_default();
let form = match rest {
_ if path.is_empty() => Form::Malformed,
[] => Form::Bare,
[tree] if tree.kind() == "token_tree" => Form::Args,
[eq, value] if eq.kind() == "=" => Form::Value(string_literal(*value, src)),
_ => Form::Malformed,
};
Attr {
path,
form,
carried: true,
}
}
fn string_literal(node: Node, src: &[u8]) -> Option<String> {
let text = parse::node_text(node, src);
match node.kind() {
"string_literal" => {
let mut cursor = node.walk();
let escaped = node
.named_children(&mut cursor)
.any(|c| c.kind() == "escape_sequence");
let inner = text.strip_prefix('"')?.strip_suffix('"')?;
(!escaped).then(|| inner.to_string())
}
"raw_string_literal" => {
let body = text.strip_prefix('r')?;
let hashes = body.len() - body.trim_start_matches('#').len();
let close = format!("\"{}", "#".repeat(hashes));
body[hashes..]
.strip_prefix('"')?
.strip_suffix(close.as_str())
.map(str::to_string)
}
_ => None,
}
}