use std::ops::Range;
use std::sync::Arc;
use regex_automata::nfa::thompson::{self, pikevm};
use regex_automata::util::captures::Captures;
use regex_automata::util::syntax;
use regex_automata::{Anchored, Input, MatchKind, meta};
use crate::posix_regex::{Flavor, translate};
const NFA_SIZE_LIMIT: usize = 4 * 1024 * 1024;
#[derive(Clone, Debug, Default)]
pub struct FunctionLines(Kind);
#[derive(Clone, Debug, Default)]
enum Kind {
#[default]
Default,
Patterns(Arc<[Rule]>),
}
#[derive(Debug)]
struct Rule {
negated: bool,
find: meta::Regex,
longest: pikevm::PikeVM,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PatternError {
#[error("the last function pattern expression is negated")]
NegatedLast,
#[error("function pattern has an empty expression")]
Empty,
#[error("function pattern is not UTF-8")]
Encoding,
#[error("unsupported function pattern construct: {0}")]
Unsupported(&'static str),
#[error("invalid function pattern")]
Invalid(#[source] Box<dyn std::error::Error + Send + Sync>),
}
const RUST: &str = concat!(
r#"^[\t ]*((pub(\([^)]+\))?[\t ]+)?((async|const|unsafe|extern[\t ]+"[^"]+")[\t ]+)?"#,
r"(struct|enum|union|mod|trait|fn|impl|macro_rules!)[<\t ]+[^;]*)$",
);
impl FunctionLines {
pub fn extended(patterns: &[u8]) -> Result<Self, PatternError> {
Self::compile(patterns, Flavor::Extended)
}
pub fn basic(patterns: &[u8]) -> Result<Self, PatternError> {
Self::compile(patterns, Flavor::Basic)
}
#[must_use]
pub fn builtin(driver: &[u8]) -> Option<Self> {
match driver {
b"rust" => rule(RUST, false)
.ok()
.map(|rule| Self(Kind::Patterns(Arc::from([rule])))),
_ => None,
}
}
fn compile(patterns: &[u8], flavor: Flavor) -> Result<Self, PatternError> {
let patterns = std::str::from_utf8(patterns).map_err(|_| PatternError::Encoding)?;
let mut rules = Vec::new();
for line in patterns.split('\n') {
let (negated, expression) = match line.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, line),
};
if expression.is_empty() {
return Err(PatternError::Empty);
}
let expression = translate(expression, flavor).map_err(PatternError::Unsupported)?;
rules.push(rule(&expression, negated)?);
}
if rules.last().is_some_and(|rule| rule.negated) {
return Err(PatternError::NegatedLast);
}
Ok(Self(Kind::Patterns(rules.into())))
}
pub(crate) fn matcher(&self) -> Matcher<'_> {
Matcher {
lines: self,
caches: Vec::new(),
}
}
}
fn rule(expression: &str, negated: bool) -> Result<Rule, PatternError> {
let invalid = |error: Box<dyn std::error::Error + Send + Sync>| PatternError::Invalid(error);
let find = meta::Regex::builder()
.syntax(syntax::Config::new())
.configure(meta::Config::new().nfa_size_limit(Some(NFA_SIZE_LIMIT)))
.build(expression)
.map_err(|error| invalid(Box::new(error)))?;
let longest = pikevm::PikeVM::builder()
.syntax(syntax::Config::new())
.thompson(thompson::Config::new().nfa_size_limit(Some(NFA_SIZE_LIMIT)))
.configure(pikevm::Config::new().match_kind(MatchKind::All))
.build(expression)
.map_err(|error| invalid(Box::new(error)))?;
Ok(Rule {
negated,
find,
longest,
})
}
pub(crate) struct Matcher<'a> {
lines: &'a FunctionLines,
caches: Vec<(pikevm::Cache, Captures)>,
}
impl Matcher<'_> {
pub(crate) fn name(&mut self, line: &[u8]) -> Option<Range<usize>> {
let span = match &self.lines.0 {
Kind::Default => matches!(line.first(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$'))
.then_some(0..line.len()),
Kind::Patterns(rules) => {
if self.caches.is_empty() {
self.caches = rules
.iter()
.map(|rule| (rule.longest.create_cache(), rule.longest.create_captures()))
.collect();
}
rules
.iter()
.zip(&mut self.caches)
.find_map(|(rule, (cache, captures))| {
let found = rule.find.search(&Input::new(line))?;
let input = Input::new(line)
.range(found.start()..)
.anchored(Anchored::Yes);
rule.longest.search(cache, &input, captures);
let span = captures.get_group(1).or_else(|| captures.get_group(0))?;
Some((!rule.negated).then(|| span.range()))
})
.flatten()
}
}?;
let end = span.end.min(span.start + 80);
let kept = line.get(span.start..end)?.trim_ascii_end().len();
Some(span.start..span.start + kept)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::FunctionLines;
fn name(lines: &FunctionLines, line: &str) -> Option<String> {
let range = lines.matcher().name(line.as_bytes())?;
Some(line[range].to_owned())
}
#[rstest]
#[case::group(r"BEGIN (f[a-z]+)", " BEGIN foo(bar) tail ", Some("foo"))]
#[case::whole_match_from_start(
"BEGIN .*",
" BEGIN foo(bar) tail ",
Some("BEGIN foo(bar) tail")
)]
#[case::leftmost_longest("BEGIN (f|foo)", " BEGIN foo(bar)", Some("foo"))]
#[case::no_match("NOMATCH", "line", None)]
#[case::case_sensitive("bar", "fooBAR", None)]
#[case::end_anchor("end$", "line0 end", Some("end"))]
#[case::first_expression_decides("^ P.*\n!POS\n^ .*", " POS two", Some(" POS two"))]
#[case::negation_rejects("!POS\n^ .*", " POS two", None)]
fn extended_names(#[case] pattern: &str, #[case] line: &str, #[case] expected: Option<&str>) {
let lines = FunctionLines::extended(pattern.as_bytes()).unwrap();
assert_eq!(name(&lines, line).as_deref(), expected);
}
#[test]
fn cuts_to_80_bytes() {
let lines = FunctionLines::extended(b"BEGIN.*").unwrap();
let line = format!("BEGIN {} yyy", "x".repeat(73));
assert_eq!(
name(&lines, &line),
Some(format!("BEGIN {}", "x".repeat(73)))
);
}
#[rstest]
#[case::function("fn foo() {", Some("fn foo() {"))]
#[case::indented(
" pub(crate) fn foo(&self) -> u8 {",
Some("pub(crate) fn foo(&self) -> u8 {")
)]
#[case::restricted("pub(in crate::a) fn foo() {", Some("pub(in crate::a) fn foo() {"))]
#[case::qualifier("pub async fn foo() {", Some("pub async fn foo() {"))]
#[case::extern_abi("extern \"C\" fn foo() {", Some("extern \"C\" fn foo() {"))]
#[case::generic("impl<'a> Foo {", Some("impl<'a> Foo {"))]
#[case::macro_rules("macro_rules! foo {", Some("macro_rules! foo {"))]
#[case::keyword_only("fn ", Some("fn"))]
#[case::two_qualifiers("pub const unsafe fn foo() {", None)]
#[case::extern_without_abi("extern fn foo() {", None)]
#[case::semicolon("struct Foo;", None)]
#[case::declaration("mod foo;", None)]
#[case::attribute("#[test] fn foo() {", None)]
#[case::no_space_after_restriction("pub(crate)fn foo() {", None)]
#[case::brace_after_keyword("impl{", None)]
#[case::other_item("const FOO: u8 = 1;", None)]
fn rust_names(#[case] line: &str, #[case] expected: Option<&str>) {
let lines = FunctionLines::builtin(b"rust").unwrap();
assert_eq!(name(&lines, line).as_deref(), expected);
}
#[rstest]
#[case::negated_last(b"a\n!b")]
#[case::back_reference(br"(a)\1")]
#[case::equivalence_class(b"[[=a=]]")]
#[case::malformed(b"(")]
#[case::not_utf8(b"\xff")]
#[case::trailing_newline(b"^HEADER\n")]
#[case::empty_line(b"^a\n\n^b")]
#[case::empty(b"")]
fn refuses(#[case] pattern: &[u8]) {
assert!(FunctionLines::extended(pattern).is_err());
}
}