php_lsp/navigation/
references.rs1use std::collections::{HashMap, HashSet};
2use std::ops::ControlFlow;
3use std::sync::Arc;
4
5use php_ast::visitor::{Visitor, walk_stmt};
6use php_ast::{Stmt, StmtKind, UseKind};
7use tower_lsp_server::ls_types::{Location, Position, Range, Uri};
8
9use super::walk::all_class_ref_names_in_stmts;
10use crate::document::ast::ParsedDoc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SymbolKind {
17 Function,
19 Method,
21 Class,
23 Property,
25 Constant,
27}
28
29pub(crate) fn session_tuple_to_location(
33 (file, line, col_start, col_end): (Arc<str>, u32, u32, u32),
34) -> Option<Location> {
35 let uri = file.parse::<Uri>().ok()?;
36 Some(Location {
37 uri,
38 range: Range {
39 start: Position {
40 line,
41 character: col_start,
42 },
43 end: Position {
44 line,
45 character: col_end,
46 },
47 },
48 })
49}
50
51pub(crate) fn ref_location_key(loc: &Location) -> (String, u32, u32, u32) {
55 (
56 loc.uri.to_string(),
57 loc.range.start.line,
58 loc.range.start.character,
59 loc.range.end.character,
60 )
61}
62
63pub(crate) fn dedup_ref_locations(locations: &mut Vec<Location>) {
66 let mut seen = HashSet::new();
67 locations.retain(|loc| seen.insert(ref_location_key(loc)));
68}
69
70struct ImportsVisitor {
71 only_kind: Option<UseKind>,
72 out: HashMap<String, String>,
73}
74
75impl<'arena, 'src> Visitor<'arena, 'src> for ImportsVisitor {
76 fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
77 match &stmt.kind {
78 StmtKind::Use(u) if self.only_kind.is_none_or(|k| u.kind == k) => {
79 for item in u.uses.iter() {
80 let (short, fqn) = crate::document::ast::use_item_alias_and_fqn(item);
81 self.out.insert(short, fqn);
82 }
83 ControlFlow::Continue(())
84 }
85 StmtKind::Namespace(_) => walk_stmt(self, stmt),
87 _ => ControlFlow::Continue(()),
88 }
89 }
90}
91
92pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> HashMap<String, String> {
97 collect_imports_filtered(doc, None)
98}
99
100pub(crate) fn collect_class_imports(doc: &ParsedDoc) -> HashMap<String, String> {
109 collect_imports_filtered(doc, Some(UseKind::Normal))
110}
111
112fn collect_imports_filtered(
113 doc: &ParsedDoc,
114 only_kind: Option<UseKind>,
115) -> HashMap<String, String> {
116 let mut v = ImportsVisitor {
117 only_kind,
118 out: HashMap::new(),
119 };
120 for stmt in doc.program().stmts.iter() {
121 let _ = v.visit_stmt(stmt);
122 }
123 v.out
124}
125
126pub(crate) fn collect_referenced_class_fqns(doc: &ParsedDoc) -> Vec<String> {
134 let imports = collect_class_imports(doc);
135 let names = all_class_ref_names_in_stmts(&doc.program().stmts);
136 let locals = collect_local_type_decl_fqns(doc);
137 let mut out: Vec<String> = names
138 .into_iter()
139 .map(|name| {
140 if let Some(stripped) = name.strip_prefix('\\') {
144 return stripped.to_string();
145 }
146 let fqn = crate::navigation::moniker::resolve_fqn(doc, &name, &imports);
147 fqn.trim_start_matches('\\').to_string()
148 })
149 .filter(|fqn| !locals.contains(fqn))
153 .collect();
154 out.sort_unstable();
155 out.dedup();
156 out
157}
158
159fn collect_local_type_decl_fqns(doc: &ParsedDoc) -> HashSet<String> {
160 use php_ast::NamespaceBody;
161 let mut out = HashSet::new();
162 fn name_of(kind: &StmtKind<'_, '_>) -> Option<String> {
163 match kind {
164 StmtKind::Class(c) => c.name.as_ref().map(|n| n.to_string()),
165 StmtKind::Interface(i) => Some(i.name.to_string()),
166 StmtKind::Trait(t) => Some(t.name.to_string()),
167 StmtKind::Enum(e) => Some(e.name.to_string()),
168 _ => None,
169 }
170 }
171 let mut current_ns: Option<String> = None;
172 for stmt in doc.program().stmts.iter() {
173 match &stmt.kind {
174 StmtKind::Namespace(ns) => {
175 let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().to_string());
176 match &ns.body {
177 NamespaceBody::Braced(inner) => {
178 let prefix = ns_name
179 .as_deref()
180 .map(|n| format!("{n}\\"))
181 .unwrap_or_default();
182 for s in inner.stmts.iter() {
183 if let Some(n) = name_of(&s.kind) {
184 out.insert(format!("{prefix}{n}"));
185 }
186 }
187 }
188 NamespaceBody::Simple => {
189 current_ns = ns_name;
190 }
191 }
192 }
193 k => {
194 if let Some(n) = name_of(k) {
195 let fqn = match ¤t_ns {
196 Some(ns) => format!("{ns}\\{n}"),
197 None => n,
198 };
199 out.insert(fqn);
200 }
201 }
202 }
203 }
204 out
205}
206
207pub fn build_mir_symbol(
218 word: &str,
219 kind: Option<SymbolKind>,
220 target_fqn: Option<&str>,
221 class_constant: bool,
222) -> Option<mir_analyzer::Name> {
223 let norm = |s: &str| -> Arc<str> { Arc::from(s.trim_start_matches('\\')) };
224 match kind {
225 Some(SymbolKind::Function) => Some(mir_analyzer::Name::Function(norm(
226 target_fqn.unwrap_or(word),
227 ))),
228 Some(SymbolKind::Class) => {
229 Some(mir_analyzer::Name::Class(norm(target_fqn.unwrap_or(word))))
230 }
231 Some(SymbolKind::Method) => Some(mir_analyzer::Name::Method {
235 class: norm(target_fqn.unwrap_or("")),
236 name: Arc::from(word.to_ascii_lowercase()),
238 }),
239 Some(SymbolKind::Property) => Some(mir_analyzer::Name::Property {
240 class: norm(target_fqn.unwrap_or("")),
241 name: Arc::from(word),
242 }),
243 Some(SymbolKind::Constant) => Some(if class_constant {
244 mir_analyzer::Name::ClassConstant {
245 class: norm(target_fqn?),
246 name: Arc::from(word),
247 }
248 } else {
249 mir_analyzer::Name::GlobalConstant(norm(target_fqn.unwrap_or(word)))
250 }),
251 None => None,
252 }
253}