use super::*;
use tree_sitter::{Parser, Point};
fn parse(source: &str) -> FileAnalysis {
let mut parser = Parser::new();
parser
.set_language(&ts_parser_perl::LANGUAGE.into())
.unwrap();
let tree = parser.parse(source, None).unwrap();
crate::builder::build(&tree, source.as_bytes())
}
fn ref_idx<F: Fn(&RefKind) -> bool>(fa: &FileAnalysis, name: &str, kind_pred: F) -> usize {
fa.refs
.iter()
.enumerate()
.find(|(_, r)| r.target_name == name && kind_pred(&r.kind))
.map(|(i, _)| i)
.unwrap_or_else(|| {
panic!(
"no ref with target_name={:?} matching predicate; refs: {:?}",
name,
fa.refs.iter().map(|r| (&r.target_name, &r.kind)).collect::<Vec<_>>(),
)
})
}
fn is_method_call(k: &RefKind) -> bool {
matches!(k, RefKind::MethodCall { .. })
}
fn is_function_call(k: &RefKind) -> bool {
matches!(k, RefKind::FunctionCall { .. })
}
#[test]
fn smallest_span_wins_method_chain() {
let fa = parse(
r#"
package Foo;
sub new { bless {}, shift }
sub bar { 1 }
package main;
Foo->new->bar();
"#,
);
let bar_idx = ref_idx(&fa, "bar", is_method_call);
let new_idx = ref_idx(&fa, "new", is_method_call);
let bar = &fa.refs[bar_idx];
let RefKind::MethodCall { invocant_span: Some(bar_invocant_span), .. } = &bar.kind else {
panic!("bar is a MethodCall");
};
let key = bar_invocant_span.start;
let resolved = fa.call_ref_by_start.get(&key).copied();
assert_eq!(
resolved,
Some(new_idx),
"lookup at the chain's leftmost point must return the \
inner `new` MethodCall (smaller span), not the outer `bar`. \
got: {:?} (bar={}, new={})",
resolved, bar_idx, new_idx,
);
assert_ne!(resolved, Some(bar_idx));
}
#[test]
fn smallest_span_wins_function_call_inner() {
let fa = parse(
r#"
sub make_b { bless {}, 'B' }
make_b()->touch();
"#,
);
let touch_idx = ref_idx(&fa, "touch", is_method_call);
let make_b_idx = ref_idx(&fa, "make_b", is_function_call);
let touch = &fa.refs[touch_idx];
let RefKind::MethodCall { invocant_span: Some(touch_invocant_span), .. } = &touch.kind else {
panic!("touch is a MethodCall");
};
let resolved = fa.call_ref_by_start.get(&touch_invocant_span.start).copied();
assert_eq!(
resolved,
Some(make_b_idx),
"lookup at touch's invocant_span.start must return the \
`make_b` FunctionCall ref (narrower span). got: {:?}",
resolved,
);
}
#[test]
fn non_call_kinds_at_call_start_dont_enter_index() {
let fa = parse(
r#"
package Foo;
sub new { bless {}, shift }
sub m { 1 }
package main;
my $x = Foo->new;
$x->m();
my $k = $x->{key};
"#,
);
for (&_pt, &idx) in fa.call_ref_by_start.iter() {
let kind = &fa.refs[idx].kind;
assert!(
matches!(kind, RefKind::MethodCall { .. } | RefKind::FunctionCall { .. }),
"non-call kind in call_ref_by_start: {:?}",
kind,
);
}
let m_idx = ref_idx(&fa, "m", is_method_call);
let m = &fa.refs[m_idx];
let RefKind::MethodCall { invocant_span: Some(m_invocant_span), .. } = &m.kind else {
panic!("m is a MethodCall");
};
let key_point = m_invocant_span.start;
let resolved = fa.call_ref_by_start.get(&key_point).copied();
assert_eq!(
resolved,
Some(m_idx),
"only call ref at $$x's start is `m` itself",
);
let non_call_at_same_start = fa
.refs
.iter()
.any(|r| {
r.span.start == key_point
&& !matches!(r.kind, RefKind::MethodCall { .. } | RefKind::FunctionCall { .. })
});
assert!(
non_call_at_same_start,
"fixture should produce at least one non-call ref starting at $$x's point; \
refs: {:?}",
fa.refs
.iter()
.map(|r| (r.span.start, &r.target_name, &r.kind))
.collect::<Vec<_>>(),
);
}
#[test]
fn is_self_guard_resolves_bareword_invocant() {
let fa = parse(
r#"
package Foo;
sub m { 1 }
package main;
Foo->m();
"#,
);
let m = &fa.refs[ref_idx(&fa, "m", is_method_call)];
let RefKind::MethodCall { invocant_span: Some(span), .. } = &m.kind else {
panic!("m is a MethodCall");
};
let resolved = fa.call_ref_by_start.get(&span.start).copied();
let m_idx = ref_idx(&fa, "m", is_method_call);
assert_eq!(resolved, Some(m_idx));
let class = fa.method_call_invocant_class(m, None);
assert_eq!(class.as_deref(), Some("Foo"));
}
#[test]
fn is_self_guard_falls_through_to_variable_branch() {
let fa = parse(
r#"
package Bar;
sub new { bless {}, shift }
sub m { 1 }
package main;
my $x = Bar->new;
$x->m();
"#,
);
let m = &fa.refs[ref_idx(&fa, "m", is_method_call)];
let RefKind::MethodCall { invocant_span: Some(span), .. } = &m.kind else {
panic!("m is a MethodCall");
};
let resolved = fa.call_ref_by_start.get(&span.start).copied();
let m_idx = ref_idx(&fa, "m", is_method_call);
assert_eq!(
resolved,
Some(m_idx),
"with no inner call receiver, the index points at the outer \
method call itself; the `is_self` guard must keep us from \
recursing on it",
);
assert_eq!(fa.method_call_invocant_class(m, None).as_deref(), Some("Bar"));
}
#[test]
fn equal_span_first_write_wins() {
let span = Span {
start: Point::new(0, 0),
end: Point::new(0, 10),
};
let refs = vec![
Ref {
kind: RefKind::MethodCall {
invocant: "$a".into(),
invocant_span: Some(Span {
start: Point::new(0, 0),
end: Point::new(0, 2),
}),
method_name_span: Span {
start: Point::new(0, 4),
end: Point::new(0, 5),
},
},
span,
scope: ScopeId(0),
target_name: "first".into(),
access: AccessKind::Read,
resolves_to: None,
},
Ref {
kind: RefKind::MethodCall {
invocant: "$b".into(),
invocant_span: Some(Span {
start: Point::new(0, 0),
end: Point::new(0, 2),
}),
method_name_span: Span {
start: Point::new(0, 6),
end: Point::new(0, 7),
},
},
span,
scope: ScopeId(0),
target_name: "second".into(),
access: AccessKind::Read,
resolves_to: None,
},
];
let fa = FileAnalysis::new(
vec![Scope {
id: ScopeId(0),
parent: None,
kind: ScopeKind::File,
span: Span {
start: Point::new(0, 0),
end: Point::new(10, 0),
},
package: None,
}],
vec![],
refs,
vec![],
vec![],
vec![],
HashMap::new(),
vec![],
HashSet::new(),
vec![],
vec![],
vec![],
HashMap::new(),
HashMap::new(),
vec![],
);
let resolved = fa.call_ref_by_start.get(&span.start).copied();
assert_eq!(
resolved,
Some(0),
"first insert wins on equal spans; got idx {:?}",
resolved,
);
}
#[test]
fn deep_chain_terminates() {
let fa = parse(
r#"
sub a { 1 }
a()->b()->c()->d()->e()->f();
"#,
);
let f_idx = ref_idx(&fa, "f", is_method_call);
let f = &fa.refs[f_idx];
let _ = fa.method_call_invocant_class(f, None);
}
#[test]
fn index_only_holds_call_shaped_kinds() {
let fa = parse(
r#"
package Foo;
sub new { bless {}, shift }
sub bar { 1 }
package main;
my $x = Foo->new;
$x->bar();
make_b()->touch();
sub make_b { 1 }
sub touch { 1 }
"#,
);
for (&_pt, &idx) in fa.call_ref_by_start.iter() {
let kind = &fa.refs[idx].kind;
assert!(
matches!(kind, RefKind::MethodCall { .. } | RefKind::FunctionCall { .. }),
"call_ref_by_start contains non-call kind: {:?}",
kind,
);
}
}