use tree_sitter::Node;
use crate::file_analysis::Span;
pub(crate) use crate::conventions::is_conventional_invocant_name;
pub(crate) trait NodeExt<'a>: Sized {
fn text(&self, src: &'a [u8]) -> Option<&'a str>;
fn field_text(&self, field: &str, src: &'a [u8]) -> Option<&'a str>;
fn span(&self) -> Span;
fn named(&self) -> NamedChildren<'a>;
}
impl<'a> NodeExt<'a> for Node<'a> {
fn text(&self, src: &'a [u8]) -> Option<&'a str> {
self.utf8_text(src).ok()
}
fn field_text(&self, field: &str, src: &'a [u8]) -> Option<&'a str> {
self.child_by_field_name(field).and_then(|n| n.text(src))
}
fn span(&self) -> Span {
node_to_span(*self)
}
fn named(&self) -> NamedChildren<'a> {
NamedChildren { node: *self, idx: 0 }
}
}
pub(crate) fn node_to_span(node: Node) -> Span {
Span {
start: node.start_position(),
end: node.end_position(),
}
}
pub(crate) fn extract_call_name(node: Node, source: &[u8]) -> Option<String> {
match node.kind() {
"method_call_expression" => node.field_text("method", source).map(str::to_string),
"function_call_expression" | "ambiguous_function_call_expression" => {
node.field_text("function", source).map(str::to_string)
}
_ => None,
}
}
pub(crate) fn fq_tail_span(node: Node, text: &str) -> Span {
match text.rfind("::") {
Some(idx) => {
let s = node.start_position();
Span {
start: tree_sitter::Point { row: s.row, column: s.column + idx + 2 },
end: node.end_position(),
}
}
None => node_to_span(node),
}
}
pub(crate) struct NamedChildren<'a> {
node: Node<'a>,
idx: usize,
}
impl<'a> Iterator for NamedChildren<'a> {
type Item = Node<'a>;
fn next(&mut self) -> Option<Node<'a>> {
while self.idx < self.node.named_child_count() {
let i = self.idx;
self.idx += 1;
if let Some(c) = self.node.named_child(i) {
return Some(c);
}
}
None
}
}
macro_rules! typed_node {
($(#[$meta:meta])* $name:ident($kind:literal) { $($(#[$fmeta:meta])* $field:ident),* $(,)? }) => {
$(#[$meta])*
#[derive(Clone, Copy)]
pub(crate) struct $name<'a>(Node<'a>);
#[allow(dead_code)]
impl<'a> $name<'a> {
pub(crate) fn cast(node: Node<'a>) -> Option<Self> {
(node.kind() == $kind).then_some(Self(node))
}
pub(crate) fn node(&self) -> Node<'a> {
self.0
}
$(
$(#[$fmeta])*
pub(crate) fn $field(&self) -> Option<Node<'a>> {
self.0.child_by_field_name(stringify!($field))
}
)*
}
};
}
typed_node! {
MethodCall("method_call_expression") {
invocant,
method,
arguments,
}
}
typed_node! {
FunctionCall("function_call_expression") {
function,
arguments,
}
}
typed_node! {
AmbiguousFunctionCall("ambiguous_function_call_expression") {
function,
arguments,
}
}
pub(crate) fn call_args<'a>(call_node: Node<'a>) -> Vec<Node<'a>> {
let Some(args) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
if matches!(args.kind(), "list_expression" | "parenthesized_expression") {
args.named().collect()
} else {
vec![args]
}
}
pub(crate) fn flatten_list<'a>(list: Node<'a>, out: &mut Vec<Node<'a>>) {
let count = list.child_count();
for i in 0..count {
let Some(child) = list.child(i) else { continue };
if matches!(child.kind(), "list_expression" | "parenthesized_expression") {
flatten_list(child, out);
} else {
out.push(child);
}
}
}
pub(crate) fn peel_groups(mut node: Node) -> Node {
while matches!(node.kind(), "parenthesized_expression" | "list_expression") {
let mut named = node.named();
match (named.next(), named.next()) {
(Some(inner), None) => node = inner,
_ => break, }
}
node
}
pub(crate) fn first_named_child_where<'a>(
node: Node<'a>,
pred: impl Fn(&str) -> bool,
) -> Option<Node<'a>> {
node.named().map(peel_groups).find(|c| pred(c.kind()))
}
pub(crate) fn pair_nodes_in<'a>(children: &[Node<'a>]) -> Vec<(Node<'a>, Node<'a>)> {
let mut out = Vec::new();
let count = children.len();
let mut i = 0;
while i < count {
let k_node = children[i];
i += 1;
if !k_node.is_named() {
continue;
}
let val = loop {
match children.get(i) {
Some(c) if c.is_named() => break Some(*c),
Some(_) => i += 1,
None => break None,
}
};
let Some(val) = val else { break };
i += 1; out.push((k_node, val));
}
out
}
pub(crate) fn pair_nodes<'a>(container: Node<'a>) -> Vec<(Node<'a>, Node<'a>)> {
let list = if container.kind() == "anonymous_hash_expression" {
container
.named()
.find(|c| c.kind() == "list_expression")
.unwrap_or(container)
} else {
container
};
let mut children = Vec::new();
flatten_list(list, &mut children);
pair_nodes_in(&children)
}
pub(crate) fn varname_child<'a>(node: Node<'a>) -> Option<Node<'a>> {
node.named().find(|c| c.kind() == "varname")
}
pub(crate) fn canonical_var_name<'a>(node: Node<'a>, src: &'a [u8]) -> Option<String> {
let sigil = match node.kind() {
"scalar" => '$',
"array" => '@',
"hash" => '%',
_ => return None,
};
let vn = varname_child(node)?;
if vn.named_child_count() > 0 {
return None;
}
Some(format!("{}{}", sigil, vn.text(src)?))
}
pub(crate) fn canonical_container_name<'a>(node: Node<'a>, src: &'a [u8]) -> Option<String> {
let parent = node.parent()?;
let bare = varname_child(node)?.text(src)?;
let target_sigil: char = match parent.kind() {
"array_element_expression" => '@',
"hash_element_expression" => '%',
"slice_expression" | "keyval_expression" => {
if parent.child_by_field_name("array").is_some_and(|c| c == node) {
'@'
} else if parent.child_by_field_name("hash").is_some_and(|c| c == node) {
'%'
} else {
return None;
}
}
_ => return None,
};
Some(format!("{}{}", target_sigil, bare))
}
pub(crate) fn canonical_place_path<'a>(
node: Node<'a>,
src: &'a [u8],
) -> Option<(String, String)> {
let base = match node.kind() {
"hash_element_expression" => {
let key = node.child_by_field_name("key")?;
if !matches!(key.kind(), "autoquoted_bareword" | "string_literal" | "number") {
return None;
}
node.named_child(0)?
}
"array_element_expression" => {
let index = node.child_by_field_name("index")?;
if index.kind() != "number" {
return None;
}
node.named_child(0)?
}
_ => return None,
};
let root = match base.kind() {
"scalar" => canonical_var_name(base, src)?,
"hash_element_expression" | "array_element_expression" => {
canonical_place_path(base, src)?.1
}
_ => return None,
};
let key = node.text(src)?.to_string();
Some((key, root))
}
pub(crate) fn qw_word_spans(qw_node: Node, src: &[u8], results: &mut Vec<(String, Span)>) {
for j in 0..qw_node.named_child_count() {
let Some(sc) = qw_node.named_child(j) else { continue };
if sc.kind() != "string_content" {
continue;
}
let Ok(text) = sc.utf8_text(src) else { continue };
let sc_start = sc.start_position();
let bytes = text.as_bytes();
let mut row = sc_start.row;
let mut col = sc_start.column;
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
if bytes[i] == b'\n' {
row += 1;
col = 0;
} else {
col += 1;
}
i += 1;
}
let word_start = tree_sitter::Point { row, column: col };
let word_begin = i;
while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
col += 1;
i += 1;
}
if i > word_begin {
results.push((
text[word_begin..i].to_string(),
Span { start: word_start, end: tree_sitter::Point { row, column: col } },
));
}
}
}
}
pub(crate) fn string_content_text(node: Node, src: &[u8]) -> Option<String> {
for i in 0..node.named_child_count() {
if let Some(content) = node.named_child(i) {
if content.kind() == "string_content" {
return content.utf8_text(src).ok().map(|s| s.to_string());
}
}
}
None
}
pub(crate) fn plain_string_literal_text(node: Node, src: &[u8]) -> Option<String> {
if !matches!(node.kind(), "string_literal" | "interpolated_string_literal") {
return None;
}
let mut content: Option<String> = None;
for i in 0..node.named_child_count() {
let c = node.named_child(i)?;
if c.kind() != "string_content" || content.is_some() || c.named_child_count() > 0 {
return None;
}
content = c.utf8_text(src).ok().map(|s| s.to_string());
}
content.filter(|s| !s.is_empty())
}
pub(crate) fn string_content_span(node: Node) -> Span {
for i in 0..node.named_child_count() {
if let Some(content) = node.named_child(i) {
if content.kind() == "string_content" {
return node_to_span(content);
}
}
}
node_to_span(node)
}
fn map_built_strings(
node: Node,
src: &[u8],
fold: &mut dyn FnMut(Node) -> Vec<(String, Span)>,
) -> Vec<(String, Span)> {
let Some(kw) = node.child(0) else { return vec![] };
if kw.utf8_text(src).ok() != Some("map") {
return vec![];
}
let Some(cb) = node.child_by_field_name("callback") else { return vec![] };
if cb.kind() != "interpolated_string_literal" {
return vec![];
}
let Some(content) = cb.named().find(|c| c.kind() == "string_content") else {
return vec![];
};
let Ok(ctext) = content.utf8_text(src) else { return vec![] };
let subs: Vec<Node> = content.named().filter(|c| c.kind() == "scalar").collect();
let [topic] = subs.as_slice() else { return vec![] };
if topic.utf8_text(src).ok() != Some("$_") {
return vec![];
}
let pre_end = topic.start_byte() - content.start_byte();
let post_start = topic.end_byte() - content.start_byte();
let (pre, post) = (&ctext[..pre_end], &ctext[post_start..]);
let Some(list) = node.child_by_field_name("list") else { return vec![] };
string_list(list, src, fold)
.into_iter()
.map(|(t, sp)| (format!("{pre}{t}{post}"), sp))
.collect()
}
pub(crate) fn string_list(
node: Node,
src: &[u8],
fold: &mut dyn FnMut(Node) -> Vec<(String, Span)>,
) -> Vec<(String, Span)> {
string_list_with_residue(node, src, fold).0
}
pub(crate) fn string_list_with_residue(
node: Node,
src: &[u8],
fold: &mut dyn FnMut(Node) -> Vec<(String, Span)>,
) -> (Vec<(String, Span)>, bool) {
match node.kind() {
"quoted_word_list" => {
let mut results = Vec::new();
qw_word_spans(node, src, &mut results);
return (results, false);
}
"string_literal" | "interpolated_string_literal" => {
if let Some(text) = string_content_text(node, src) {
return (vec![(text, string_content_span(node))], false);
}
return (vec![], true);
}
"bareword" | "autoquoted_bareword" | "array" => {
let v = fold(node);
let residue = v.is_empty();
return (v, residue);
}
"map_grep_expression" => {
let v = map_built_strings(node, src, fold);
let residue = v.is_empty();
return (v, residue);
}
_ => {}
}
let mut results = Vec::new();
let mut residue = false;
for i in 0..node.child_count() {
let Some(child) = node.child(i) else { continue };
match child.kind() {
"quoted_word_list" => qw_word_spans(child, src, &mut results),
"string_literal" | "interpolated_string_literal" => {
if let Some(text) = string_content_text(child, src) {
results.push((text, string_content_span(child)));
} else {
residue = true;
}
}
"parenthesized_expression" | "list_expression" | "anonymous_array_expression" => {
let (v, r) = string_list_with_residue(child, src, fold);
results.extend(v);
residue |= r;
}
"bareword" | "autoquoted_bareword" | "array" => {
let v = fold(child);
residue |= v.is_empty();
results.extend(v);
}
"map_grep_expression" => {
let v = map_built_strings(child, src, fold);
residue |= v.is_empty();
results.extend(v);
}
_ => {
if child.is_named() && !matches!(child.kind(), "comment" | "pod") {
residue = true;
}
}
}
}
(results, residue)
}
pub(crate) fn is_conditionally_executed(node: Node) -> bool {
let mut cur = node.parent();
while let Some(p) = cur {
match p.kind() {
"subroutine_declaration_statement"
| "method_declaration_statement"
| "anonymous_subroutine_expression"
| "source_file" => return false,
"conditional_statement"
| "postfix_conditional_expression"
| "conditional_expression"
| "lowprec_logical_expression"
| "binary_expression"
| "loop_statement"
| "for_statement"
| "postfix_for_expression"
| "postfix_loop_expression" => return true,
_ => {}
}
cur = p.parent();
}
false
}
pub(crate) fn varname_inner_scalar_text<'a>(node: Node<'a>, src: &'a [u8]) -> Option<String> {
for i in 0..node.named_child_count() {
let c = node.named_child(i)?;
if c.kind() != "varname" {
continue;
}
for j in 0..c.named_child_count() {
let gc = c.named_child(j)?;
if gc.kind() == "scalar" {
return gc.text(src).map(|s| s.to_string());
}
}
}
None
}
pub(crate) fn element_arrow_deref(element: Node, src: &[u8]) -> bool {
let Some(container) = element.named_child(0) else { return false };
let Some(sub) = element
.child_by_field_name("key")
.or_else(|| element.child_by_field_name("index"))
else {
return false;
};
src.get(container.end_byte()..sub.start_byte())
.is_some_and(|gap| gap.windows(2).any(|w| w == b"->"))
}
pub(crate) fn is_element_access_base(node: Node) -> bool {
let Some(parent) = node.parent() else { return false };
match parent.kind() {
"hash_element_expression" | "array_element_expression" => {
parent.named_child(0).is_some_and(|c| c == node)
}
"slice_expression" | "keyval_expression" => {
parent.child_by_field_name("hashref").is_some_and(|c| c == node)
|| parent.child_by_field_name("arrayref").is_some_and(|c| c == node)
}
"varname" => parent.parent().is_some_and(|gp| {
matches!(
gp.kind(),
"slice_container_variable" | "keyval_container_variable"
)
}),
_ => false,
}
}
pub(crate) fn constructor_invocant<'a>(node: Node<'a>, src: &'a [u8]) -> Option<&'a str> {
use crate::conventions::InvocantText;
let call = MethodCall::cast(node)?;
if !call
.method()?
.text(src)
.is_some_and(crate::conventions::is_constructor_name)
{
return None;
}
let inv = call.invocant()?.text(src)?;
match InvocantText::parse(inv) {
InvocantText::Bareword(_) | InvocantText::CurrentPackage => Some(inv),
InvocantText::Scalar(_)
| InvocantText::NonScalar(_)
| InvocantText::PositionalReceiver => None,
}
}
pub(crate) fn is_conventional_invocant_scalar<'a>(node: Node<'a>, src: &'a [u8]) -> bool {
node.kind() == "scalar"
&& varname_child(node)
.and_then(|v| v.text(src))
.is_some_and(is_conventional_invocant_name)
}