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_server::ls_types::notification::Notification for IndexReadyNotification {
13 type Params = ();
14 const METHOD: &'static str = "$/php-lsp/indexReady";
15}
16
17enum ReferencesPartialResult {}
25#[derive(serde::Serialize, serde::Deserialize)]
26#[serde(rename_all = "camelCase")]
27struct ReferencesPartialResultParams {
28 token: tower_lsp_server::ls_types::NumberOrString,
29 value: Vec<tower_lsp_server::ls_types::Location>,
30}
31impl tower_lsp_server::ls_types::notification::Notification for ReferencesPartialResult {
32 type Params = ReferencesPartialResultParams;
33 const METHOD: &'static str = "$/progress";
34}
35
36pub(crate) async fn send_references_partial_result(
40 client: &Client,
41 token: tower_lsp_server::ls_types::NumberOrString,
42 locations: Vec<tower_lsp_server::ls_types::Location>,
43) {
44 client
45 .send_notification::<ReferencesPartialResult>(ReferencesPartialResultParams {
46 token,
47 value: locations,
48 })
49 .await;
50}
51
52use tower_lsp_server::Client;
53use tower_lsp_server::ls_types::*;
54
55#[derive(Debug, serde::Serialize, serde::Deserialize)]
57pub struct DebugStats {
58 pub parses: u64,
60 pub ref_index_locks: u64,
64 pub warm_sweeps_completed: u64,
67 pub workspace_symbol_index_ready: bool,
70 pub workspace_index_walks: u64,
74 pub reachability_scan_passes: u64,
81 pub analysis_compute_count: u64,
86 pub warm_start_replays_completed: u64,
91}
92
93use crate::document::ast::ParsedDoc;
94use crate::document::document_store::DocumentStore;
95use crate::document::open_files::OpenFiles;
96use crate::lang::autoload::Psr4Map;
97use crate::lang::config::LspConfig;
98use crate::laravel::LaravelIndex;
99
100use crate::analysis::diagnostics::merge_file_diagnostics;
101use crate::document::open_files::compute_open_file_diagnostics;
102
103pub struct Backend {
104 client: Client,
105 docs: Arc<DocumentStore>,
106 open_files: OpenFiles,
110 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
111 psr4: Arc<ArcSwap<Psr4Map>>,
112 laravel: Arc<ArcSwap<LaravelIndex>>,
113 config: Arc<ArcSwap<LspConfig>>,
114 client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
120}
121
122impl Backend {
123 pub fn new(client: Client) -> Self {
124 let docs = Arc::new(DocumentStore::new());
129 let psr4 = docs.psr4_arc();
130 Backend {
131 client,
132 docs,
133 open_files: OpenFiles::new(),
134 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
135 psr4,
136 laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
137 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
138 client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
139 }
140 }
141
142 pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
146 Ok(DebugStats {
147 parses: self.docs.parse_count(),
148 ref_index_locks: self.docs.ref_index_lock_count(),
149 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
150 workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
151 workspace_index_walks: self.docs.workspace_index_walks(),
152 reachability_scan_passes: self.docs.reachability_scan_passes(),
153 analysis_compute_count: self.docs.analysis_compute_count(),
154 warm_start_replays_completed: self.docs.warm_start_replays_completed(),
155 })
156 }
157
158 fn set_open_text(&self, uri: Uri, text: String) -> u64 {
159 self.open_files.set_open_text(&self.docs, uri, text)
160 }
161
162 fn close_open_file(&self, uri: &Uri) {
163 self.open_files.close(&self.docs, uri);
164 }
165
166 fn ingest_if_not_open(&self, uri: Uri, text: &str) {
170 if !self.open_files.contains(&uri) {
171 self.docs.ingest(uri, text);
172 }
173 }
174
175 fn ingest_from_doc_if_not_open(&self, uri: Uri, doc: &ParsedDoc) {
177 if !self.open_files.contains(&uri) {
178 self.docs.ingest_from_doc(uri, doc);
179 }
180 }
181
182 fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
183 self.open_files.text(uri)
184 }
185
186 fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
187 self.open_files.set_parse_diagnostics(uri, diagnostics);
188 }
189
190 fn get_parse_diagnostics(&self, uri: &Uri) -> Option<Vec<Diagnostic>> {
191 self.open_files.parse_diagnostics(uri)
192 }
193
194 fn all_open_files_with_diagnostics(&self) -> Vec<(Uri, Vec<Diagnostic>, Option<i64>)> {
195 self.open_files.all_with_diagnostics()
196 }
197
198 fn open_urls(&self) -> Vec<Uri> {
199 self.open_files.urls()
200 }
201
202 fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
203 self.open_files.get_doc(&self.docs, uri)
204 }
205
206 fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
207 self.open_files.get_doc_stale(&self.docs, uri)
208 }
209
210 fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
214 self.docs
215 .get_doc_salsa(uri)
216 .map(|doc| doc.file_imports())
217 .unwrap_or_default()
218 }
219
220 fn resolve_reference_target_fqn(
226 &self,
227 uri: &Uri,
228 doc_opt: Option<&Arc<ParsedDoc>>,
229 word: &str,
230 kind: Option<crate::navigation::references::SymbolKind>,
231 position: Position,
232 constant_owner: Option<String>,
233 ) -> Option<String> {
234 use crate::navigation::references::SymbolKind;
235 let doc = doc_opt?;
236 let imports = self.file_imports(uri);
237 match kind {
238 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
239 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
240 resolved.contains('\\').then_some(resolved)
241 }
242 Some(SymbolKind::Method) => {
243 let short_owner =
245 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
246 Some(crate::navigation::moniker::resolve_fqn(
248 doc,
249 &short_owner,
250 &imports,
251 ))
252 }
253 Some(SymbolKind::Property) => {
254 let stmts = &doc.program().stmts;
259 if crate::backend::helpers::cursor_is_on_property_decl(
260 doc.source(),
261 stmts,
262 position,
263 )
264 .is_none()
265 && promoted_property_at_cursor(doc.source(), stmts, position).is_none()
266 {
267 return None;
268 }
269 let short_owner =
270 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
271 Some(crate::navigation::moniker::resolve_fqn(
272 doc,
273 &short_owner,
274 &imports,
275 ))
276 }
277 Some(SymbolKind::Constant) => {
278 if let Some(owner) = constant_owner {
279 if owner.contains('\\') {
282 Some(owner.trim_start_matches('\\').to_string())
283 } else {
284 Some(crate::navigation::moniker::resolve_fqn(
285 doc, &owner, &imports,
286 ))
287 }
288 } else {
289 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
292 fqn.contains('\\').then_some(fqn)
293 }
294 }
295 _ => None,
296 }
297 }
298
299 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
302 let roots = self.root_paths.load();
303 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
304 }
305}
306
307fn resolve_reference_symbol(
316 doc_opt: Option<&Arc<ParsedDoc>>,
317 source: &str,
318 position: Position,
319 word: String,
320) -> (
321 String,
322 Option<crate::navigation::references::SymbolKind>,
323 Option<String>,
324) {
325 use crate::navigation::references::SymbolKind;
326 let mut constant_owner: Option<String> = None;
327 let (word, kind) = if let Some(doc) = doc_opt
328 && let Some(prop_name) =
329 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
330 {
331 (prop_name, Some(SymbolKind::Property))
332 } else if let Some(doc) = doc_opt {
333 let stmts = &doc.program().stmts;
334 if cursor_is_on_method_decl(doc.source(), stmts, position) {
335 (word, Some(SymbolKind::Method))
336 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
337 (prop_name, Some(SymbolKind::Property))
338 } else if let Some((const_name, owner)) =
339 cursor_is_on_constant_decl(doc.source(), stmts, position)
340 {
341 constant_owner = owner;
342 (const_name, Some(SymbolKind::Constant))
343 } else {
344 let k = symbol_kind_at(source, position, &word);
345 if matches!(k, Some(SymbolKind::Constant))
350 && let Some(raw) = class_before_double_colon(source, position)
351 {
352 constant_owner = Some(match raw.as_str() {
353 "self" | "static" => {
354 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
355 .unwrap_or(raw)
356 }
357 _ => raw,
358 });
359 }
360 (word, k)
361 }
362 } else {
363 let k = symbol_kind_at(source, position, &word);
364 (word, k)
365 };
366 (word, kind, constant_owner)
367}
368
369fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
377 let line = source.lines().nth(position.line as usize)?;
378 let chars: Vec<char> = line.chars().collect();
379 let col = position.character as usize;
380
381 let mut utf16_col = 0usize;
382 let mut char_idx = 0usize;
383 for ch in &chars {
384 if utf16_col >= col {
385 break;
386 }
387 utf16_col += ch.len_utf16();
388 char_idx += 1;
389 }
390
391 let is_word = |c: char| c.is_alphanumeric() || c == '_';
392 while char_idx > 0 && is_word(chars[char_idx - 1]) {
393 char_idx -= 1;
394 }
395
396 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
397 return None;
398 }
399
400 let class_end = char_idx - 2;
401 let mut class_start = class_end;
402 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
403 class_start -= 1;
404 }
405
406 let name: String = chars[class_start..class_end].iter().collect();
407 if name.is_empty() { None } else { Some(name) }
408}
409
410async fn compute_dependent_publishes_owned(
415 docs: Arc<DocumentStore>,
416 open_files: OpenFiles,
417 changed_uri: Uri,
418 diag_cfg: crate::lang::config::DiagnosticsConfig,
419) -> Vec<(Uri, Vec<Diagnostic>)> {
420 tokio::task::spawn_blocking(move || {
421 let open_set: Vec<std::sync::Arc<str>> = open_files
427 .urls()
428 .into_iter()
429 .filter(|u| u != &changed_uri)
430 .map(|u| std::sync::Arc::from(u.as_str()))
431 .collect();
432 if open_set.is_empty() {
433 return Vec::new();
434 }
435
436 let session = docs.current_analysis_session();
437 let _interactive = docs.interactive_read_guard();
440 let cancel = docs.begin_reanalyze();
444 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
445 if cancel.is_cancelled() || analyses.is_empty() {
449 return Vec::new();
450 }
451
452 let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
453 .into_iter()
454 .filter_map(|(file, analysis)| {
455 let url = (file.as_ref()).parse::<Uri>().ok()?;
456 Some((url, analysis))
457 })
458 .collect();
459 if dependents.is_empty() {
460 return Vec::new();
461 }
462
463 let dep_files: Vec<Arc<str>> = dependents
467 .iter()
468 .map(|(u, _)| Arc::from(u.as_str()))
469 .collect();
470 let class_issues = session.class_issues(&dep_files);
471 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
472 std::collections::HashMap::new();
473 for issue in class_issues {
474 if issue.suppressed {
475 continue;
476 }
477 let file = issue.location.file.clone();
478 class_issues_by_file.entry(file).or_default().push(issue);
479 }
480
481 let mut out: Vec<(Uri, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
482 for (url, analysis) in dependents {
483 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
484 let mut issues: Vec<mir_issues::Issue> = analysis
485 .issues
486 .into_iter()
487 .filter(|i| !i.suppressed)
488 .collect();
489 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
490 issues.extend(extra);
491 }
492 let semantic =
493 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
494 let mut diags = merge_file_diagnostics(parse, semantic);
495 diags.extend(open_files.external_diagnostics(&url));
496 out.push((url, diags));
497 }
498 out
499 })
500 .await
501 .unwrap_or_default()
502}
503
504pub(super) async fn republish_after_index_ready(
517 client: Client,
518 docs: Arc<DocumentStore>,
519 open_files: OpenFiles,
520 diag_cfg: crate::lang::config::DiagnosticsConfig,
521) {
522 let uris = open_files.urls();
523 if uris.is_empty() {
524 return;
525 }
526 let docs_ref = Arc::clone(&docs);
527 let open_files_ref = open_files.clone();
528 let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
529 uris.into_iter()
530 .map(|uri| {
531 let diags =
532 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri, &diag_cfg);
533 (uri, diags)
534 })
535 .collect()
536 })
537 .await
538 .unwrap_or_default();
539 for (uri, diags) in all {
540 let hash = diagnostics_content_hash(&diags);
541 if open_files.published_hash(&uri) == Some(hash) {
542 continue;
543 }
544 open_files.note_published(&uri, hash);
545 client.publish_diagnostics(uri, diags, None).await;
546 }
547}
548
549pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
552 use std::collections::hash_map::DefaultHasher;
553 use std::hash::{Hash, Hasher};
554 let mut hasher = DefaultHasher::new();
555 serde_json::to_string(diagnostics)
556 .unwrap_or_default()
557 .hash(&mut hasher);
558 hasher.finish()
559}
560
561pub(super) async fn publish_with_dependents(
570 client: Client,
571 docs: Arc<DocumentStore>,
572 open_files: OpenFiles,
573 uri: Uri,
574 diag_cfg: crate::lang::config::DiagnosticsConfig,
575) {
576 let docs_ref = Arc::clone(&docs);
577 let open_files_ref = open_files.clone();
578 let uri_ref = uri.clone();
579 let diag_cfg_ref = diag_cfg.clone();
580 let all_diags = tokio::task::spawn_blocking(move || {
581 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
582 })
583 .await
584 .unwrap_or_default();
585 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
586 client
587 .publish_diagnostics(uri.clone(), all_diags, None)
588 .await;
589 let dependents =
590 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg).await;
591 for (dep_uri, dep_diags) in dependents {
592 let hash = diagnostics_content_hash(&dep_diags);
593 if open_files.published_hash(&dep_uri) == Some(hash) {
594 continue;
595 }
596 open_files.note_published(&dep_uri, hash);
597 client.publish_diagnostics(dep_uri, dep_diags, None).await;
598 }
599}
600
601fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
604 use std::collections::hash_map::DefaultHasher;
605 use std::hash::{Hash, Hasher};
606
607 let mut hasher = DefaultHasher::new();
608 uri.hash(&mut hasher);
609 diagnostics.len().hash(&mut hasher);
610
611 for diag in diagnostics {
612 diag.range.start.line.hash(&mut hasher);
613 diag.range.start.character.hash(&mut hasher);
614 diag.range.end.line.hash(&mut hasher);
615 diag.range.end.character.hash(&mut hasher);
616 diag.message.hash(&mut hasher);
617 let severity_val = match diag.severity {
618 Some(tower_lsp_server::ls_types::DiagnosticSeverity::ERROR) => 1,
619 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
620 Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
621 Some(tower_lsp_server::ls_types::DiagnosticSeverity::HINT) => 4,
622 None => 0,
623 _ => 5, };
625 severity_val.hash(&mut hasher);
626 if let Some(code) = &diag.code {
627 format!("{:?}", code).hash(&mut hasher);
628 }
629 if let Some(source) = &diag.source {
630 source.hash(&mut hasher);
631 }
632 if let Some(tags) = &diag.tags {
633 for tag in tags {
634 let tag_val = match *tag {
635 tower_lsp_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
636 tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
637 _ => 3,
638 };
639 tag_val.hash(&mut hasher);
640 }
641 }
642 }
643
644 format!("v1:{:x}", hasher.finish())
645}
646
647mod handlers;
648mod helpers;
649pub mod panic_guard;
650mod server;
651#[cfg(test)]
652mod tests;