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
19#[derive(Debug, serde::Serialize, serde::Deserialize)]
21pub struct DebugStats {
22 pub parses: u64,
24 pub ref_index_locks: u64,
27 pub warm_sweeps_completed: u64,
30}
31
32use crate::document::ast::ParsedDoc;
33use crate::document::document_store::DocumentStore;
34use crate::document::open_files::OpenFiles;
35use crate::lang::autoload::Psr4Map;
36use crate::lang::config::LspConfig;
37use crate::lang::phpstorm_meta::PhpStormMeta;
38use crate::text::fqn_short_name;
39
40use crate::navigation::references::find_constructor_references;
41
42use crate::analysis::diagnostics::merge_file_diagnostics;
43use crate::document::open_files::compute_open_file_diagnostics;
44
45pub struct Backend {
46 client: Client,
47 docs: Arc<DocumentStore>,
48 open_files: OpenFiles,
52 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
53 psr4: Arc<ArcSwap<Psr4Map>>,
54 meta: Arc<ArcSwap<PhpStormMeta>>,
55 config: Arc<ArcSwap<LspConfig>>,
56}
57
58impl Backend {
59 pub fn new(client: Client) -> Self {
60 let docs = Arc::new(DocumentStore::new());
65 let psr4 = docs.psr4_arc();
66 Backend {
67 client,
68 docs,
69 open_files: OpenFiles::new(),
70 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
71 psr4,
72 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
73 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
74 }
75 }
76
77 pub async fn debug_stats(&self) -> tower_lsp::jsonrpc::Result<DebugStats> {
81 Ok(DebugStats {
82 parses: self.docs.parse_count(),
83 ref_index_locks: self.docs.ref_index_lock_count(),
84 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
85 })
86 }
87
88 fn set_open_text(&self, uri: Url, text: String) -> u64 {
89 self.open_files.set_open_text(&self.docs, uri, text)
90 }
91
92 fn close_open_file(&self, uri: &Url) {
93 self.open_files.close(&self.docs, uri);
94 }
95
96 fn ingest_if_not_open(&self, uri: Url, text: &str) {
100 if !self.open_files.contains(&uri) {
101 self.docs.ingest(uri, text);
102 }
103 }
104
105 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
107 if !self.open_files.contains(&uri) {
108 self.docs.ingest_from_doc(uri, doc);
109 }
110 }
111
112 fn get_open_text(&self, uri: &Url) -> Option<String> {
113 self.open_files.text(uri)
114 }
115
116 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
117 self.open_files.set_parse_diagnostics(uri, diagnostics);
118 }
119
120 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
121 self.open_files.parse_diagnostics(uri)
122 }
123
124 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
125 self.open_files.all_with_diagnostics()
126 }
127
128 fn open_urls(&self) -> Vec<Url> {
129 self.open_files.urls()
130 }
131
132 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
133 self.open_files.get_doc(&self.docs, uri)
134 }
135
136 fn get_doc_stale(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
137 self.open_files.get_doc_stale(&self.docs, uri)
138 }
139
140 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
142 self.docs
143 .get_doc_salsa(uri)
144 .map(|doc| crate::references::collect_file_imports(&doc))
145 .unwrap_or_default()
146 }
147
148 fn construct_references(
161 &self,
162 uri: &Url,
163 source: &str,
164 position: Position,
165 class_name: &str,
166 include_declaration: bool,
167 ) -> Vec<Location> {
168 let short_name = fqn_short_name(class_name).to_owned();
169 let class_fqn = class_name.contains('\\').then_some(class_name);
170 let candidate_docs = self.docs.candidate_docs_for(&short_name);
172 let mut locations = find_constructor_references(&short_name, &candidate_docs, class_fqn);
176 if include_declaration && let Some(range) = crate::text::word_range_at(source, position) {
181 locations.push(Location {
182 uri: uri.clone(),
183 range,
184 });
185 }
186 locations
187 }
188
189 fn resolve_reference_target_fqn(
195 &self,
196 uri: &Url,
197 doc_opt: Option<&Arc<ParsedDoc>>,
198 word: &str,
199 kind: Option<crate::navigation::references::SymbolKind>,
200 position: Position,
201 constant_owner: Option<String>,
202 ) -> Option<String> {
203 use crate::navigation::references::SymbolKind;
204 let doc = doc_opt?;
205 let imports = self.file_imports(uri);
206 match kind {
207 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
208 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
209 resolved.contains('\\').then_some(resolved)
210 }
211 Some(SymbolKind::Method) => {
212 let short_owner =
214 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
215 Some(crate::navigation::moniker::resolve_fqn(
217 doc,
218 &short_owner,
219 &imports,
220 ))
221 }
222 Some(SymbolKind::Property) => {
223 let stmts = &doc.program().stmts;
229 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
230 let short_owner =
231 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
232 Some(crate::navigation::moniker::resolve_fqn(
233 doc,
234 &short_owner,
235 &imports,
236 ))
237 }
238 Some(SymbolKind::Constant) => {
239 if constant_owner.is_some() {
240 constant_owner
242 } else {
243 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
246 fqn.contains('\\').then_some(fqn)
247 }
248 }
249 _ => None,
250 }
251 }
252
253 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
256 let roots = self.root_paths.load();
257 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
258 }
259}
260
261fn resolve_reference_symbol(
270 doc_opt: Option<&Arc<ParsedDoc>>,
271 source: &str,
272 position: Position,
273 word: String,
274) -> (
275 String,
276 Option<crate::navigation::references::SymbolKind>,
277 Option<String>,
278) {
279 use crate::navigation::references::SymbolKind;
280 let mut constant_owner: Option<String> = None;
281 let (word, kind) = if let Some(doc) = doc_opt
282 && let Some(prop_name) =
283 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
284 {
285 (prop_name, Some(SymbolKind::Property))
286 } else if let Some(doc) = doc_opt {
287 let stmts = &doc.program().stmts;
288 if cursor_is_on_method_decl(doc.source(), stmts, position) {
289 (word, Some(SymbolKind::Method))
290 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
291 (prop_name, Some(SymbolKind::Property))
292 } else if let Some((const_name, owner)) =
293 cursor_is_on_constant_decl(doc.source(), stmts, position)
294 {
295 constant_owner = owner;
296 (const_name, Some(SymbolKind::Constant))
297 } else {
298 let k = symbol_kind_at(source, position, &word);
299 if matches!(k, Some(SymbolKind::Constant))
304 && let Some(raw) = class_before_double_colon(source, position)
305 {
306 constant_owner = Some(match raw.as_str() {
307 "self" | "static" => {
308 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
309 .unwrap_or(raw)
310 }
311 _ => raw,
312 });
313 }
314 (word, k)
315 }
316 } else {
317 let k = symbol_kind_at(source, position, &word);
318 (word, k)
319 };
320 (word, kind, constant_owner)
321}
322
323fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
331 let line = source.lines().nth(position.line as usize)?;
332 let chars: Vec<char> = line.chars().collect();
333 let col = position.character as usize;
334
335 let mut utf16_col = 0usize;
336 let mut char_idx = 0usize;
337 for ch in &chars {
338 if utf16_col >= col {
339 break;
340 }
341 utf16_col += ch.len_utf16();
342 char_idx += 1;
343 }
344
345 let is_word = |c: char| c.is_alphanumeric() || c == '_';
346 while char_idx > 0 && is_word(chars[char_idx - 1]) {
347 char_idx -= 1;
348 }
349
350 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
351 return None;
352 }
353
354 let class_end = char_idx - 2;
355 let mut class_start = class_end;
356 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
357 class_start -= 1;
358 }
359
360 let name: String = chars[class_start..class_end].iter().collect();
361 if name.is_empty() { None } else { Some(name) }
362}
363
364async fn compute_dependent_publishes_owned(
369 docs: Arc<DocumentStore>,
370 open_files: OpenFiles,
371 changed_uri: Url,
372 diag_cfg: crate::lang::config::DiagnosticsConfig,
373) -> Vec<(Url, Vec<Diagnostic>)> {
374 tokio::task::spawn_blocking(move || {
375 let open_set: Vec<std::sync::Arc<str>> = open_files
381 .urls()
382 .into_iter()
383 .filter(|u| u != &changed_uri)
384 .map(|u| std::sync::Arc::from(u.as_str()))
385 .collect();
386 if open_set.is_empty() {
387 return Vec::new();
388 }
389
390 let php_version = docs.workspace_php_version();
391 let session = docs.analysis_session(php_version);
392 let _interactive = docs.interactive_read_guard();
395 let cancel = docs.begin_reanalyze();
399 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
400 if cancel.is_cancelled() || analyses.is_empty() {
404 return Vec::new();
405 }
406
407 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
408 .into_iter()
409 .filter_map(|(file, analysis)| {
410 let url = Url::parse(file.as_ref()).ok()?;
411 Some((url, analysis))
412 })
413 .collect();
414 if dependents.is_empty() {
415 return Vec::new();
416 }
417
418 let dep_files: Vec<Arc<str>> = dependents
422 .iter()
423 .map(|(u, _)| Arc::from(u.as_str()))
424 .collect();
425 let class_issues = session.class_issues(&dep_files);
426 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
427 std::collections::HashMap::new();
428 for issue in class_issues {
429 if issue.suppressed {
430 continue;
431 }
432 let file = issue.location.file.clone();
433 class_issues_by_file.entry(file).or_default().push(issue);
434 }
435
436 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
437 for (url, analysis) in dependents {
438 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
439 let mut issues: Vec<mir_issues::Issue> = analysis
440 .issues
441 .into_iter()
442 .filter(|i| !i.suppressed)
443 .collect();
444 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
445 issues.extend(extra);
446 }
447 let semantic =
448 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
449 out.push((url, merge_file_diagnostics(parse, semantic)));
450 }
451 out
452 })
453 .await
454 .unwrap_or_default()
455}
456
457pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
460 use std::collections::hash_map::DefaultHasher;
461 use std::hash::{Hash, Hasher};
462 let mut hasher = DefaultHasher::new();
463 serde_json::to_string(diagnostics)
464 .unwrap_or_default()
465 .hash(&mut hasher);
466 hasher.finish()
467}
468
469pub(super) async fn publish_with_dependents(
478 client: Client,
479 docs: Arc<DocumentStore>,
480 open_files: OpenFiles,
481 uri: Url,
482 diag_cfg: crate::lang::config::DiagnosticsConfig,
483) {
484 let docs_ref = Arc::clone(&docs);
485 let open_files_ref = open_files.clone();
486 let uri_ref = uri.clone();
487 let diag_cfg_ref = diag_cfg.clone();
488 let all_diags = tokio::task::spawn_blocking(move || {
489 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
490 })
491 .await
492 .unwrap_or_default();
493 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
494 client
495 .publish_diagnostics(uri.clone(), all_diags, None)
496 .await;
497 let dependents =
498 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg).await;
499 for (dep_uri, dep_diags) in dependents {
500 let hash = diagnostics_content_hash(&dep_diags);
501 if open_files.published_hash(&dep_uri) == Some(hash) {
502 continue;
503 }
504 open_files.note_published(&dep_uri, hash);
505 client.publish_diagnostics(dep_uri, dep_diags, None).await;
506 }
507}
508
509fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
512 use std::collections::hash_map::DefaultHasher;
513 use std::hash::{Hash, Hasher};
514
515 let mut hasher = DefaultHasher::new();
516 uri.hash(&mut hasher);
517 diagnostics.len().hash(&mut hasher);
518
519 for diag in diagnostics {
520 diag.range.start.line.hash(&mut hasher);
521 diag.range.start.character.hash(&mut hasher);
522 diag.range.end.line.hash(&mut hasher);
523 diag.range.end.character.hash(&mut hasher);
524 diag.message.hash(&mut hasher);
525 let severity_val = match diag.severity {
526 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
527 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
528 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
529 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
530 None => 0,
531 _ => 5, };
533 severity_val.hash(&mut hasher);
534 if let Some(code) = &diag.code {
535 format!("{:?}", code).hash(&mut hasher);
536 }
537 if let Some(source) = &diag.source {
538 source.hash(&mut hasher);
539 }
540 if let Some(tags) = &diag.tags {
541 for tag in tags {
542 let tag_val = match *tag {
543 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
544 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
545 _ => 3,
546 };
547 tag_val.hash(&mut hasher);
548 }
549 }
550 }
551
552 format!("v1:{:x}", hasher.finish())
553}
554
555mod handlers;
556mod helpers;
557pub mod panic_guard;
558mod server;
559#[cfg(test)]
560mod tests;