use std::sync::OnceLock;
use tree_sitter::Query;
fn perl_language() -> tree_sitter::Language {
ts_parser_perl::LANGUAGE.into()
}
pub fn cpanfile_requires() -> &'static Query {
static QUERY: OnceLock<Query> = OnceLock::new();
QUERY.get_or_init(|| {
Query::new(
&perl_language(),
r#"
[
(function_call_expression
function: (_) @fn
(string_literal (string_content) @module))
(function_call_expression
function: (_) @fn
(list_expression . (string_literal (string_content) @module)))
(ambiguous_function_call_expression
function: (_) @fn
(string_literal (string_content) @module))
(ambiguous_function_call_expression
function: (_) @fn
(list_expression . (string_literal (string_content) @module)))
]
(#eq? @fn "requires")
"#,
)
.expect("cpanfile_requires query should compile")
})
}
#[cfg(test)]
pub fn exports_qw() -> &'static Query {
static QUERY: OnceLock<Query> = OnceLock::new();
QUERY.get_or_init(|| {
Query::new(
&perl_language(),
r#"
[
(assignment_expression
left: [
(variable_declaration (array) @var)
(array) @var
]
(quoted_word_list (string_content) @words))
(assignment_expression
left: [
(variable_declaration (array) @var)
(array) @var
]
(list_expression
(quoted_word_list (string_content) @words)))
]
"#,
)
.expect("exports_qw query should compile")
})
}
#[cfg(test)]
pub fn exports_paren_list() -> &'static Query {
static QUERY: OnceLock<Query> = OnceLock::new();
QUERY.get_or_init(|| {
Query::new(
&perl_language(),
r#"
[
(assignment_expression
left: [
(variable_declaration (array) @var)
(array) @var
]
(list_expression
(string_literal (string_content) @word)))
(assignment_expression
left: [
(variable_declaration (array) @var)
(array) @var
]
(string_literal (string_content) @word))
]
"#,
)
.expect("exports_paren_list query should compile")
})
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::{Parser, QueryCursor, StreamingIterator};
fn parse(source: &str) -> tree_sitter::Tree {
let mut parser = Parser::new();
parser
.set_language(&perl_language())
.unwrap();
parser.parse(source, None).unwrap()
}
#[test]
fn test_cpanfile_requires_cached() {
let q1 = cpanfile_requires();
let q2 = cpanfile_requires();
assert!(std::ptr::eq(q1, q2), "Should return same cached query");
}
#[test]
fn test_cpanfile_requires_matches() {
let source = "requires 'DBI';\nrequires 'JSON', '>= 2.0';";
let tree = parse(source);
let query = cpanfile_requires();
let module_idx = query.capture_index_for_name("module").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut modules = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
if cap.index == module_idx {
modules.push(cap.node.utf8_text(source.as_bytes()).unwrap().to_string());
}
}
}
assert_eq!(modules, vec!["DBI", "JSON"]);
}
#[test]
fn test_exports_qw_matches() {
let source = r#"
our @EXPORT_OK = qw(alpha beta gamma);
our @EXPORT = qw(delta);
"#;
let tree = parse(source);
let query = exports_qw();
let var_idx = query.capture_index_for_name("var").unwrap();
let words_idx = query.capture_index_for_name("words").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut results: Vec<(String, Vec<String>)> = Vec::new();
while let Some(m) = matches.next() {
let mut var_name = String::new();
let mut words = Vec::new();
for cap in m.captures {
let text = cap.node.utf8_text(source.as_bytes()).unwrap();
if cap.index == var_idx {
var_name = text.to_string();
} else if cap.index == words_idx {
words.extend(text.split_whitespace().map(String::from));
}
}
if !var_name.is_empty() {
results.push((var_name, words));
}
}
assert_eq!(results.len(), 2);
assert_eq!(results[0].0, "@EXPORT_OK");
assert_eq!(results[0].1, vec!["alpha", "beta", "gamma"]);
assert_eq!(results[1].0, "@EXPORT");
assert_eq!(results[1].1, vec!["delta"]);
}
#[test]
fn test_exports_paren_list_matches() {
let source = r#"
our @EXPORT_OK = ('foo', 'bar', 'baz');
"#;
let tree = parse(source);
let query = exports_paren_list();
let var_idx = query.capture_index_for_name("var").unwrap();
let word_idx = query.capture_index_for_name("word").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut var_name = String::new();
let mut words = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
let text = cap.node.utf8_text(source.as_bytes()).unwrap();
if cap.index == var_idx && var_name.is_empty() {
var_name = text.to_string();
} else if cap.index == word_idx {
words.push(text.to_string());
}
}
}
assert_eq!(var_name, "@EXPORT_OK");
assert_eq!(words, vec!["foo", "bar", "baz"]);
}
#[test]
fn test_exports_paren_list_single_element() {
let source = "our @EXPORT_OK = ('single');\n";
let tree = parse(source);
let query = exports_paren_list();
let word_idx = query.capture_index_for_name("word").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut words = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
if cap.index == word_idx {
words.push(cap.node.utf8_text(source.as_bytes()).unwrap().to_string());
}
}
}
assert_eq!(words, vec!["single"]);
}
#[test]
fn test_exports_qw_no_our() {
let source = "@EXPORT = qw(no_our);\n";
let tree = parse(source);
let query = exports_qw();
let var_idx = query.capture_index_for_name("var").unwrap();
let words_idx = query.capture_index_for_name("words").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut var_name = String::new();
let mut words = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
let text = cap.node.utf8_text(source.as_bytes()).unwrap();
if cap.index == var_idx {
var_name = text.to_string();
} else if cap.index == words_idx {
words.extend(text.split_whitespace().map(String::from));
}
}
}
assert_eq!(var_name, "@EXPORT");
assert_eq!(words, vec!["no_our"]);
}
#[test]
fn test_exports_qw_inside_parens() {
let source = "our @EXPORT = (qw/lolz this is nested/);\n";
let tree = parse(source);
let query = exports_qw();
let words_idx = query.capture_index_for_name("words").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes());
let mut words = Vec::new();
while let Some(m) = matches.next() {
for cap in m.captures {
if cap.index == words_idx {
words.extend(
cap.node.utf8_text(source.as_bytes()).unwrap()
.split_whitespace().map(String::from),
);
}
}
}
assert_eq!(words, vec!["lolz", "this", "is", "nested"]);
}
#[test]
fn test_exports_mixed_strings_and_qw() {
let source = "our @EXPORT_OK = ('now', 'what', qw/man this is stuffs/);\n";
let tree = parse(source);
let root = tree.root_node();
let bytes = source.as_bytes();
let qw = exports_qw();
let qw_words_idx = qw.capture_index_for_name("words").unwrap();
let mut cursor1 = QueryCursor::new();
let mut matches1 = cursor1.matches(qw, root, bytes);
let mut all_words = Vec::new();
while let Some(m) = matches1.next() {
for cap in m.captures {
if cap.index == qw_words_idx {
all_words.extend(
cap.node.utf8_text(bytes).unwrap()
.split_whitespace().map(String::from),
);
}
}
}
let pl = exports_paren_list();
let pl_word_idx = pl.capture_index_for_name("word").unwrap();
let mut cursor2 = QueryCursor::new();
let mut matches2 = cursor2.matches(pl, root, bytes);
while let Some(m) = matches2.next() {
for cap in m.captures {
if cap.index == pl_word_idx {
all_words.push(cap.node.utf8_text(bytes).unwrap().to_string());
}
}
}
all_words.sort();
assert_eq!(all_words, vec!["is", "man", "now", "stuffs", "this", "what"]);
}
#[test]
fn test_exports_empty_qw() {
let source = "our @EXPORT = qw();\n";
let tree = parse(source);
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(exports_qw(), tree.root_node(), source.as_bytes());
assert!(matches.next().is_none(), "empty qw should produce no matches");
}
#[test]
fn test_exports_empty_parens() {
let source = "our @EXPORT_OK = ();\n";
let tree = parse(source);
let mut cursor1 = QueryCursor::new();
let mut matches1 = cursor1.matches(exports_qw(), tree.root_node(), source.as_bytes());
assert!(matches1.next().is_none());
let mut cursor2 = QueryCursor::new();
let mut matches2 = cursor2.matches(exports_paren_list(), tree.root_node(), source.as_bytes());
assert!(matches2.next().is_none());
}
}