1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[allow(unused_imports)]
5use self::helpers::*;
6
7use arc_swap::ArcSwap;
8
9enum IndexReadyNotification {}
12impl tower_lsp::lsp_types::notification::Notification for IndexReadyNotification {
13 type Params = ();
14 const METHOD: &'static str = "$/php-lsp/indexReady";
15}
16use tower_lsp::Client;
17use tower_lsp::lsp_types::*;
18
19use crate::ast::ParsedDoc;
20use crate::autoload::Psr4Map;
21use crate::config::LspConfig;
22use crate::document_store::DocumentStore;
23use crate::open_files::OpenFiles;
24use crate::phpstorm_meta::PhpStormMeta;
25use crate::util::fqn_short_name;
26
27use crate::navigation::references::find_constructor_references;
28
29use crate::analysis::diagnostics::merge_file_diagnostics;
30
31pub struct Backend {
32 client: Client,
33 docs: Arc<DocumentStore>,
34 open_files: OpenFiles,
38 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
39 psr4: Arc<ArcSwap<Psr4Map>>,
40 meta: Arc<ArcSwap<PhpStormMeta>>,
41 config: Arc<ArcSwap<LspConfig>>,
42}
43
44impl Backend {
45 pub fn new(client: Client) -> Self {
46 let docs = Arc::new(DocumentStore::new());
51 let psr4 = docs.psr4_arc();
52 Backend {
53 client,
54 docs,
55 open_files: OpenFiles::new(),
56 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
57 psr4,
58 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
59 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
60 }
61 }
62
63 fn set_open_text(&self, uri: Url, text: String) -> u64 {
66 self.open_files.set_open_text(&self.docs, uri, text)
67 }
68
69 fn close_open_file(&self, uri: &Url) {
70 self.open_files.close(&self.docs, uri);
71 }
72
73 fn index_if_not_open(&self, uri: Url, text: &str) {
77 if !self.open_files.contains(&uri) {
78 self.docs.index(uri, text);
79 }
80 }
81
82 fn index_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
84 if !self.open_files.contains(&uri) {
85 self.docs.index_from_doc(uri, doc);
86 }
87 }
88
89 fn get_open_text(&self, uri: &Url) -> Option<String> {
90 self.open_files.text(uri)
91 }
92
93 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
94 self.open_files.set_parse_diagnostics(uri, diagnostics);
95 }
96
97 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
98 self.open_files.parse_diagnostics(uri)
99 }
100
101 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
102 self.open_files.all_with_diagnostics()
103 }
104
105 fn open_urls(&self) -> Vec<Url> {
106 self.open_files.urls()
107 }
108
109 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
110 self.open_files.get_doc(&self.docs, uri)
111 }
112
113 fn codebase(&self) -> mir_analyzer::db::MirDbStorage {
116 let php_version = self.docs.workspace_php_version();
117 let session = self.docs.analysis_session(php_version);
118 session.snapshot_db()
119 }
120
121 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
123 self.docs
124 .get_doc_salsa(uri)
125 .map(|doc| crate::references::collect_file_imports(&doc))
126 .unwrap_or_default()
127 }
128
129 fn construct_references(
142 &self,
143 uri: &Url,
144 source: &str,
145 position: Position,
146 class_name: &str,
147 include_declaration: bool,
148 ) -> Vec<Location> {
149 let all_docs = self.docs.all_docs_for_scan();
150 let short_name = fqn_short_name(class_name).to_owned();
151 let class_fqn = class_name.contains('\\').then_some(class_name);
152 let mut locations = find_constructor_references(&short_name, &all_docs, class_fqn);
156 if include_declaration && let Some(range) = crate::util::word_range_at(source, position) {
161 locations.push(Location {
162 uri: uri.clone(),
163 range,
164 });
165 }
166 locations
167 }
168
169 fn resolve_reference_target_fqn(
175 &self,
176 uri: &Url,
177 doc_opt: Option<&Arc<ParsedDoc>>,
178 word: &str,
179 kind: Option<crate::navigation::references::SymbolKind>,
180 position: Position,
181 constant_owner: Option<String>,
182 ) -> Option<String> {
183 use crate::navigation::references::SymbolKind;
184 let doc = doc_opt?;
185 let imports = self.file_imports(uri);
186 match kind {
187 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
188 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
189 resolved.contains('\\').then_some(resolved)
190 }
191 Some(SymbolKind::Method) => {
192 let short_owner = crate::type_map::enclosing_class_at(doc.source(), doc, position)?;
194 Some(crate::navigation::moniker::resolve_fqn(
196 doc,
197 &short_owner,
198 &imports,
199 ))
200 }
201 Some(SymbolKind::Property) => {
202 let stmts = &doc.program().stmts;
208 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
209 let short_owner = crate::type_map::enclosing_class_at(doc.source(), doc, position)?;
210 Some(crate::navigation::moniker::resolve_fqn(
211 doc,
212 &short_owner,
213 &imports,
214 ))
215 }
216 Some(SymbolKind::Constant) => {
217 if constant_owner.is_some() {
218 constant_owner
220 } else {
221 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
224 fqn.contains('\\').then_some(fqn)
225 }
226 }
227 _ => None,
228 }
229 }
230
231 fn session_method_references(
244 &self,
245 word: &str,
246 kind: Option<crate::navigation::references::SymbolKind>,
247 target_fqn: Option<&str>,
248 owner_short: Option<&str>,
249 ) -> Option<Vec<Location>> {
250 if !matches!(
251 kind,
252 Some(crate::navigation::references::SymbolKind::Method)
253 ) {
254 return None;
255 }
256 let sym = build_mir_symbol(word, kind, target_fqn)?;
257 let locs = self
258 .docs
259 .session_references_to(&sym)
260 .into_iter()
261 .filter_map(|tuple| {
262 let loc = crate::references::session_tuple_to_location(tuple)?;
263 if let Some(short) = owner_short {
264 let mentions = self
265 .docs
266 .source_text(&loc.uri)
267 .as_ref()
268 .map(|src| src.contains(short))
269 .unwrap_or(true);
270 if !mentions {
271 return None;
272 }
273 }
274 Some(loc)
275 })
276 .collect();
277 Some(locs)
278 }
279
280 fn session_property_references(
292 &self,
293 word: &str,
294 kind: Option<crate::navigation::references::SymbolKind>,
295 target_fqn: Option<&str>,
296 ) -> Option<Vec<Location>> {
297 if !matches!(
298 kind,
299 Some(crate::navigation::references::SymbolKind::Property)
300 ) {
301 return None;
302 }
303 let sym = build_mir_symbol(word, kind, target_fqn)?;
304 let locs = self
305 .docs
306 .session_references_to(&sym)
307 .into_iter()
308 .filter_map(crate::references::session_tuple_to_location)
309 .collect();
310 Some(locs)
311 }
312
313 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
316 let roots = self.root_paths.load();
317 crate::autoload::resolve_php_version_from_roots(&roots, explicit)
318 }
319
320 async fn compute_dependent_publishes(
325 &self,
326 changed_uri: &Url,
327 diag_cfg: &crate::config::DiagnosticsConfig,
328 ) -> Vec<(Url, Vec<Diagnostic>)> {
329 compute_dependent_publishes_owned(
330 Arc::clone(&self.docs),
331 self.open_files.clone(),
332 changed_uri.clone(),
333 diag_cfg.clone(),
334 )
335 .await
336 }
337}
338
339fn build_mir_symbol(
345 word: &str,
346 kind: Option<crate::navigation::references::SymbolKind>,
347 target_fqn: Option<&str>,
348) -> Option<mir_analyzer::Name> {
349 use crate::navigation::references::SymbolKind;
350 use std::sync::Arc as StdArc;
351 match kind {
352 Some(SymbolKind::Function) => {
353 target_fqn.map(|fqn| mir_analyzer::Name::Function(StdArc::from(fqn)))
354 }
355 Some(SymbolKind::Class) => {
356 target_fqn.map(|fqn| mir_analyzer::Name::Class(StdArc::from(fqn)))
357 }
358 Some(SymbolKind::Method) => target_fqn.map(|owning| mir_analyzer::Name::Method {
359 class: StdArc::from(owning),
360 name: StdArc::from(word.to_ascii_lowercase()),
363 }),
364 Some(SymbolKind::Property) => target_fqn.map(|owning| mir_analyzer::Name::Property {
365 class: StdArc::from(owning),
366 name: StdArc::from(word),
367 }),
368 Some(SymbolKind::Constant) | None => None,
369 }
370}
371
372fn resolve_reference_symbol(
381 doc_opt: Option<&Arc<ParsedDoc>>,
382 source: &str,
383 position: Position,
384 word: String,
385) -> (
386 String,
387 Option<crate::navigation::references::SymbolKind>,
388 Option<String>,
389) {
390 use crate::navigation::references::SymbolKind;
391 let mut constant_owner: Option<String> = None;
392 let (word, kind) = if let Some(doc) = doc_opt
393 && let Some(prop_name) =
394 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
395 {
396 (prop_name, Some(SymbolKind::Property))
397 } else if let Some(doc) = doc_opt {
398 let stmts = &doc.program().stmts;
399 if cursor_is_on_method_decl(doc.source(), stmts, position) {
400 (word, Some(SymbolKind::Method))
401 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
402 (prop_name, Some(SymbolKind::Property))
403 } else if let Some((const_name, owner)) =
404 cursor_is_on_constant_decl(doc.source(), stmts, position)
405 {
406 constant_owner = owner;
407 (const_name, Some(SymbolKind::Constant))
408 } else {
409 let k = symbol_kind_at(source, position, &word);
410 (word, k)
411 }
412 } else {
413 let k = symbol_kind_at(source, position, &word);
414 (word, k)
415 };
416 (word, kind, constant_owner)
417}
418
419async fn compute_dependent_publishes_owned(
424 docs: Arc<DocumentStore>,
425 open_files: OpenFiles,
426 changed_uri: Url,
427 diag_cfg: crate::config::DiagnosticsConfig,
428) -> Vec<(Url, Vec<Diagnostic>)> {
429 tokio::task::spawn_blocking(move || {
430 let php_version = docs.workspace_php_version();
438 let session = docs.analysis_session(php_version);
439 let analyses = session.reanalyze_dependents(changed_uri.as_str());
440 if analyses.is_empty() {
441 return Vec::new();
442 }
443
444 let open_urls: std::collections::HashSet<Url> = open_files
447 .urls()
448 .into_iter()
449 .filter(|u| u != &changed_uri)
450 .collect();
451 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
452 .into_iter()
453 .filter_map(|(file, analysis)| {
454 let url = Url::parse(file.as_ref()).ok()?;
455 open_urls.contains(&url).then_some((url, analysis))
456 })
457 .collect();
458 if dependents.is_empty() {
459 return Vec::new();
460 }
461
462 let dep_files: Vec<Arc<str>> = dependents
466 .iter()
467 .map(|(u, _)| Arc::from(u.as_str()))
468 .collect();
469 let class_issues = session.class_issues(&dep_files);
470 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
471 std::collections::HashMap::new();
472 for issue in class_issues {
473 if issue.suppressed {
474 continue;
475 }
476 let file = issue.location.file.clone();
477 class_issues_by_file.entry(file).or_default().push(issue);
478 }
479
480 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
481 for (url, analysis) in dependents {
482 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
483 let mut issues: Vec<mir_issues::Issue> = analysis
484 .issues
485 .into_iter()
486 .filter(|i| !i.suppressed)
487 .collect();
488 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
489 issues.extend(extra);
490 }
491 let semantic =
492 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
493 out.push((url, merge_file_diagnostics(parse, semantic)));
494 }
495 out
496 })
497 .await
498 .unwrap_or_default()
499}
500
501fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
504 use std::collections::hash_map::DefaultHasher;
505 use std::hash::{Hash, Hasher};
506
507 let mut hasher = DefaultHasher::new();
508 uri.hash(&mut hasher);
509 diagnostics.len().hash(&mut hasher);
510
511 for diag in diagnostics {
512 diag.range.start.line.hash(&mut hasher);
513 diag.range.start.character.hash(&mut hasher);
514 diag.range.end.line.hash(&mut hasher);
515 diag.range.end.character.hash(&mut hasher);
516 diag.message.hash(&mut hasher);
517 let severity_val = match diag.severity {
518 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
519 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
520 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
521 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
522 None => 0,
523 _ => 5, };
525 severity_val.hash(&mut hasher);
526 if let Some(code) = &diag.code {
527 format!("{:?}", code).hash(&mut hasher);
528 }
529 if let Some(source) = &diag.source {
530 source.hash(&mut hasher);
531 }
532 if let Some(tags) = &diag.tags {
533 for tag in tags {
534 let tag_val = match *tag {
535 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
536 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
537 _ => 3,
538 };
539 tag_val.hash(&mut hasher);
540 }
541 }
542 }
543
544 format!("v1:{:x}", hasher.finish())
545}
546
547mod helpers;
548mod server;
549#[cfg(test)]
550mod tests;