use perl_ast::classification::NodeKindCategory;
use perl_ast::{Node, NodeKind, SourceLocation};
#[path = "helpers.rs"]
mod helpers;
fn loc() -> SourceLocation {
SourceLocation::new(0, 1)
}
fn leaf() -> Node {
Node::new(NodeKind::Identifier { name: "x".to_string() }, loc())
}
fn block_node() -> Node {
Node::new(NodeKind::Block { statements: vec![] }, loc())
}
fn all_variants() -> Vec<NodeKind> {
helpers::all_nodekind_instances().into_iter().map(|n| n.kind).collect()
}
#[test]
fn all_variants_have_category_and_flags() {
for kind in all_variants() {
let _cat = kind.category();
let flags = kind.flags();
let _ = flags;
}
}
#[test]
fn recovery_implies_not_safe_for_breakpoint() {
for kind in all_variants() {
let flags = kind.flags();
if flags.recovery_artifact {
assert!(
!flags.safe_for_breakpoint,
"variant {:?} has recovery_artifact=true but safe_for_breakpoint=true",
kind.kind_name(),
);
}
}
}
const SAFE_FOR_BREAKPOINT_TRUE: &[&str] = &[
"ExpressionStatement",
"VariableDeclaration",
"VariableListDeclaration",
"Assignment",
"Binary",
"Ternary",
"Unary",
"Diamond",
"Readline",
"Glob",
"Heredoc",
"Block",
"Eval",
"Do",
"Defer",
"Try",
"If",
"LabeledStatement",
"While",
"Tie",
"Untie",
"For",
"Foreach",
"Given",
"When",
"Default",
"StatementModifier",
"Subroutine",
"Method",
"Return",
"LoopControl",
"Goto",
"MethodCall",
"FunctionCall",
"IndirectCall",
"Match",
"Substitution",
"Transliteration",
"Package",
"PhaseBlock",
"Class",
];
const SAFE_FOR_BREAKPOINT_FALSE: &[&str] = &[
"Program",
"Variable",
"VariableWithAttributes",
"Ellipsis",
"Undef",
"Number",
"String",
"ArrayLiteral",
"HashLiteral",
"Typeglob",
"Regex",
"Prototype",
"Signature",
"MandatoryParameter",
"OptionalParameter",
"SlurpyParameter",
"NamedParameter",
"DataSection",
"Format",
"Identifier",
"Use",
"No",
"NestedVariableList",
"Error",
"MissingExpression",
"MissingStatement",
"MissingIdentifier",
"MissingBlock",
"UnknownRest",
];
#[test]
fn safe_for_breakpoint_exact_true_set() {
for kind in all_variants() {
let name = kind.kind_name();
let flags = kind.flags();
if SAFE_FOR_BREAKPOINT_TRUE.contains(&name) {
assert!(
flags.safe_for_breakpoint,
"expected {name} to be safe_for_breakpoint=true, got false",
);
}
}
}
#[test]
fn safe_for_breakpoint_exact_false_set() {
for kind in all_variants() {
let name = kind.kind_name();
let flags = kind.flags();
if SAFE_FOR_BREAKPOINT_FALSE.contains(&name) {
assert!(
!flags.safe_for_breakpoint,
"expected {name} to be safe_for_breakpoint=false, got true",
);
}
}
}
#[test]
fn safe_for_breakpoint_covers_all_70_variants() {
for kind in all_variants() {
let name = kind.kind_name();
let in_true = SAFE_FOR_BREAKPOINT_TRUE.contains(&name);
let in_false = SAFE_FOR_BREAKPOINT_FALSE.contains(&name);
assert!(
in_true ^ in_false,
"variant {name} must appear in exactly one of the safe_for_breakpoint lists"
);
}
}
#[test]
fn category_spot_checks() {
assert_eq!(NodeKind::Program { statements: vec![] }.category(), NodeKindCategory::Program);
assert_eq!(
NodeKind::ExpressionStatement { expression: Box::new(leaf()) }.category(),
NodeKindCategory::Statement,
);
assert_eq!(
NodeKind::VariableDeclaration {
declarator: "my".to_string(),
variable: Box::new(leaf()),
attributes: vec![],
initializer: None,
}
.category(),
NodeKindCategory::Declaration
);
assert_eq!(
NodeKind::Variable { sigil: "$".to_string(), name: "x".to_string() }.category(),
NodeKindCategory::Expression
);
assert_eq!(NodeKind::Number { value: "1".to_string() }.category(), NodeKindCategory::Literal);
assert_eq!(NodeKind::Ellipsis.category(), NodeKindCategory::Operator);
assert_eq!(NodeKind::Block { statements: vec![] }.category(), NodeKindCategory::Scope);
assert_eq!(
NodeKind::Error { message: "e".to_string(), expected: vec![], found: None, partial: None }
.category(),
NodeKindCategory::Recovery
);
assert_eq!(
NodeKind::DataSection { marker: "__DATA__".to_string(), body: None }.category(),
NodeKindCategory::Declaration
);
assert_eq!(
NodeKind::PhaseBlock {
phase: "BEGIN".to_string(),
phase_span: None,
block: Box::new(block_node()),
}
.category(),
NodeKindCategory::Declaration
);
assert_eq!(
NodeKind::Class {
name: "Foo".to_string(),
name_span: None,
parents: vec![],
body: Box::new(block_node())
}
.category(),
NodeKindCategory::Declaration
);
assert_eq!(
NodeKind::Subroutine {
name: Some("foo".to_string()),
name_span: None,
declarator: None,
prototype: None,
signature: None,
attributes: vec![],
body: Box::new(block_node()),
}
.category(),
NodeKindCategory::Declaration
);
}
#[test]
fn convenience_accessors_match_flags() {
for kind in all_variants() {
let flags = kind.flags();
assert_eq!(
kind.is_executable(),
flags.executable,
"is_executable mismatch for {}",
kind.kind_name()
);
assert_eq!(
kind.introduces_scope(),
flags.introduces_scope,
"introduces_scope mismatch for {}",
kind.kind_name()
);
assert_eq!(
kind.declares_symbol(),
flags.declares_symbol,
"declares_symbol mismatch for {}",
kind.kind_name()
);
assert_eq!(
kind.references_symbol(),
flags.references_symbol,
"references_symbol mismatch for {}",
kind.kind_name()
);
assert_eq!(
kind.safe_for_breakpoint(),
flags.safe_for_breakpoint,
"safe_for_breakpoint mismatch for {}",
kind.kind_name()
);
assert_eq!(
kind.is_recovery(),
flags.recovery_artifact,
"is_recovery mismatch for {}",
kind.kind_name()
);
}
}
#[test]
fn flags_validate_all_variants() {
for kind in all_variants() {
let flags = kind.flags();
assert!(
flags.validate().is_ok(),
"flags.validate() failed for variant {}: {:?}",
kind.kind_name(),
flags
);
}
}
#[test]
fn recovery_category_implies_recovery_artifact() {
for kind in all_variants() {
let flags = kind.flags();
let cat = kind.category();
if cat == NodeKindCategory::Recovery {
assert!(
flags.recovery_artifact,
"variant {} has Recovery category but recovery_artifact=false",
kind.kind_name()
);
}
}
}
#[test]
fn flags_spot_checks() {
{
let kind = NodeKind::Program { statements: vec![] };
let f = kind.flags();
assert!(!f.executable, "Program.executable should be false");
assert!(f.introduces_scope, "Program.introduces_scope should be true");
assert!(f.contains_children, "Program.contains_children should be true");
assert!(!f.safe_for_breakpoint, "Program.safe_for_breakpoint should be false");
}
{
let kind = NodeKind::Subroutine {
name: Some("foo".to_string()),
name_span: None,
declarator: None,
prototype: None,
signature: None,
attributes: vec![],
body: Box::new(block_node()),
};
let f = kind.flags();
assert!(!f.executable, "Subroutine.executable should be false");
assert!(f.introduces_scope, "Subroutine.introduces_scope should be true");
assert!(f.declares_symbol, "Subroutine.declares_symbol should be true");
assert!(f.safe_for_breakpoint, "Subroutine.safe_for_breakpoint should be true");
}
{
let kind = NodeKind::Error {
message: "e".to_string(),
expected: vec![],
found: None,
partial: None,
};
let f = kind.flags();
assert!(f.recovery_artifact, "Error.recovery_artifact should be true");
assert!(!f.safe_for_breakpoint, "Error.safe_for_breakpoint should be false");
}
{
let kind = NodeKind::Package { name: "Foo".to_string(), name_span: loc(), block: None };
let f = kind.flags();
assert!(f.executable, "Package.executable should be true");
assert!(f.introduces_scope, "Package.introduces_scope should be true");
assert!(f.declares_symbol, "Package.declares_symbol should be true");
assert!(f.safe_for_breakpoint, "Package.safe_for_breakpoint should be true");
}
{
let kind = NodeKind::Identifier { name: "foo".to_string() };
let f = kind.flags();
assert!(!f.executable, "Identifier.executable should be false");
assert!(f.references_symbol, "Identifier.references_symbol should be true");
assert!(!f.safe_for_breakpoint, "Identifier.safe_for_breakpoint should be false");
}
{
let kind = NodeKind::Heredoc {
delimiter: "EOF".to_string(),
content: "body".to_string(),
interpolated: false,
indented: false,
command: false,
body_span: None,
};
let f = kind.flags();
assert!(f.executable, "Heredoc.executable should be true");
assert!(f.safe_for_breakpoint, "Heredoc.safe_for_breakpoint should be true");
}
}
#[test]
fn all_kind_names_count_matches_variants() {
let constructed = all_variants().len();
let named = NodeKind::ALL_KIND_NAMES.len();
assert_eq!(
constructed, named,
"all_variants() produced {constructed} variants but ALL_KIND_NAMES has {named} entries"
);
}
#[test]
fn instance_dependent_flags_eval() {
let eval_with_block = NodeKind::Eval { block: Box::new(block_node()) };
let f = eval_with_block.flags();
assert!(
f.safe_for_breakpoint,
"Eval variant flag safe_for_breakpoint should be true (prefilter)"
);
assert!(
f.introduces_scope,
"Eval variant flag introduces_scope should be true (conservative prefilter)"
);
}
#[test]
fn instance_dependent_flags_package() {
let pkg_with_block = NodeKind::Package {
name: "Foo".to_string(),
name_span: loc(),
block: Some(Box::new(block_node())),
};
let f = pkg_with_block.flags();
assert!(
f.safe_for_breakpoint,
"Package variant flag safe_for_breakpoint should be true (prefilter)"
);
assert!(
f.introduces_scope,
"Package variant flag introduces_scope should be true (conservative prefilter)"
);
let pkg_without_block =
NodeKind::Package { name: "Bar".to_string(), name_span: loc(), block: None };
let f2 = pkg_without_block.flags();
assert!(
f2.safe_for_breakpoint,
"Package variant flag safe_for_breakpoint should be true even without block (prefilter)"
);
assert!(
f2.introduces_scope,
"Package variant flag introduces_scope should be true even without block (prefilter)"
);
}
#[test]
fn instance_dependent_flags_phaseblock() {
let begin_block = NodeKind::PhaseBlock {
phase: "BEGIN".to_string(),
phase_span: None,
block: Box::new(block_node()),
};
let f = begin_block.flags();
assert!(
f.safe_for_breakpoint,
"PhaseBlock variant flag safe_for_breakpoint should be true (has block, prefilter)"
);
assert!(
f.introduces_scope,
"PhaseBlock variant flag introduces_scope should be true (has block, prefilter)"
);
let end_block = NodeKind::PhaseBlock {
phase: "END".to_string(),
phase_span: None,
block: Box::new(block_node()),
};
let f2 = end_block.flags();
assert!(
f2.safe_for_breakpoint,
"PhaseBlock variant flag safe_for_breakpoint should be true (all phases have same variant flags)"
);
assert!(
f2.introduces_scope,
"PhaseBlock variant flag introduces_scope should be true (all phases have same variant flags)"
);
}
#[test]
fn use_not_safe_for_breakpoint() {
let use_kind =
NodeKind::Use { module: "strict".to_string(), args: vec![], has_filter_risk: false };
assert!(
!use_kind.flags().safe_for_breakpoint,
"Use must have safe_for_breakpoint=false (compile-time pragma)"
);
}
#[test]
fn no_not_safe_for_breakpoint() {
let no_kind =
NodeKind::No { module: "warnings".to_string(), args: vec![], has_filter_risk: false };
assert!(
!no_kind.flags().safe_for_breakpoint,
"No must have safe_for_breakpoint=false (compile-time unimport)"
);
}