use std::collections::{HashMap, HashSet};
use std::ops::ControlFlow;
use std::sync::Arc;
use php_ast::visitor::{Visitor, walk_stmt};
use php_ast::{
ClassMemberKind, EnumMemberKind, ExprKind, NamespaceBody, Span, Stmt, StmtKind, UseKind,
};
use rayon::prelude::*;
use tower_lsp::lsp_types::{Location, Position, Range, Url};
use crate::ast::{ParsedDoc, str_offset_in_range};
use crate::util::{fqn_short_name, utf16_code_units};
use crate::walk::{
all_class_ref_names_in_stmts, class_refs_in_stmts, constant_refs_in_stmts,
fqn_new_class_refs_in_stmts, function_refs_in_stmts, global_constant_refs_in_stmts,
method_refs_in_stmts, new_refs_in_stmts, property_refs_in_stmts, refs_in_stmts,
refs_in_stmts_with_use,
};
pub type RefLookup<'a> = dyn Fn(&str) -> Vec<(Arc<str>, u32, u16, u16)> + 'a;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
Function,
Method,
Class,
Property,
Constant,
}
fn class_has_ancestor(
codebase: &mir_analyzer::db::MirDbStorage,
class_fqcn: &str,
target_fqcn: &str,
) -> bool {
mir_analyzer::db::extends_or_implements(codebase, class_fqcn, target_fqcn)
}
pub fn find_references(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
) -> Vec<Location> {
find_references_inner(word, all_docs, include_declaration, false, kind, None)
}
pub fn find_references_with_target(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
target_fqn: &str,
) -> Vec<Location> {
let include_use = kind.is_none();
find_references_inner(
word,
all_docs,
include_declaration,
include_use,
kind,
Some(target_fqn),
)
}
pub fn find_references_with_use(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
) -> Vec<Location> {
find_references_inner(word, all_docs, include_declaration, true, None, None)
}
pub fn find_constructor_references(
short_name: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
class_fqn: Option<&str>,
) -> Vec<Location> {
all_docs
.par_iter()
.flat_map_iter(|(uri, doc)| {
if !doc.view().source().contains(short_name)
&& !class_fqn
.is_some_and(|f| doc.view().source().contains(f.trim_start_matches('\\')))
{
return Vec::new();
}
if let Some(fqn) = class_fqn
&& !doc_can_reference_target(doc, short_name, fqn)
&& !doc.view().source().contains(fqn.trim_start_matches('\\'))
{
return Vec::new();
}
let mut spans = Vec::new();
new_refs_in_stmts(&doc.program().stmts, short_name, class_fqn, &mut spans);
let sv = doc.view();
spans
.into_iter()
.map(|span| {
let start = sv.position_of(span.start);
let end = sv.position_of(span.end);
Location {
uri: uri.clone(),
range: Range { start, end },
}
})
.collect::<Vec<_>>()
})
.collect()
}
pub(crate) fn session_tuple_to_location(
(file, line, col_start, col_end): (Arc<str>, u32, u32, u32),
) -> Option<Location> {
let uri = Url::parse(&file).ok()?;
Some(Location {
uri,
range: Range {
start: Position {
line,
character: col_start,
},
end: Position {
line,
character: col_end,
},
},
})
}
pub(crate) fn ref_location_key(loc: &Location) -> (String, u32, u32, u32) {
(
loc.uri.to_string(),
loc.range.start.line,
loc.range.start.character,
loc.range.end.character,
)
}
pub(crate) fn dedup_ref_locations(locations: &mut Vec<Location>) {
let mut seen = HashSet::new();
locations.retain(|loc| seen.insert(ref_location_key(loc)));
}
pub fn find_references_codebase(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
kind: Option<SymbolKind>,
codebase: &mir_analyzer::db::MirDbStorage,
lookup_refs: &RefLookup<'_>,
) -> Option<Vec<Location>> {
find_references_codebase_with_target(
word,
all_docs,
include_declaration,
kind,
None,
codebase,
lookup_refs,
)
}
pub fn find_references_codebase_with_target(
_word: &str,
_all_docs: &[(Url, Arc<ParsedDoc>)],
_include_declaration: bool,
kind: Option<SymbolKind>,
_target_fqn: Option<&str>,
_codebase: &mir_analyzer::db::MirDbStorage,
_lookup_refs: &RefLookup<'_>,
) -> Option<Vec<Location>> {
match kind {
Some(SymbolKind::Function) => {
None
}
Some(SymbolKind::Class) => None,
Some(SymbolKind::Method) => {
None
}
None => None,
Some(SymbolKind::Property) | Some(SymbolKind::Constant) => None,
}
}
fn find_references_inner(
word: &str,
all_docs: &[(Url, Arc<ParsedDoc>)],
include_declaration: bool,
include_use: bool,
kind: Option<SymbolKind>,
target_fqn: Option<&str>,
) -> Vec<Location> {
let namespace_filter_active =
matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
all_docs
.par_iter()
.flat_map_iter(|(uri, doc)| {
if !doc.view().source().contains(word) {
return Vec::new();
}
if namespace_filter_active
&& let Some(target) = target_fqn
&& !doc_can_reference_target(doc, word, target)
{
return Vec::new();
}
scan_doc(
word,
uri,
doc,
include_declaration,
include_use,
kind,
target_fqn,
)
})
.collect()
}
fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
let target = target_fqn.trim_start_matches('\\');
let imports = collect_file_imports(doc);
let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
resolved == target
|| (resolved == word && !target.contains('\\'))
|| (resolved == word && target == format!("\\{word}"))
}
struct ImportsVisitor {
only_kind: Option<UseKind>,
out: HashMap<String, String>,
}
impl<'arena, 'src> Visitor<'arena, 'src> for ImportsVisitor {
fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
match &stmt.kind {
StmtKind::Use(u) if self.only_kind.is_none_or(|k| u.kind == k) => {
for item in u.uses.iter() {
let fqn = item.name.to_string_repr().into_owned();
let short = item
.alias
.map(|a| a.to_string())
.unwrap_or_else(|| fqn_short_name(&fqn).to_string());
self.out.insert(short, fqn);
}
ControlFlow::Continue(())
}
StmtKind::Namespace(_) => walk_stmt(self, stmt),
_ => ControlFlow::Continue(()),
}
}
}
pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> HashMap<String, String> {
collect_imports_filtered(doc, None)
}
pub(crate) fn collect_class_imports(doc: &ParsedDoc) -> HashMap<String, String> {
collect_imports_filtered(doc, Some(UseKind::Normal))
}
fn collect_imports_filtered(
doc: &ParsedDoc,
only_kind: Option<UseKind>,
) -> HashMap<String, String> {
let mut v = ImportsVisitor {
only_kind,
out: HashMap::new(),
};
for stmt in doc.program().stmts.iter() {
let _ = v.visit_stmt(stmt);
}
v.out
}
pub(crate) fn collect_fqn_new_class_refs(doc: &ParsedDoc) -> Vec<String> {
fqn_new_class_refs_in_stmts(&doc.program().stmts)
}
pub(crate) fn collect_referenced_class_fqns(doc: &ParsedDoc) -> Vec<String> {
let imports = collect_class_imports(doc);
let names = all_class_ref_names_in_stmts(&doc.program().stmts);
let locals = collect_local_type_decl_fqns(doc);
let mut out: Vec<String> = names
.into_iter()
.map(|name| {
if let Some(stripped) = name.strip_prefix('\\') {
return stripped.to_string();
}
let fqn = crate::moniker::resolve_fqn(doc, &name, &imports);
fqn.trim_start_matches('\\').to_string()
})
.filter(|fqn| !locals.contains(fqn))
.collect();
out.sort_unstable();
out.dedup();
out
}
fn collect_local_type_decl_fqns(doc: &ParsedDoc) -> HashSet<String> {
use php_ast::NamespaceBody;
let mut out = HashSet::new();
fn name_of(kind: &StmtKind<'_, '_>) -> Option<String> {
match kind {
StmtKind::Class(c) => c.name.as_ref().map(|n| n.to_string()),
StmtKind::Interface(i) => Some(i.name.to_string()),
StmtKind::Trait(t) => Some(t.name.to_string()),
StmtKind::Enum(e) => Some(e.name.to_string()),
_ => None,
}
}
let mut current_ns: Option<String> = None;
for stmt in doc.program().stmts.iter() {
match &stmt.kind {
StmtKind::Namespace(ns) => {
let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().to_string());
match &ns.body {
NamespaceBody::Braced(inner) => {
let prefix = ns_name
.as_deref()
.map(|n| format!("{n}\\"))
.unwrap_or_default();
for s in inner.stmts.iter() {
if let Some(n) = name_of(&s.kind) {
out.insert(format!("{prefix}{n}"));
}
}
}
NamespaceBody::Simple => {
current_ns = ns_name;
}
}
}
k => {
if let Some(n) = name_of(k) {
let fqn = match ¤t_ns {
Some(ns) => format!("{ns}\\{n}"),
None => n,
};
out.insert(fqn);
}
}
}
}
out
}
fn scan_doc(
word: &str,
uri: &Url,
doc: &Arc<ParsedDoc>,
include_declaration: bool,
include_use: bool,
kind: Option<SymbolKind>,
target_fqn: Option<&str>,
) -> Vec<Location> {
let source = doc.source();
if !source.contains(word) {
return Vec::new();
}
let stmts = &doc.program().stmts;
let mut spans = Vec::new();
if include_use {
refs_in_stmts_with_use(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
} else {
match kind {
Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
Some(SymbolKind::Property) => {
property_refs_in_stmts(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(
source,
stmts,
word,
Some(SymbolKind::Property),
&mut decl_spans,
);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
}
Some(SymbolKind::Constant) => {
let is_global = target_fqn.is_none_or(|fqn| fqn.contains('\\'));
if is_global {
global_constant_refs_in_stmts(source, stmts, word, target_fqn, &mut spans);
} else {
constant_refs_in_stmts(source, stmts, word, target_fqn, &mut spans);
}
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(
source,
stmts,
word,
Some(SymbolKind::Constant),
&mut decl_spans,
);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
}
None => {
refs_in_stmts(source, stmts, word, &mut spans);
if !include_declaration {
let mut decl_spans = Vec::new();
collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
let decl_set: HashSet<(u32, u32)> =
decl_spans.iter().map(|s| (s.start, s.end)).collect();
spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
}
}
}
if include_declaration
&& matches!(
kind,
Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
)
{
collect_declaration_spans(source, stmts, word, kind, &mut spans);
}
}
let sv = doc.view();
let word_utf16_len: u32 = utf16_code_units(word);
spans
.into_iter()
.map(|span| {
let start = sv.position_of(span.start);
let end = Position {
line: start.line,
character: start.character + word_utf16_len,
};
Location {
uri: uri.clone(),
range: Range { start, end },
}
})
.collect()
}
fn declaration_name_span(source: &str, name: &str, stmt_span: Span) -> Span {
let start = str_offset_in_range(source, stmt_span, name).unwrap_or(stmt_span.start);
Span {
start,
end: start + name.len() as u32,
}
}
fn collect_declaration_spans(
source: &str,
stmts: &[Stmt<'_, '_>],
word: &str,
kind: Option<SymbolKind>,
out: &mut Vec<Span>,
) {
let want_free = matches!(kind, None | Some(SymbolKind::Function));
let want_method = matches!(kind, None | Some(SymbolKind::Method));
let want_type = matches!(kind, None | Some(SymbolKind::Class));
let want_property = matches!(kind, None | Some(SymbolKind::Property));
let want_constant = matches!(kind, None | Some(SymbolKind::Constant));
for stmt in stmts {
match &stmt.kind {
StmtKind::Function(f) if want_free && f.name == word => {
out.push(declaration_name_span(
source,
&f.name.to_string(),
stmt.span,
));
}
StmtKind::Class(c) => {
if want_type
&& let Some(name) = c.name
&& name == word
{
out.push(declaration_name_span(source, &name.to_string(), stmt.span));
}
if want_method || want_property || want_constant {
for member in c.body.members.iter() {
match &member.kind {
ClassMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
ClassMemberKind::Method(m)
if want_property && m.name == "__construct" =>
{
for p in m.params.iter() {
if p.visibility.is_some() && p.name == word {
out.push(declaration_name_span(
source,
&p.name.to_string(),
p.span,
));
}
}
}
ClassMemberKind::Property(p) if want_property && p.name == word => {
out.push(declaration_name_span(
source,
&p.name.to_string(),
member.span,
));
}
ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
member.span,
));
}
_ => {}
}
}
}
}
StmtKind::Interface(i) => {
if want_type && i.name == word {
out.push(declaration_name_span(
source,
&i.name.to_string(),
stmt.span,
));
}
if want_method || want_constant {
for member in i.body.members.iter() {
match &member.kind {
ClassMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
member.span,
));
}
_ => {}
}
}
}
}
StmtKind::Trait(t) => {
if want_type && t.name == word {
out.push(declaration_name_span(
source,
&t.name.to_string(),
stmt.span,
));
}
if want_method || want_property || want_constant {
for member in t.body.members.iter() {
match &member.kind {
ClassMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
ClassMemberKind::Property(p) if want_property && p.name == word => {
out.push(declaration_name_span(
source,
&p.name.to_string(),
member.span,
));
}
ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
member.span,
));
}
_ => {}
}
}
}
}
StmtKind::Enum(e) => {
if want_type && e.name == word {
out.push(declaration_name_span(
source,
&e.name.to_string(),
stmt.span,
));
}
for member in e.body.members.iter() {
match &member.kind {
EnumMemberKind::Method(m) if want_method && m.name == word => {
out.push(declaration_name_span(
source,
&m.name.to_string(),
member.span,
));
}
EnumMemberKind::Case(c) if want_type && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
member.span,
));
}
EnumMemberKind::ClassConst(c) if want_constant && c.name == word => {
out.push(declaration_name_span(
source,
&c.name.to_string(),
member.span,
));
}
_ => {}
}
}
}
StmtKind::Const(items) if want_constant => {
for item in items.iter() {
if item.name == word {
let name = item.name.to_string();
out.push(declaration_name_span(source, &name, item.span));
}
}
}
StmtKind::Expression(expr) if want_constant => {
if let ExprKind::FunctionCall(f) = &expr.kind
&& let ExprKind::Identifier(id) = &f.name.kind
&& id.as_str() == "define"
&& let Some(first_arg) = f.args.first()
&& let ExprKind::String(s) = &first_arg.value.kind
&& *s == word
{
let start = first_arg.value.span.start + 1;
out.push(Span {
start,
end: start + s.len() as u32,
});
}
}
StmtKind::Namespace(ns) => {
if let NamespaceBody::Braced(inner) = &ns.body {
collect_declaration_spans(source, &inner.stmts, word, kind, out);
}
}
_ => {}
}
}
}