use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::file_analysis::{
HashKeyOwner, InferredType, ParametricType, Scope, ScopeId, Span, SymbolId,
};
use tree_sitter::Point;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Witness {
pub attachment: WitnessAttachment,
pub source: WitnessSource,
pub payload: WitnessPayload,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WitnessAttachment {
Variable { name: String, scope: ScopeId },
Expression(RefIdx),
Symbol(SymbolId),
CallSite(RefIdx),
HashKey { owner: HashKeyOwner, name: String },
Package(String),
Expr(Span),
MethodOnClass { class: String, name: String },
SymbolReturnArm(SymbolId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RefIdx(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WitnessSource {
Builder(String),
Plugin(String),
Enrichment(String),
DerivedFrom(RefIdx),
}
impl WitnessSource {
pub fn priority(&self) -> u8 {
match self {
WitnessSource::Plugin(_) => 100,
WitnessSource::Builder(_)
| WitnessSource::Enrichment(_)
| WitnessSource::DerivedFrom(_) => 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum WitnessPayload {
InferredType(InferredType),
Observation(TypeObservation),
Edge(WitnessAttachment),
ReturnExpr(ReturnExpr),
Fact { family: String, key: String, value: FactValue },
Derivation,
Custom { family: String, json: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ReturnExpr {
Concrete(InferredType),
Receiver,
Operator(ParametricOp),
UnionOnArgs { branches: Vec<(ArgGuard, ReturnExpr)> },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ParametricOp {
RowOf(Box<ReturnExpr>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ArgGuard {
Empty,
Exact(u32),
AtLeast(u32),
Any,
}
impl ArgGuard {
pub fn matches(self, arity_hint: Option<u32>) -> bool {
match (self, arity_hint) {
(ArgGuard::Empty, Some(0)) => true,
(ArgGuard::Exact(n), Some(h)) => n == h,
(ArgGuard::AtLeast(n), Some(h)) => h >= n,
(ArgGuard::Any, _) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TypeObservation {
ClassAssertion(String),
FirstParamInMethod { package: String },
HashRefAccess,
ArrayRefAccess,
CodeRefInvocation,
NumericUse,
StringUse,
RegexpUse,
BlessTarget(Rep),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Rep {
Hash,
Array,
Scalar,
Code,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FactValue {
Str(String),
List(Vec<FactValue>),
Bool(bool),
Num(f64),
Map(Vec<(String, FactValue)>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[allow(dead_code)]
pub enum FrameworkFact {
Moo,
Moose,
MojoBase,
CoreClass,
Plain,
}
impl FrameworkFact {
pub fn backing_rep(self) -> Option<Rep> {
match self {
FrameworkFact::Moo | FrameworkFact::Moose | FrameworkFact::MojoBase => Some(Rep::Hash),
FrameworkFact::CoreClass => None, FrameworkFact::Plain => None,
}
}
}
#[derive(Debug, Default, Serialize)]
pub struct WitnessBag {
witnesses: Vec<Witness>,
#[serde(skip)]
index: HashMap<WitnessAttachment, Vec<usize>>,
}
impl<'de> Deserialize<'de> for WitnessBag {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct WitnessBagOnDisk {
witnesses: Vec<Witness>,
}
let on_disk = WitnessBagOnDisk::deserialize(deserializer)?;
let mut bag = WitnessBag {
witnesses: on_disk.witnesses,
index: HashMap::new(),
};
bag.rebuild_index();
Ok(bag)
}
}
impl WitnessBag {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, w: Witness) -> usize {
let idx = self.witnesses.len();
self.index.entry(w.attachment.clone()).or_default().push(idx);
self.witnesses.push(w);
idx
}
#[allow(dead_code)]
pub fn all(&self) -> &[Witness] {
&self.witnesses
}
pub fn for_attachment(&self, att: &WitnessAttachment) -> Vec<&Witness> {
self.index
.get(att)
.map(|ixs| ixs.iter().map(|&i| &self.witnesses[i]).collect())
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn filter<P: Fn(&Witness) -> bool>(&self, pred: P) -> Vec<&Witness> {
self.witnesses.iter().filter(|w| pred(w)).collect()
}
pub fn rebuild_index(&mut self) {
self.index.clear();
for (i, w) in self.witnesses.iter().enumerate() {
self.index.entry(w.attachment.clone()).or_default().push(i);
}
}
pub fn truncate(&mut self, baseline: usize) {
if baseline >= self.witnesses.len() {
return;
}
self.witnesses.truncate(baseline);
self.rebuild_index();
}
pub fn remove_by_source_tag(&mut self, tag: &str) -> usize {
let before = self.witnesses.len();
self.witnesses.retain(|w| match &w.source {
WitnessSource::Builder(s) => s != tag,
_ => true,
});
let removed = before - self.witnesses.len();
if removed > 0 {
self.rebuild_index();
}
removed
}
pub fn len(&self) -> usize {
self.witnesses.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.witnesses.is_empty()
}
}
#[derive(Clone)]
pub struct ReducerQuery<'a> {
pub attachment: &'a WitnessAttachment,
pub point: Option<tree_sitter::Point>,
pub framework: FrameworkFact,
pub arity_hint: Option<u32>,
pub receiver: Option<InferredType>,
pub context: Option<&'a BagContext<'a>>,
}
pub struct BagContext<'a> {
pub scopes: &'a [Scope],
pub package_framework: &'a HashMap<String, FrameworkFact>,
pub module_index: Option<&'a crate::module_index::ModuleIndex>,
pub package_parents: &'a HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)] pub enum ReducedValue {
Type(InferredType),
FactMap(Vec<(String, FactValue)>),
None,
}
pub trait WitnessReducer: Send + Sync {
#[allow(dead_code)] fn name(&self) -> &str;
fn claims(&self, w: &Witness) -> bool;
fn reduce(&self, ws: &[&Witness], q: &ReducerQuery) -> ReducedValue;
}
pub struct FrameworkAwareTypeFold;
impl WitnessReducer for FrameworkAwareTypeFold {
fn name(&self) -> &str {
"framework_aware_type_fold"
}
fn claims(&self, w: &Witness) -> bool {
matches!(
w.attachment,
WitnessAttachment::Variable { .. } | WitnessAttachment::Expression(_)
) && matches!(
w.payload,
WitnessPayload::InferredType(_) | WitnessPayload::Observation(_)
) && !matches!(&w.source, WitnessSource::Builder(s) if s == "branch_arm")
}
fn reduce(&self, ws: &[&Witness], q: &ReducerQuery) -> ReducedValue {
if let Some(point) = q.point {
let mut narrow: Option<(&Witness, u64)> = None;
for w in ws {
if let WitnessPayload::InferredType(_) = w.payload {
if span_contains(&w.span, point) && !span_is_zero(&w.span) {
let area = span_area(&w.span);
if narrow.map(|(_, a)| area < a).unwrap_or(true) {
narrow = Some((*w, area));
}
}
}
}
if let Some((w, _)) = narrow {
if let WitnessPayload::InferredType(t) = &w.payload {
return ReducedValue::Type(t.clone());
}
}
}
let mut class_assertion: Option<String> = None;
let mut first_param_class: Option<String> = None;
let mut rep_obs: Option<Rep> = None;
let mut bless_rep: Option<Rep> = None;
let mut num = false;
let mut str_ = false;
let mut re = false;
let mut plain_type: Option<InferredType> = None;
for w in ws {
if let Some(point) = q.point {
if w.span.start > point {
continue;
}
}
if let (Some(point), WitnessPayload::InferredType(_)) = (q.point, &w.payload) {
if !span_is_zero(&w.span) && !span_contains(&w.span, point) {
continue;
}
}
match &w.payload {
WitnessPayload::InferredType(t) => match t {
InferredType::ClassName(name) => class_assertion = Some(name.clone()),
InferredType::FirstParam { package } => {
first_param_class = Some(package.clone())
}
other => plain_type = Some(other.clone()),
},
WitnessPayload::Observation(obs) => match obs {
TypeObservation::ClassAssertion(name) => class_assertion = Some(name.clone()),
TypeObservation::FirstParamInMethod { package } => {
first_param_class = Some(package.clone())
}
TypeObservation::HashRefAccess => rep_obs = merge_rep(rep_obs, Rep::Hash),
TypeObservation::ArrayRefAccess => rep_obs = merge_rep(rep_obs, Rep::Array),
TypeObservation::CodeRefInvocation => rep_obs = merge_rep(rep_obs, Rep::Code),
TypeObservation::BlessTarget(r) => bless_rep = Some(*r),
TypeObservation::NumericUse => num = true,
TypeObservation::StringUse => str_ = true,
TypeObservation::RegexpUse => re = true,
},
_ => {}
}
}
if let Some(name) = class_assertion.clone().or(first_param_class.clone()) {
let backing = bless_rep.or_else(|| q.framework.backing_rep());
match (rep_obs, backing) {
(None, _) => return ReducedValue::Type(InferredType::ClassName(name)),
(Some(obs), Some(b)) if obs == b => {
return ReducedValue::Type(InferredType::ClassName(name));
}
(Some(obs), None) => {
let _ = obs;
return ReducedValue::Type(InferredType::ClassName(name));
}
(Some(obs), Some(b)) => {
let _ = (obs, b);
return ReducedValue::Type(InferredType::ClassName(name));
}
}
}
if let Some(t) = plain_type {
return ReducedValue::Type(t);
}
if let Some(r) = rep_obs.or(bless_rep) {
return ReducedValue::Type(match r {
Rep::Hash => InferredType::HashRef,
Rep::Array => InferredType::ArrayRef,
Rep::Code => InferredType::CodeRef { return_edge: None },
Rep::Scalar => InferredType::String,
});
}
if re {
return ReducedValue::Type(InferredType::Regexp);
}
if num {
return ReducedValue::Type(InferredType::Numeric);
}
if str_ {
return ReducedValue::Type(InferredType::String);
}
ReducedValue::None
}
}
fn span_contains(span: &Span, point: tree_sitter::Point) -> bool {
span.start <= point && point <= span.end
}
fn span_is_zero(span: &Span) -> bool {
span.start == span.end
}
fn span_area(span: &Span) -> u64 {
let rows = span.end.row.saturating_sub(span.start.row) as u64;
if rows == 0 {
span.end.column.saturating_sub(span.start.column) as u64
} else {
rows * 10_000 + (span.end.column as u64)
}
}
fn merge_rep(existing: Option<Rep>, new: Rep) -> Option<Rep> {
match existing {
None => Some(new),
Some(r) if r == new => Some(r),
Some(_) => Some(new),
}
}
pub struct BranchArmFold;
impl WitnessReducer for BranchArmFold {
fn name(&self) -> &str {
"branch_arm_fold"
}
fn claims(&self, w: &Witness) -> bool {
matches!(
w.attachment,
WitnessAttachment::Variable { .. } | WitnessAttachment::Expr(_)
) && matches!(&w.source, WitnessSource::Builder(s) if s == "branch_arm")
&& matches!(w.payload, WitnessPayload::InferredType(_))
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
let arms: Vec<&InferredType> = ws
.iter()
.filter_map(|w| match &w.payload {
WitnessPayload::InferredType(t) => Some(t),
_ => None,
})
.collect();
if arms.len() < 2 {
return ReducedValue::None;
}
let first = arms[0];
if arms.iter().all(|t| *t == first) {
return ReducedValue::Type(first.clone());
}
ReducedValue::None
}
}
pub struct SymbolReturnArmFold;
impl WitnessReducer for SymbolReturnArmFold {
fn name(&self) -> &str {
"symbol_return_arm_fold"
}
fn claims(&self, w: &Witness) -> bool {
matches!(w.attachment, WitnessAttachment::SymbolReturnArm(_))
&& matches!(w.payload, WitnessPayload::InferredType(_))
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
let arms: Vec<InferredType> = ws
.iter()
.filter_map(|w| match &w.payload {
WitnessPayload::InferredType(t) => Some(t.clone()),
_ => None,
})
.collect();
match crate::file_analysis::resolve_return_type(&arms) {
Some(t) => ReducedValue::Type(t),
None => ReducedValue::None,
}
}
}
pub struct ExprReturn;
impl WitnessReducer for ExprReturn {
fn name(&self) -> &str {
"expr_return"
}
fn claims(&self, w: &Witness) -> bool {
matches!(w.attachment, WitnessAttachment::Expr(_))
&& matches!(w.payload, WitnessPayload::InferredType(_))
&& !matches!(&w.source, WitnessSource::Builder(s) if s == "branch_arm")
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
for w in ws.iter().rev() {
if let WitnessPayload::InferredType(t) = &w.payload {
return ReducedValue::Type(t.clone());
}
}
ReducedValue::None
}
}
pub struct SubReturnReducer;
impl WitnessReducer for SubReturnReducer {
fn name(&self) -> &str {
"sub_return"
}
fn claims(&self, w: &Witness) -> bool {
matches!(w.attachment, WitnessAttachment::Symbol(_))
&& matches!(w.payload, WitnessPayload::InferredType(_))
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
for w in ws.iter().rev() {
if let WitnessPayload::InferredType(t) = &w.payload {
return ReducedValue::Type(t.clone());
}
}
ReducedValue::None
}
}
pub struct MethodOnClassReducer;
impl WitnessReducer for MethodOnClassReducer {
fn name(&self) -> &str {
"method_on_class"
}
fn claims(&self, w: &Witness) -> bool {
matches!(w.attachment, WitnessAttachment::MethodOnClass { .. })
&& matches!(w.payload, WitnessPayload::InferredType(_))
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
for w in ws.iter().rev() {
if let WitnessPayload::InferredType(t) = &w.payload {
return ReducedValue::Type(t.clone());
}
}
ReducedValue::None
}
}
pub struct PluginOverrideReducer;
impl WitnessReducer for PluginOverrideReducer {
fn name(&self) -> &str {
"plugin_override"
}
fn claims(&self, w: &Witness) -> bool {
matches!(w.attachment, WitnessAttachment::Symbol(_))
&& matches!(w.payload, WitnessPayload::InferredType(_))
&& w.source.priority() > 10
}
fn reduce(&self, ws: &[&Witness], _q: &ReducerQuery) -> ReducedValue {
let mut best: Option<(&Witness, u8)> = None;
for w in ws {
let pr = w.source.priority();
match best {
None => best = Some((*w, pr)),
Some((_, prev)) if pr >= prev => best = Some((*w, pr)),
_ => {}
}
}
if let Some((w, _)) = best {
if let WitnessPayload::InferredType(t) = &w.payload {
return ReducedValue::Type(t.clone());
}
}
ReducedValue::None
}
}
pub struct ReturnExprReducer;
impl WitnessReducer for ReturnExprReducer {
fn name(&self) -> &str {
"return_expr"
}
fn claims(&self, w: &Witness) -> bool {
matches!(
w.attachment,
WitnessAttachment::Symbol(_) | WitnessAttachment::MethodOnClass { .. }
) && matches!(w.payload, WitnessPayload::ReturnExpr(_))
}
fn reduce(&self, ws: &[&Witness], q: &ReducerQuery) -> ReducedValue {
let mut best: Option<(&Witness, u8)> = None;
for w in ws.iter().rev() {
let pr = w.source.priority();
match best {
None => best = Some((*w, pr)),
Some((_, prev)) if pr > prev => best = Some((*w, pr)),
_ => {}
}
}
let Some((w, _)) = best else {
return ReducedValue::None;
};
let WitnessPayload::ReturnExpr(re) = &w.payload else {
return ReducedValue::None;
};
match eval_return_expr(re, q) {
Some(t) => ReducedValue::Type(t),
None => ReducedValue::None,
}
}
}
fn eval_return_expr(re: &ReturnExpr, q: &ReducerQuery) -> Option<InferredType> {
match re {
ReturnExpr::Concrete(t) => Some(t.clone()),
ReturnExpr::Receiver => q.receiver.clone(),
ReturnExpr::Operator(op) => match op {
ParametricOp::RowOf(inner) => {
let inner_t = eval_return_expr(inner, q)?;
Some(InferredType::Parametric(ParametricType::RowOf(Box::new(
inner_t,
))))
}
},
ReturnExpr::UnionOnArgs { branches } => {
if q.arity_hint.is_some() {
for (guard, sub) in branches {
if guard.matches(q.arity_hint) {
return eval_return_expr(sub, q);
}
}
return None;
}
for (guard, sub) in branches {
if matches!(guard, ArgGuard::Any) {
return eval_return_expr(sub, q);
}
}
for (guard, sub) in branches {
if matches!(guard, ArgGuard::Empty) {
return eval_return_expr(sub, q);
}
}
None
}
}
}
type VisitedKey = (usize, WitnessAttachment, u8, Option<u32>);
type VisitedSet = std::collections::HashSet<VisitedKey>;
fn receiver_discriminant(r: &Option<InferredType>) -> u8 {
let Some(t) = r else { return 0 };
match t {
InferredType::ClassName(_) => 1,
InferredType::FirstParam { .. } => 2,
InferredType::HashRef => 3,
InferredType::ArrayRef => 4,
InferredType::CodeRef { .. } => 5,
InferredType::Regexp => 6,
InferredType::Numeric => 7,
InferredType::String => 8,
InferredType::Parametric(_) => 9,
InferredType::Sequence(_) => 10,
}
}
const QUERY_REC_DEPTH_CAP: u32 = 512;
thread_local! {
static QUERY_REC_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static QUERY_REC_DEPTH_WARNED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[derive(Default)]
pub struct ReducerRegistry {
reducers: Vec<Box<dyn WitnessReducer>>,
}
impl ReducerRegistry {
pub fn new() -> Self {
Self { reducers: Vec::new() }
}
pub fn with_defaults() -> Self {
let mut r = Self::new();
r.register(Box::new(PluginOverrideReducer));
r.register(Box::new(ReturnExprReducer));
r.register(Box::new(SymbolReturnArmFold));
r.register(Box::new(BranchArmFold));
r.register(Box::new(FrameworkAwareTypeFold));
r.register(Box::new(ExprReturn));
r.register(Box::new(MethodOnClassReducer));
r.register(Box::new(SubReturnReducer));
r
}
pub fn register(&mut self, r: Box<dyn WitnessReducer>) {
self.reducers.push(r);
}
pub fn query(&self, bag: &WitnessBag, q: &ReducerQuery) -> ReducedValue {
let mut visited: VisitedSet = std::collections::HashSet::new();
self.query_rec(bag, q, &mut visited)
}
fn query_rec(
&self,
bag: &WitnessBag,
q: &ReducerQuery,
visited: &mut VisitedSet,
) -> ReducedValue {
let depth = QUERY_REC_DEPTH.with(|c| {
let d = c.get();
c.set(d + 1);
d
});
if depth >= QUERY_REC_DEPTH_CAP {
QUERY_REC_DEPTH_WARNED.with(|w| {
if !w.get() {
w.set(true);
eprintln!(
"perl-lsp: query_rec depth cap ({}) hit on attachment {:?} — \
returning None to avoid stack overflow. \
This indicates an un-broken recursion path; \
please report.",
QUERY_REC_DEPTH_CAP, q.attachment,
);
}
});
QUERY_REC_DEPTH.with(|c| c.set(c.get() - 1));
return ReducedValue::None;
}
let key: VisitedKey = (
bag as *const _ as usize,
q.attachment.clone(),
receiver_discriminant(&q.receiver),
q.arity_hint,
);
if !visited.insert(key.clone()) {
QUERY_REC_DEPTH.with(|c| c.set(c.get() - 1));
return ReducedValue::None;
}
let result = self.query_rec_body(bag, q, visited);
visited.remove(&key);
QUERY_REC_DEPTH.with(|c| c.set(c.get() - 1));
result
}
fn query_rec_body(
&self,
bag: &WitnessBag,
q: &ReducerQuery,
visited: &mut VisitedSet,
) -> ReducedValue {
let materialized = self.materialize(bag, q, visited);
for r in &self.reducers {
let claimed: Vec<&Witness> =
materialized.iter().filter(|w| r.claims(w)).collect();
if claimed.is_empty() {
continue;
}
let v = r.reduce(&claimed, q);
if v != ReducedValue::None {
return v;
}
}
if let WitnessAttachment::MethodOnClass { class, name } = q.attachment {
if let Some(ctx) = q.context {
if let Some(idx) = ctx.module_index {
if let Some(cached) = idx.get_cached(class) {
if !std::ptr::eq(bag, &cached.analysis.witnesses) {
let cached_ctx = BagContext {
scopes: &cached.analysis.scopes,
package_framework: &cached.analysis.package_framework,
module_index: Some(idx),
package_parents: &cached.analysis.package_parents,
};
let sub_q = ReducerQuery {
attachment: q.attachment,
point: q.point,
framework: q.framework,
arity_hint: q.arity_hint,
receiver: q.receiver.clone(),
context: Some(&cached_ctx),
};
let v = self.query_rec(
&cached.analysis.witnesses,
&sub_q,
visited,
);
if v != ReducedValue::None {
return v;
}
}
}
}
let mut parents: Vec<String> =
ctx.package_parents.get(class).cloned().unwrap_or_default();
if let Some(idx) = ctx.module_index {
for p in idx.parents_cached(class) {
if !parents.contains(&p) {
parents.push(p);
}
}
}
for p in parents {
let parent_att = WitnessAttachment::MethodOnClass {
class: p,
name: name.clone(),
};
let sub_q = ReducerQuery {
attachment: &parent_att,
point: q.point,
framework: q.framework,
arity_hint: q.arity_hint,
receiver: q.receiver.clone(),
context: q.context,
};
let v = self.query_rec(bag, &sub_q, visited);
if v != ReducedValue::None {
return v;
}
}
if let Some(idx) = ctx.module_index {
let mut found: Option<InferredType> = None;
idx.for_each_entity_bridged_to(class, |cached, sym| {
if found.is_some() {
return;
}
if !matches!(
sym.kind,
crate::file_analysis::SymKind::Sub
| crate::file_analysis::SymKind::Method
) {
return;
}
if &sym.name != name {
return;
}
if let Some(t) = cached
.analysis
.symbol_return_type_via_bag(sym.id, None)
{
found = Some(t);
}
});
if let Some(t) = found {
return ReducedValue::Type(t);
}
}
}
}
ReducedValue::None
}
fn materialize(
&self,
bag: &WitnessBag,
q: &ReducerQuery,
visited: &mut VisitedSet,
) -> Vec<Witness> {
let raw = bag.for_attachment(q.attachment);
let mut out: Vec<Witness> = Vec::with_capacity(raw.len());
for w in raw {
match &w.payload {
WitnessPayload::Edge(target) => {
let resolved = match (target, q.context) {
(
WitnessAttachment::Variable { name, scope },
Some(ctx),
) => {
let point = scope_point(ctx.scopes, *scope);
self.query_variable_with_visited(
bag,
ctx.scopes,
ctx.package_framework,
name,
*scope,
point,
visited,
)
}
_ => {
let sub_q = ReducerQuery {
attachment: target,
point: q.point,
framework: q.framework,
arity_hint: q.arity_hint,
receiver: q.receiver.clone(),
context: q.context,
};
if let ReducedValue::Type(t) = self.query_rec(bag, &sub_q, visited) {
Some(t)
} else {
None
}
}
};
if let Some(t) = resolved {
out.push(Witness {
attachment: w.attachment.clone(),
source: w.source.clone(),
payload: WitnessPayload::InferredType(t),
span: w.span,
});
}
}
_ => out.push(w.clone()),
}
}
out
}
fn query_variable_with_visited(
&self,
bag: &WitnessBag,
scopes: &[Scope],
package_framework: &HashMap<String, FrameworkFact>,
var: &str,
scope: ScopeId,
point: Point,
visited: &mut VisitedSet,
) -> Option<InferredType> {
let mut chain: Vec<ScopeId> = Vec::new();
let mut cur = Some(scope);
while let Some(sid) = cur {
chain.push(sid);
cur = scopes[sid.0 as usize].parent;
}
let framework = chain
.iter()
.find_map(|sid| scopes[sid.0 as usize].package.as_ref())
.and_then(|pkg| package_framework.get(pkg).copied())
.unwrap_or(FrameworkFact::Plain);
let empty_parents: HashMap<String, Vec<String>> = HashMap::new();
let ctx = BagContext {
scopes,
package_framework,
module_index: None,
package_parents: &empty_parents,
};
for sid in chain {
let att = WitnessAttachment::Variable {
name: var.to_string(),
scope: sid,
};
let q = ReducerQuery {
attachment: &att,
point: Some(point),
framework,
arity_hint: None,
receiver: None,
context: Some(&ctx),
};
if let ReducedValue::Type(t) = self.query_rec(bag, &q, visited) {
return Some(t);
}
}
None
}
}
fn scope_point(scopes: &[Scope], scope: ScopeId) -> tree_sitter::Point {
scopes
.get(scope.0 as usize)
.map(|s| s.span.end)
.unwrap_or(tree_sitter::Point { row: 0, column: 0 })
}
pub fn query_sub_return_type(
bag: &WitnessBag,
symbols: &[crate::file_analysis::Symbol],
sub_name: &str,
arity_hint: Option<u32>,
receiver: Option<InferredType>,
context: Option<&BagContext>,
) -> Option<InferredType> {
let reg = ReducerRegistry::with_defaults();
let local_sym = symbols.iter().find(|s| {
s.name == sub_name
&& matches!(
s.kind,
crate::file_analysis::SymKind::Sub | crate::file_analysis::SymKind::Method
)
});
if let Some(sym) = local_sym {
let att = WitnessAttachment::Symbol(sym.id);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint,
receiver: receiver.clone(),
context,
};
if let ReducedValue::Type(t) = reg.query(bag, &q) {
return Some(t);
}
if let Some(class) = sym.package.as_ref() {
let att = WitnessAttachment::MethodOnClass {
class: class.clone(),
name: sub_name.to_string(),
};
let effective_receiver = receiver
.clone()
.or_else(|| Some(InferredType::ClassName(class.clone())));
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint,
receiver: effective_receiver,
context,
};
if let ReducedValue::Type(t) = reg.query(bag, &q) {
return Some(t);
}
}
}
if let Some(ctx) = context {
if let Some(idx) = ctx.module_index {
for module_name in idx.find_exporters(sub_name) {
let Some(cached) = idx.get_cached(&module_name) else { continue };
let Some(sym) = cached.analysis.symbols.iter().find(|s| {
s.name == sub_name
&& matches!(
s.kind,
crate::file_analysis::SymKind::Sub
| crate::file_analysis::SymKind::Method
)
}) else {
continue;
};
let cached_ctx = BagContext {
scopes: &cached.analysis.scopes,
package_framework: &cached.analysis.package_framework,
module_index: Some(idx),
package_parents: &cached.analysis.package_parents,
};
let att = WitnessAttachment::Symbol(sym.id);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint,
receiver: receiver.clone(),
context: Some(&cached_ctx),
};
if let ReducedValue::Type(t) = reg.query(&cached.analysis.witnesses, &q) {
return Some(t);
}
}
}
}
None
}
pub fn query_variable_type(
bag: &WitnessBag,
scopes: &[Scope],
package_framework: &HashMap<String, FrameworkFact>,
var: &str,
scope: ScopeId,
point: Point,
) -> Option<InferredType> {
let reg = ReducerRegistry::with_defaults();
let mut visited: VisitedSet = std::collections::HashSet::new();
reg.query_variable_with_visited(
bag,
scopes,
package_framework,
var,
scope,
point,
&mut visited,
)
}
#[cfg(test)]
#[path = "witnesses_tests.rs"]
mod tests;