use crate::ast::{Node, NodeKind};
use perl_semantic_facts::{Confidence, EntityId, FileId, ValueShape};
pub struct ValueShapeInferrer;
impl ValueShapeInferrer {
pub fn infer(ast: &Node, _file_id: FileId) -> Vec<(EntityId, ValueShape)> {
let mut state = InferrerState {
current_package: "main".to_string(),
in_method: false,
results: Vec::new(),
};
state.walk(ast);
state.results
}
}
struct InferrerState {
current_package: String,
in_method: bool,
results: Vec<(EntityId, ValueShape)>,
}
impl InferrerState {
fn walk(&mut self, node: &Node) {
match &node.kind {
NodeKind::Program { statements } | NodeKind::Block { statements } => {
for stmt in statements {
self.walk(stmt);
}
return;
}
NodeKind::Package { name, block: Some(block), .. } => {
let prev = self.current_package.clone();
self.current_package = name.clone();
self.walk(block);
self.current_package = prev;
return;
}
NodeKind::Package { name, block: None, .. } => {
self.current_package = name.clone();
return;
}
NodeKind::Subroutine { body, .. } | NodeKind::Method { body, .. } => {
let prev_in_method = self.in_method;
self.in_method = true;
self.walk(body);
self.in_method = prev_in_method;
return;
}
NodeKind::VariableDeclaration { variable, initializer: Some(init), .. } => {
if let Some(shape) = self.infer_from_rhs(init) {
let entity_id = entity_id_from_variable(variable);
self.results.push((entity_id, shape));
}
}
NodeKind::Assignment { lhs, rhs, .. } => {
if let Some(shape) = self.infer_from_rhs(rhs) {
let entity_id = entity_id_from_variable(lhs);
self.results.push((entity_id, shape));
}
}
NodeKind::Variable { sigil, name } if sigil == "$" && name == "self" => {
if self.in_method {
let entity_id = entity_id_from_node(node);
self.results.push((
entity_id,
ValueShape::Object {
package: self.current_package.clone(),
confidence: Confidence::Medium,
},
));
}
}
_ => {}
}
for child in node.children() {
self.walk(child);
}
}
fn infer_from_rhs(&self, rhs: &Node) -> Option<ValueShape> {
match &rhs.kind {
NodeKind::MethodCall { object, method, .. } if method == "new" => {
if let Some(pkg) = package_name_from_node(object) {
return Some(ValueShape::Object { package: pkg, confidence: Confidence::High });
}
None
}
NodeKind::FunctionCall { name, args } if name == "bless" => {
if let Some(pkg_node) = args.get(1) {
if let Some(pkg) = string_value(pkg_node) {
return Some(ValueShape::Object {
package: pkg,
confidence: Confidence::Low,
});
}
}
if args.len() == 1 {
return Some(ValueShape::Object {
package: self.current_package.clone(),
confidence: Confidence::Low,
});
}
None
}
_ => None,
}
}
}
fn package_name_from_node(node: &Node) -> Option<String> {
match &node.kind {
NodeKind::Identifier { name } => Some(name.clone()),
NodeKind::String { value, .. } => normalize_package_string(value),
_ => None,
}
}
fn string_value(node: &Node) -> Option<String> {
match &node.kind {
NodeKind::String { value, .. } => normalize_package_string(value),
NodeKind::Identifier { name } => Some(name.clone()),
_ => None,
}
}
fn normalize_package_string(value: &str) -> Option<String> {
let normalized = value.trim().trim_matches('\'').trim_matches('"').trim();
if normalized.is_empty() { None } else { Some(normalized.to_string()) }
}
fn entity_id_from_variable(node: &Node) -> EntityId {
entity_id_from_node(node)
}
fn entity_id_from_node(node: &Node) -> EntityId {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in (node.location.start as u64).to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
for byte in (node.location.end as u64).to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
EntityId(hash)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Parser;
fn parse_and_infer(code: &str) -> Vec<(EntityId, ValueShape)> {
let mut parser = Parser::new(code);
let ast = match parser.parse() {
Ok(ast) => ast,
Err(_) => return Vec::new(),
};
ValueShapeInferrer::infer(&ast, FileId(1))
}
fn first_object(results: &[(EntityId, ValueShape)]) -> Option<(&str, Confidence)> {
for (_, shape) in results {
if let ValueShape::Object { package, confidence } = shape {
return Some((package.as_str(), *confidence));
}
}
None
}
#[test]
fn constructor_call_infers_object_high() -> Result<(), String> {
let results = parse_and_infer("my $obj = Foo->new();\n");
let (pkg, conf) = first_object(&results).ok_or("expected Object shape from Foo->new()")?;
assert_eq!(pkg, "Foo");
assert_eq!(conf, Confidence::High);
Ok(())
}
#[test]
fn qualified_constructor_call_infers_object() -> Result<(), String> {
let results = parse_and_infer("my $obj = My::App->new();\n");
let (pkg, conf) =
first_object(&results).ok_or("expected Object shape from My::App->new()")?;
assert_eq!(pkg, "My::App");
assert_eq!(conf, Confidence::High);
Ok(())
}
#[test]
fn bless_with_package_infers_object_low() -> Result<(), String> {
let code = "package Foo;\nsub new { my $self = bless {}, 'Foo'; }\n";
let results = parse_and_infer(code);
let (pkg, conf) =
first_object(&results).ok_or("expected Object shape from bless {}, 'Foo'")?;
assert_eq!(pkg, "Foo");
assert_eq!(conf, Confidence::Low);
Ok(())
}
#[test]
fn self_in_method_infers_enclosing_package() -> Result<(), String> {
let code = "package Bar;\nsub greet { my $msg = $self->name(); }\n";
let results = parse_and_infer(code);
let has_bar_medium = results.iter().any(|(_, shape)| {
matches!(shape, ValueShape::Object { package, confidence }
if package == "Bar" && *confidence == Confidence::Medium)
});
assert!(
has_bar_medium,
"expected $self to infer Object {{ Bar, Medium }}, got {results:?}"
);
Ok(())
}
#[test]
fn plain_scalar_produces_no_shape() -> Result<(), String> {
let results = parse_and_infer("my $x = 42;\n");
assert!(first_object(&results).is_none(), "plain scalar should not produce Object shape");
Ok(())
}
#[test]
fn multiple_packages_track_context() -> Result<(), String> {
let code = r#"
package Alpha;
sub new { my $self = bless {}, 'Alpha'; }
package Beta;
sub new { my $self = bless {}, 'Beta'; }
"#;
let results = parse_and_infer(code);
let has_alpha = results.iter().any(
|(_, shape)| matches!(shape, ValueShape::Object { package, .. } if package == "Alpha"),
);
let has_beta = results.iter().any(
|(_, shape)| matches!(shape, ValueShape::Object { package, .. } if package == "Beta"),
);
assert!(has_alpha, "expected Alpha object shape");
assert!(has_beta, "expected Beta object shape");
Ok(())
}
}