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::document::ast::ParsedDoc;
20use crate::document::document_store::DocumentStore;
21use crate::document::open_files::OpenFiles;
22use crate::lang::autoload::Psr4Map;
23use crate::lang::config::LspConfig;
24use crate::lang::phpstorm_meta::PhpStormMeta;
25use crate::text::fqn_short_name;
26
27use crate::navigation::references::find_constructor_references;
28
29use crate::analysis::diagnostics::merge_file_diagnostics;
30use crate::document::open_files::compute_open_file_diagnostics;
31
32pub struct Backend {
33 client: Client,
34 docs: Arc<DocumentStore>,
35 open_files: OpenFiles,
39 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
40 psr4: Arc<ArcSwap<Psr4Map>>,
41 meta: Arc<ArcSwap<PhpStormMeta>>,
42 config: Arc<ArcSwap<LspConfig>>,
43}
44
45impl Backend {
46 pub fn new(client: Client) -> Self {
47 let docs = Arc::new(DocumentStore::new());
52 let psr4 = docs.psr4_arc();
53 Backend {
54 client,
55 docs,
56 open_files: OpenFiles::new(),
57 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
58 psr4,
59 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
60 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
61 }
62 }
63
64 fn set_open_text(&self, uri: Url, text: String) -> u64 {
65 self.open_files.set_open_text(&self.docs, uri, text)
66 }
67
68 fn close_open_file(&self, uri: &Url) {
69 self.open_files.close(&self.docs, uri);
70 }
71
72 fn ingest_if_not_open(&self, uri: Url, text: &str) {
76 if !self.open_files.contains(&uri) {
77 self.docs.ingest(uri, text);
78 }
79 }
80
81 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
83 if !self.open_files.contains(&uri) {
84 self.docs.ingest_from_doc(uri, doc);
85 }
86 }
87
88 fn get_open_text(&self, uri: &Url) -> Option<String> {
89 self.open_files.text(uri)
90 }
91
92 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
93 self.open_files.set_parse_diagnostics(uri, diagnostics);
94 }
95
96 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
97 self.open_files.parse_diagnostics(uri)
98 }
99
100 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
101 self.open_files.all_with_diagnostics()
102 }
103
104 fn open_urls(&self) -> Vec<Url> {
105 self.open_files.urls()
106 }
107
108 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
109 self.open_files.get_doc(&self.docs, uri)
110 }
111
112 fn codebase(&self) -> mir_analyzer::db::MirDbStorage {
115 let php_version = self.docs.workspace_php_version();
116 let session = self.docs.analysis_session(php_version);
117 session.snapshot_db()
118 }
119
120 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
122 self.docs
123 .get_doc_salsa(uri)
124 .map(|doc| crate::references::collect_file_imports(&doc))
125 .unwrap_or_default()
126 }
127
128 fn construct_references(
141 &self,
142 uri: &Url,
143 source: &str,
144 position: Position,
145 class_name: &str,
146 include_declaration: bool,
147 ) -> Vec<Location> {
148 let short_name = fqn_short_name(class_name).to_owned();
149 let class_fqn = class_name.contains('\\').then_some(class_name);
150 let candidate_docs = self.docs.candidate_docs_for(&short_name);
152 let mut locations = find_constructor_references(&short_name, &candidate_docs, class_fqn);
156 if include_declaration && let Some(range) = crate::text::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 =
194 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
195 Some(crate::navigation::moniker::resolve_fqn(
197 doc,
198 &short_owner,
199 &imports,
200 ))
201 }
202 Some(SymbolKind::Property) => {
203 let stmts = &doc.program().stmts;
209 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
210 let short_owner =
211 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
212 Some(crate::navigation::moniker::resolve_fqn(
213 doc,
214 &short_owner,
215 &imports,
216 ))
217 }
218 Some(SymbolKind::Constant) => {
219 if constant_owner.is_some() {
220 constant_owner
222 } else {
223 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
226 fqn.contains('\\').then_some(fqn)
227 }
228 }
229 _ => None,
230 }
231 }
232
233 fn session_method_references(
246 &self,
247 word: &str,
248 kind: Option<crate::navigation::references::SymbolKind>,
249 target_fqn: Option<&str>,
250 owner_short: Option<&str>,
251 ) -> Option<Vec<Location>> {
252 if !matches!(
253 kind,
254 Some(crate::navigation::references::SymbolKind::Method)
255 ) {
256 return None;
257 }
258 let sym = build_mir_symbol(word, kind, target_fqn)?;
259 let locs = self
260 .docs
261 .session_references_to(&sym)
262 .into_iter()
263 .filter_map(|tuple| {
264 let loc = crate::references::session_tuple_to_location(tuple)?;
265 if let Some(short) = owner_short {
266 let mentions = self
267 .docs
268 .source_text(&loc.uri)
269 .as_ref()
270 .map(|src| src.contains(short))
271 .unwrap_or(true);
272 if !mentions {
273 return None;
274 }
275 }
276 Some(loc)
277 })
278 .collect();
279 Some(locs)
280 }
281
282 fn session_property_references(
294 &self,
295 word: &str,
296 kind: Option<crate::navigation::references::SymbolKind>,
297 target_fqn: Option<&str>,
298 ) -> Option<Vec<Location>> {
299 if !matches!(
300 kind,
301 Some(crate::navigation::references::SymbolKind::Property)
302 ) {
303 return None;
304 }
305 let sym = build_mir_symbol(word, kind, target_fqn)?;
306 let locs = self
307 .docs
308 .session_references_to(&sym)
309 .into_iter()
310 .filter_map(crate::references::session_tuple_to_location)
311 .collect();
312 Some(locs)
313 }
314
315 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
318 let roots = self.root_paths.load();
319 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
320 }
321
322 async fn compute_dependent_publishes(
327 &self,
328 changed_uri: &Url,
329 diag_cfg: &crate::lang::config::DiagnosticsConfig,
330 ) -> Vec<(Url, Vec<Diagnostic>)> {
331 compute_dependent_publishes_owned(
332 Arc::clone(&self.docs),
333 self.open_files.clone(),
334 changed_uri.clone(),
335 diag_cfg.clone(),
336 )
337 .await
338 }
339}
340
341fn build_mir_symbol(
347 word: &str,
348 kind: Option<crate::navigation::references::SymbolKind>,
349 target_fqn: Option<&str>,
350) -> Option<mir_analyzer::Name> {
351 use crate::navigation::references::SymbolKind;
352 use std::sync::Arc as StdArc;
353 match kind {
354 Some(SymbolKind::Function) => {
355 target_fqn.map(|fqn| mir_analyzer::Name::Function(StdArc::from(fqn)))
356 }
357 Some(SymbolKind::Class) => {
358 target_fqn.map(|fqn| mir_analyzer::Name::Class(StdArc::from(fqn)))
359 }
360 Some(SymbolKind::Method) => target_fqn.map(|owning| mir_analyzer::Name::Method {
361 class: StdArc::from(owning),
362 name: StdArc::from(word.to_ascii_lowercase()),
365 }),
366 Some(SymbolKind::Property) => target_fqn.map(|owning| mir_analyzer::Name::Property {
367 class: StdArc::from(owning),
368 name: StdArc::from(word),
369 }),
370 Some(SymbolKind::Constant) | None => None,
371 }
372}
373
374fn resolve_reference_symbol(
383 doc_opt: Option<&Arc<ParsedDoc>>,
384 source: &str,
385 position: Position,
386 word: String,
387) -> (
388 String,
389 Option<crate::navigation::references::SymbolKind>,
390 Option<String>,
391) {
392 use crate::navigation::references::SymbolKind;
393 let mut constant_owner: Option<String> = None;
394 let (word, kind) = if let Some(doc) = doc_opt
395 && let Some(prop_name) =
396 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
397 {
398 (prop_name, Some(SymbolKind::Property))
399 } else if let Some(doc) = doc_opt {
400 let stmts = &doc.program().stmts;
401 if cursor_is_on_method_decl(doc.source(), stmts, position) {
402 (word, Some(SymbolKind::Method))
403 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
404 (prop_name, Some(SymbolKind::Property))
405 } else if let Some((const_name, owner)) =
406 cursor_is_on_constant_decl(doc.source(), stmts, position)
407 {
408 constant_owner = owner;
409 (const_name, Some(SymbolKind::Constant))
410 } else {
411 let k = symbol_kind_at(source, position, &word);
412 if matches!(k, Some(SymbolKind::Constant))
417 && let Some(raw) = class_before_double_colon(source, position)
418 {
419 constant_owner = Some(match raw.as_str() {
420 "self" | "static" => {
421 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
422 .unwrap_or(raw)
423 }
424 _ => raw,
425 });
426 }
427 (word, k)
428 }
429 } else {
430 let k = symbol_kind_at(source, position, &word);
431 (word, k)
432 };
433 (word, kind, constant_owner)
434}
435
436fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
444 let line = source.lines().nth(position.line as usize)?;
445 let chars: Vec<char> = line.chars().collect();
446 let col = position.character as usize;
447
448 let mut utf16_col = 0usize;
449 let mut char_idx = 0usize;
450 for ch in &chars {
451 if utf16_col >= col {
452 break;
453 }
454 utf16_col += ch.len_utf16();
455 char_idx += 1;
456 }
457
458 let is_word = |c: char| c.is_alphanumeric() || c == '_';
459 while char_idx > 0 && is_word(chars[char_idx - 1]) {
460 char_idx -= 1;
461 }
462
463 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
464 return None;
465 }
466
467 let class_end = char_idx - 2;
468 let mut class_start = class_end;
469 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
470 class_start -= 1;
471 }
472
473 let name: String = chars[class_start..class_end].iter().collect();
474 if name.is_empty() { None } else { Some(name) }
475}
476
477async fn compute_dependent_publishes_owned(
482 docs: Arc<DocumentStore>,
483 open_files: OpenFiles,
484 changed_uri: Url,
485 diag_cfg: crate::lang::config::DiagnosticsConfig,
486) -> Vec<(Url, Vec<Diagnostic>)> {
487 tokio::task::spawn_blocking(move || {
488 let php_version = docs.workspace_php_version();
496 let session = docs.analysis_session(php_version);
497 let analyses = session.reanalyze_dependents(changed_uri.as_str());
498 if analyses.is_empty() {
499 return Vec::new();
500 }
501
502 let open_urls: std::collections::HashSet<Url> = open_files
505 .urls()
506 .into_iter()
507 .filter(|u| u != &changed_uri)
508 .collect();
509 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
510 .into_iter()
511 .filter_map(|(file, analysis)| {
512 let url = Url::parse(file.as_ref()).ok()?;
513 open_urls.contains(&url).then_some((url, analysis))
514 })
515 .collect();
516 if dependents.is_empty() {
517 return Vec::new();
518 }
519
520 let dep_files: Vec<Arc<str>> = dependents
524 .iter()
525 .map(|(u, _)| Arc::from(u.as_str()))
526 .collect();
527 let class_issues = session.class_issues(&dep_files);
528 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
529 std::collections::HashMap::new();
530 for issue in class_issues {
531 if issue.suppressed {
532 continue;
533 }
534 let file = issue.location.file.clone();
535 class_issues_by_file.entry(file).or_default().push(issue);
536 }
537
538 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
539 for (url, analysis) in dependents {
540 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
541 let mut issues: Vec<mir_issues::Issue> = analysis
542 .issues
543 .into_iter()
544 .filter(|i| !i.suppressed)
545 .collect();
546 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
547 issues.extend(extra);
548 }
549 let semantic =
550 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
551 out.push((url, merge_file_diagnostics(parse, semantic)));
552 }
553 out
554 })
555 .await
556 .unwrap_or_default()
557}
558
559pub(super) async fn publish_with_dependents(
563 client: Client,
564 docs: Arc<DocumentStore>,
565 open_files: OpenFiles,
566 uri: Url,
567 diag_cfg: crate::lang::config::DiagnosticsConfig,
568) {
569 let docs_ref = Arc::clone(&docs);
570 let open_files_ref = open_files.clone();
571 let uri_ref = uri.clone();
572 let diag_cfg_ref = diag_cfg.clone();
573 let all_diags = tokio::task::spawn_blocking(move || {
574 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
575 })
576 .await
577 .unwrap_or_default();
578 client
579 .publish_diagnostics(uri.clone(), all_diags, None)
580 .await;
581 let dependents = compute_dependent_publishes_owned(docs, open_files, uri, diag_cfg).await;
582 for (dep_uri, dep_diags) in dependents {
583 client.publish_diagnostics(dep_uri, dep_diags, None).await;
584 }
585}
586
587fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
590 use std::collections::hash_map::DefaultHasher;
591 use std::hash::{Hash, Hasher};
592
593 let mut hasher = DefaultHasher::new();
594 uri.hash(&mut hasher);
595 diagnostics.len().hash(&mut hasher);
596
597 for diag in diagnostics {
598 diag.range.start.line.hash(&mut hasher);
599 diag.range.start.character.hash(&mut hasher);
600 diag.range.end.line.hash(&mut hasher);
601 diag.range.end.character.hash(&mut hasher);
602 diag.message.hash(&mut hasher);
603 let severity_val = match diag.severity {
604 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
605 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
606 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
607 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
608 None => 0,
609 _ => 5, };
611 severity_val.hash(&mut hasher);
612 if let Some(code) = &diag.code {
613 format!("{:?}", code).hash(&mut hasher);
614 }
615 if let Some(source) = &diag.source {
616 source.hash(&mut hasher);
617 }
618 if let Some(tags) = &diag.tags {
619 for tag in tags {
620 let tag_val = match *tag {
621 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
622 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
623 _ => 3,
624 };
625 tag_val.hash(&mut hasher);
626 }
627 }
628 }
629
630 format!("v1:{:x}", hasher.finish())
631}
632
633mod handlers;
634mod helpers;
635pub mod panic_guard;
636mod server;
637#[cfg(test)]
638mod tests;