use crate::Perl;
use crate::lang_helpers::doc_comment::pod_comment_region;
use crate::node::{Ancestors, Node};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PerlDataSectionRows {
Pod {
first: usize,
last: usize,
documentation: bool,
},
Data(usize),
}
pub fn perl_data_section_rows(
section: &[u8],
start_row: usize,
mut visit: impl FnMut(PerlDataSectionRows),
) {
let mut block: Option<(usize, usize, usize)> = None;
let mut lines = section.split(|&byte| byte == b'\n');
let mut offset = lines.next().map_or(0, |marker_rest| marker_rest.len() + 1);
for (row, line) in (start_row.saturating_add(1)..).zip(lines) {
let end = offset + line.len();
let blank = line.trim_ascii().is_empty();
let opens = block.is_none() && opens_pod(line);
if opens {
block = Some((row, offset, row));
}
match block {
Some((first, start, _)) if line.starts_with(b"=cut") => {
visit(pod_rows(section, first, start..end, row));
block = None;
}
_ if opens || blank => {}
Some((first, start, _)) => block = Some((first, start, row)),
None => visit(PerlDataSectionRows::Data(row)),
}
offset = end + 1;
}
if let Some((first, start, last)) = block {
visit(pod_rows(section, first, start..section.len(), last));
}
}
fn pod_rows(
section: &[u8],
first: usize,
bytes: std::ops::Range<usize>,
last: usize,
) -> PerlDataSectionRows {
PerlDataSectionRows::Pod {
first,
last,
documentation: !pod_comment_region(section.get(bytes).unwrap_or_default()),
}
}
fn opens_pod(line: &[u8]) -> bool {
let Some(rest) = line.strip_prefix(b"=") else {
return false;
};
let word = rest
.iter()
.position(|byte| !byte.is_ascii_alphanumeric())
.unwrap_or(rest.len());
let (command, after) = rest.split_at(word);
command.first().is_some_and(u8::is_ascii_alphabetic)
&& command != b"cut"
&& after
.first()
.is_none_or(|byte| byte.is_ascii_whitespace() || *byte == b'\x0b')
}
#[must_use]
pub fn perl_and_is_operator(and: &Node, parent: &Node) -> bool {
matches!(
parent.kind_id().into(),
Perl::UnaryExpression | Perl::BinaryExpression
) && first_non_extra(parent).is_some_and(|first| first.id() != and.id())
}
#[must_use]
pub fn perl_is_and_key_misparse(expr: &Node) -> bool {
first_non_extra(expr).is_some_and(|first| first.kind_id() == Perl::And as u16)
}
#[must_use]
pub fn perl_not_is_key<'a>(not: &Node<'a>, parent: &Node<'a>) -> bool {
parent.is_error()
|| parent
.children_after(not)
.find(|child| child.is_error() || !child.as_tree_sitter().is_extra())
.is_some_and(|next| {
(next.is_error() && next.is_child(Perl::FatComma as u16))
|| next.start_byte() == next.end_byte()
})
}
#[must_use]
pub(crate) fn perl_file_test_is_dash_key<'a>(
test: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> bool {
test.children()
.find(|child| child.is_error() || !child.as_tree_sitter().is_extra())
.is_some_and(|operand| {
if operand.is_error() {
operand.is_child(Perl::FatComma as u16) && operand.start_row() == test.start_row()
} else {
is_glued_word(&operand, test)
&& is_plain_bareword(&operand)
&& ancestors
.iter(test)
.next()
.is_some_and(|(key, above)| is_key_position(&key, above))
}
})
}
#[must_use]
pub(crate) fn perl_is_dash_key_word<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
ancestors.iter(node).next().is_some_and(|(test, above)| {
test.kind_id() == Perl::FileHandleOperator as u16
&& is_glued_word(node, &test)
&& perl_file_test_is_dash_key(&test, above)
})
}
#[must_use]
pub fn perl_bareword_is_key<'a>(word: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
if perl_is_dash_key_word(word, ancestors) {
return true;
}
if !is_plain_bareword(word) {
return false;
}
match ancestors.iter(word).next() {
Some((unary, above)) if dash_operator(&unary).is_some() => is_key_position(&unary, above),
_ => is_key_position(word, ancestors),
}
}
#[must_use]
pub fn perl_bareword_is_call<'a>(word: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
!ancestors.parent(word).is_some_and(|parent| {
matches!(
parent.kind_id().into(),
Perl::CallExpressionWithSpacedArgs
| Perl::CallExpressionWithSub
| Perl::CallExpressionWithArgsWithBrackets
| Perl::CallExpressionWithVariable
| Perl::CallExpressionRecursive
)
}) && !perl_bareword_is_key(word, ancestors)
}
#[must_use]
pub(crate) fn perl_dash_signs_key<'a>(dash: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
perl_dash_signs_keyword_key(dash, ancestors)
|| ancestors.iter(dash).next().is_some_and(|(parent, above)| {
parent.kind_id() == Perl::UnaryExpression as u16
&& next_non_extra(dash, ancestors).is_some_and(|word| is_plain_bareword(&word))
&& is_key_position(&parent, above)
})
}
#[must_use]
pub(crate) fn perl_bareword_key_sign<'a>(
identifier: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> Option<Node<'a>> {
let mut up = ancestors.iter(identifier);
let (_, word_ancestors) = up
.next()
.filter(|(word, _)| word.kind_id() == Perl::CallExpressionWithBareword as u16)?;
let (unary, _) = up.next()?;
let dash = dash_operator(&unary)?;
perl_dash_signs_key(&dash, word_ancestors).then_some(dash)
}
#[must_use]
pub fn perl_dash_signs_keyword_key<'a>(dash: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
let Some(parent) = ancestors
.parent(dash)
.filter(|parent| parent.kind_id() != Perl::BinaryExpression as u16)
else {
return false;
};
next_non_extra(dash, ancestors).is_some_and(|next| {
keyword_is_key(&next, &parent)
|| first_non_extra(&next).is_some_and(|keyword| keyword_is_key(&keyword, &next))
})
}
#[must_use]
pub fn perl_key_is_signed<'a>(key: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
let mut up = ancestors.iter(key);
up.next().is_some_and(|(_, parent_ancestors)| {
let beside = ancestors
.siblings_before(key)
.find(|sibling| !sibling.as_tree_sitter().is_extra());
if let Some(dash) = beside.filter(|node| node.kind_id() == Perl::DASH as u16) {
return perl_dash_signs_keyword_key(&dash, ancestors);
}
up.next().is_some_and(|(outer, _)| {
dash_operator(&outer)
.is_some_and(|dash| perl_dash_signs_keyword_key(&dash, parent_ancestors))
})
})
}
fn keyword_is_key(token: &Node, parent: &Node) -> bool {
match token.kind_id().into() {
Perl::And => !perl_and_is_operator(token, parent),
Perl::Not => perl_not_is_key(token, parent),
_ => false,
}
}
const FILE_TEST_TOKEN_LEN: usize = 2;
fn is_glued_word(word: &Node, test: &Node) -> bool {
word.kind_id() == Perl::CallExpressionWithBareword as u16
&& word.start_byte() == test.start_byte() + FILE_TEST_TOKEN_LEN
}
fn is_key_position<'a>(key: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
let (mut node, mut node_ancestors) = (*key, ancestors);
let mut whole = true;
for (parent, above) in ancestors.iter(key) {
if whole
&& matches!(
parent.kind_id().into(),
Perl::HashAccessVariableSimple | Perl::HashAccessVariable
)
{
return true;
}
if let Some(next) = node_ancestors
.siblings_after(&node)
.find(|sibling| sibling.is_error() || !sibling.as_tree_sitter().is_extra())
{
return next.kind_id() == Perl::FatComma as u16;
}
whole &= parent.kind_id() == Perl::BinaryExpression as u16 && non_extra_count(&parent) == 1;
(node, node_ancestors) = (parent, above);
}
false
}
fn is_plain_bareword(word: &Node) -> bool {
if word.kind_id() != Perl::CallExpressionWithBareword as u16 {
return false;
}
match word.child_count() {
1 => word
.child(0)
.is_some_and(|child| child.kind_id() == Perl::Identifier as u16),
_ => word
.children()
.filter(|child| !child.as_tree_sitter().is_extra())
.map(|child| child.kind_id())
.eq([Perl::Identifier as u16]),
}
}
fn dash_operator<'a>(node: &Node<'a>) -> Option<Node<'a>> {
if node.kind_id() != Perl::UnaryExpression as u16 {
return None;
}
node.child_by_field_name("operator")
.filter(|op| op.kind_id() == Perl::DASH as u16)
}
fn non_extra_count(node: &Node) -> usize {
node.children()
.filter(|child| !child.as_tree_sitter().is_extra())
.count()
}
fn next_non_extra<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> Option<Node<'a>> {
ancestors
.siblings_after(node)
.find(|sibling| !sibling.as_tree_sitter().is_extra())
}
fn first_non_extra<'a>(node: &Node<'a>) -> Option<Node<'a>> {
node.children()
.find(|child| !child.as_tree_sitter().is_extra())
}