// Human-readable diagnostics for glob patterns.
//
// These APIs are intentionally independent from the matcher. They are useful
// in command-line tools, benchmark reports, and bug tickets where showing the
// parsed structure is more actionable than returning only true or false.
///|
/// A coarse risk category used to choose traversal and caching strategies.
pub enum PatternRisk {
Literal
Bounded
Recursive
HighBranching
} derive(Debug, Eq)
///|
/// A complete, stable explanation of one validated pattern.
pub(all) struct PatternExplanation {
source : String
normalized : String
tokens : Array[String]
ast_text : String
stats : PatternStats
risk : PatternRisk
literal_prefix : String
separator_count : Int
} derive(Debug, Eq)
///|
fn char_label(c : Char) -> String {
"'" + c.to_string() + "'"
}
///|
fn token_label(token : Token) -> String {
match token {
Token::Text(text) => "Text(\"" + text + "\")"
Token::Question => "Question"
Token::Star => "Star"
Token::GlobStar => "GlobStar"
Token::LBrace => "LBrace"
Token::RBrace => "RBrace"
Token::Comma => "Comma"
Token::LBracket => "LBracket"
Token::RBracket => "RBracket"
Token::Negate => "Negate"
Token::Char(c) => "Char(" + char_label(c) + ")"
Token::Range(start, end) =>
"Range(" + char_label(start) + "," + char_label(end) + ")"
}
}
///|
fn render_tokens(tokens : Array[Token]) -> Array[String] {
let result : Array[String] = []
for token in tokens {
result.push(token_label(token))
}
result
}
///|
fn render_elements(elements : Array[CharClassElement]) -> String {
let builder = StringBuilder::new()
builder.write_char('[')
for element in elements {
match element {
CharClassElement::Single(c) => builder.write_string(char_label(c))
CharClassElement::Range(start, end) => {
builder.write_string(char_label(start))
builder.write_char('-')
builder.write_string(char_label(end))
}
}
}
builder.write_char(']')
builder.to_string()
}
///|
fn render_ast_into(ast : AST, builder : StringBuilder) -> Unit {
match ast {
AST::Text(text) => builder.write_string("Text(\"" + text + "\")")
AST::Question => builder.write_string("Question")
AST::Star => builder.write_string("Star")
AST::GlobStar => builder.write_string("GlobStar")
AST::CharClass(negated, elements) => {
builder.write_string("CharClass(")
if negated {
builder.write_string("negated,")
} else {
builder.write_string("positive,")
}
builder.write_string(render_elements(elements))
builder.write_char(')')
}
AST::Brace(options) => {
builder.write_string("Brace(")
let mut first_option = true
for option in options {
if !first_option {
builder.write_char('|')
}
first_option = false
builder.write_char('[')
let mut first_node = true
for node in option {
if !first_node {
builder.write_char(',')
}
first_node = false
render_ast_into(node, builder)
}
builder.write_char(']')
}
builder.write_char(')')
}
AST::Seq(nodes) => {
builder.write_string("Seq(")
let mut first = true
for node in nodes {
if !first {
builder.write_char(',')
}
first = false
render_ast_into(node, builder)
}
builder.write_char(')')
}
}
}
///|
fn render_ast(ast : AST) -> String {
let builder = StringBuilder::new()
render_ast_into(ast, builder)
builder.to_string()
}
///|
fn classify_risk(stats : PatternStats) -> PatternRisk {
if stats.is_literal() {
PatternRisk::Literal
} else if stats.brace_group_count >= 3 ||
stats.char_class_count >= 4 ||
stats.star_count >= 5 {
PatternRisk::HighBranching
} else if stats.globstar_count > 0 {
PatternRisk::Recursive
} else {
PatternRisk::Bounded
}
}
///|
/// Parses a pattern and returns all information needed for diagnostics.
pub fn explain_pattern(
pattern : String,
) -> Result[PatternExplanation, GlobError] {
match tokenize(pattern) {
Err(err) => Err(err)
Ok(tokens) =>
match parse(tokens) {
Err(err) => Err(err)
Ok(ast) => {
let stats = {
..ast_stats(ast),
escaped_char_count: count_escaped_chars(pattern),
}
let prefix = literal_prefix(pattern).unwrap()
let separators = separator_count(pattern).unwrap()
Ok({
source: pattern,
normalized: normalize_path(pattern),
tokens: render_tokens(tokens),
ast_text: render_ast(ast),
stats,
risk: classify_risk(stats),
literal_prefix: prefix,
separator_count: separators,
})
}
}
}
}
///|
pub fn PatternExplanation::is_literal(self : PatternExplanation) -> Bool {
self.risk == PatternRisk::Literal
}
///|
pub fn PatternExplanation::complexity_score(self : PatternExplanation) -> Int {
self.stats.magic_count() * 10 + self.stats.escaped_char_count
}
///|
/// Returns a compact machine-readable category name.
pub fn PatternExplanation::risk_name(self : PatternExplanation) -> String {
match self.risk {
PatternRisk::Literal => "literal"
PatternRisk::Bounded => "bounded"
PatternRisk::Recursive => "recursive"
PatternRisk::HighBranching => "high-branching"
}
}
///|
/// Recommends a caller-visible execution strategy.
pub fn PatternExplanation::recommendation(self : PatternExplanation) -> String {
match self.risk {
PatternRisk::Literal => "use an exact path lookup"
PatternRisk::Bounded => "compile once and filter candidate paths"
PatternRisk::Recursive =>
"use the literal prefix to prune traversal before matching"
PatternRisk::HighBranching =>
"bound candidate breadth and report a match limit to the caller"
}
}
///|
/// Produces a stable identifier suitable for benchmark grouping.
pub fn PatternExplanation::signature(self : PatternExplanation) -> String {
"glob:v1:" +
self.risk_name() +
":" +
self.complexity_score().to_string() +
":" +
self.literal_prefix
}
///|
pub fn PatternExplanation::token_count(self : PatternExplanation) -> Int {
self.tokens.length()
}
///|
pub fn PatternExplanation::has_recursive_wildcard(
self : PatternExplanation,
) -> Bool {
self.stats.globstar_count > 0
}
///|
pub fn PatternExplanation::branch_count(self : PatternExplanation) -> Int {
let mut branches = 1
if self.stats.brace_group_count > 0 {
branches = branches * (self.stats.brace_group_count + 1)
}
if self.stats.char_class_count > 0 {
branches = branches * (self.stats.char_class_count + 1)
}
branches
}
///|
/// Renders a short one-line diagnostic for logs and CLI output.
pub fn PatternExplanation::summary(self : PatternExplanation) -> String {
"pattern=\"" +
self.source +
"\" risk=" +
self.risk_name() +
" complexity=" +
self.complexity_score().to_string() +
" prefix=\"" +
self.literal_prefix +
"\" tokens=" +
self.token_count().to_string()
}