pub(crate) mod docblock;
mod extraction;
use crate::php_type::PhpType;
pub(crate) use extraction::extract_symbol_map;
#[derive(Debug, Clone)]
pub(crate) struct SymbolSpan {
pub start: u32,
pub end: u32,
pub kind: SymbolKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SelfStaticParentKind {
Self_,
Static,
Parent,
This,
}
#[derive(Debug, Clone)]
pub(crate) enum SymbolKind {
ClassReference {
name: String,
is_fqn: bool,
},
ClassDeclaration { name: String },
MemberAccess {
subject_text: String,
member_name: String,
is_static: bool,
is_method_call: bool,
is_docblock_reference: bool,
},
Variable {
name: String,
},
FunctionCall { name: String, is_definition: bool },
SelfStaticParent(SelfStaticParentKind),
ConstantReference { name: String },
MemberDeclaration {
name: String,
is_static: bool,
},
}
#[derive(Debug, Clone)]
pub(crate) struct TemplateParamDef {
pub name_offset: u32,
pub name: String,
pub bound: Option<PhpType>,
pub variance: crate::types::TemplateVariance,
pub scope_start: u32,
pub scope_end: u32,
}
#[derive(Debug, Clone)]
pub(crate) struct CallSite {
pub args_start: u32,
pub args_end: u32,
pub call_expression: String,
pub comma_offsets: Vec<u32>,
pub arg_offsets: Vec<u32>,
pub arg_count: u32,
pub has_unpacking: bool,
pub named_arg_indices: Vec<u32>,
pub named_arg_names: Vec<String>,
pub spread_arg_indices: Vec<u32>,
}
#[derive(Debug, Clone)]
pub(crate) struct VarDefSite {
pub offset: u32,
pub name: String,
pub kind: VarDefKind,
pub scope_start: u32,
pub effective_from: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum VarDefKind {
Assignment,
Parameter,
Property,
Foreach,
Catch,
StaticDecl,
GlobalDecl,
ArrayDestructuring,
ListDestructuring,
ClosureCapture,
DocblockParam,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct SymbolMap {
pub spans: Vec<SymbolSpan>,
pub var_defs: Vec<VarDefSite>,
pub scopes: Vec<(u32, u32)>,
pub body_scopes: Vec<(u32, u32)>,
pub narrowing_blocks: Vec<(u32, u32)>,
pub assert_narrowing_offsets: Vec<u32>,
pub template_defs: Vec<TemplateParamDef>,
pub call_sites: Vec<CallSite>,
pub breakable_scopes: Vec<(u32, u32)>,
pub loop_scopes: Vec<(u32, u32)>,
pub switch_scopes: Vec<(u32, u32)>,
}
impl SymbolMap {
pub fn lookup(&self, offset: u32) -> Option<&SymbolSpan> {
let idx = self.spans.partition_point(|s| s.start <= offset);
if idx == 0 {
return None;
}
let candidate = &self.spans[idx - 1];
if offset < candidate.end {
Some(candidate)
} else {
None
}
}
pub fn find_enclosing_scope(&self, offset: u32) -> u32 {
let mut best: u32 = 0;
for &(start, end) in &self.scopes {
if start <= offset && offset <= end && start > best {
best = start;
}
}
best
}
pub fn find_variable_scope(&self, var_name: &str, offset: u32) -> u32 {
if let Some(def) = self.var_defs.iter().find(|d| {
d.name == var_name
&& (d.kind == VarDefKind::Parameter || d.kind == VarDefKind::DocblockParam)
&& offset >= d.offset
&& offset < d.offset + 1 + d.name.len() as u32
}) {
return def.scope_start;
}
self.find_enclosing_scope(offset)
}
pub fn find_narrowing_block(&self, offset: u32) -> u32 {
let mut best: u32 = 0;
for &(start, end) in &self.narrowing_blocks {
if start <= offset && offset <= end && start > best {
best = start;
}
}
best
}
pub fn find_preceding_assert_offset(&self, offset: u32) -> u32 {
match self
.assert_narrowing_offsets
.partition_point(|&o| o < offset)
{
0 => 0,
i => self.assert_narrowing_offsets[i - 1],
}
}
pub fn find_template_def(&self, name: &str, cursor_offset: u32) -> Option<&TemplateParamDef> {
self.template_defs.iter().rev().find(|d| {
d.name == name && cursor_offset >= d.scope_start && cursor_offset <= d.scope_end
})
}
pub fn find_var_definition(
&self,
var_name: &str,
cursor_offset: u32,
scope_start: u32,
) -> Option<&VarDefSite> {
self.var_defs.iter().rev().find(|d| {
d.name == var_name && d.scope_start == scope_start && d.effective_from <= cursor_offset
})
}
pub fn active_var_def_offset(&self, var_name: &str, cursor_offset: u32) -> u32 {
let scope_start = self.find_enclosing_scope(cursor_offset);
self.find_var_definition(var_name, cursor_offset, scope_start)
.map(|d| d.effective_from)
.unwrap_or(0)
}
pub fn is_at_var_definition(&self, var_name: &str, cursor_offset: u32) -> bool {
self.var_def_kind_at(var_name, cursor_offset).is_some()
}
pub fn var_def_kind_at(&self, var_name: &str, cursor_offset: u32) -> Option<&VarDefKind> {
self.var_def_at(var_name, cursor_offset).map(|d| &d.kind)
}
pub fn var_def_at(&self, var_name: &str, cursor_offset: u32) -> Option<&VarDefSite> {
self.var_defs.iter().find(|d| {
d.name == var_name
&& cursor_offset >= d.offset
&& cursor_offset < d.offset + 1 + d.name.len() as u32
})
}
pub fn find_enclosing_call_site(&self, offset: u32) -> Option<&CallSite> {
self.call_sites
.iter()
.rev()
.find(|cs| offset >= cs.args_start && offset <= cs.args_end)
}
pub fn is_inside_nested_scope_of_call(&self, offset: u32, call: &CallSite) -> bool {
self.body_scopes.iter().any(|&(body_start, body_end)| {
body_start > call.args_start
&& body_start < call.args_end
&& offset >= body_start
&& offset <= body_end
})
}
pub fn is_inside_function_like_scope(&self, offset: u32) -> bool {
self.find_enclosing_scope(offset) != 0
}
fn offset_in_sorted_ranges(ranges: &[(u32, u32)], offset: u32) -> bool {
let idx = ranges.partition_point(|&(start, _)| start <= offset);
ranges[..idx].iter().rev().any(|&(_, end)| offset <= end)
}
pub fn is_inside_breakable_scope(&self, offset: u32) -> bool {
Self::offset_in_sorted_ranges(&self.breakable_scopes, offset)
}
pub fn is_inside_loop_scope(&self, offset: u32) -> bool {
Self::offset_in_sorted_ranges(&self.loop_scopes, offset)
}
pub fn is_inside_switch_scope(&self, offset: u32) -> bool {
Self::offset_in_sorted_ranges(&self.switch_scopes, offset)
}
}
#[cfg(test)]
mod tests;