1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7use arc_swap::ArcSwap;
8use tower::Service;
9use tower_lsp_server::jsonrpc::{Request as JsonRpcRequest, Response as JsonRpcResponse};
10
11use self::helpers::{
12 cursor_is_on_constant_decl, cursor_is_on_property_decl, promoted_property_at_cursor,
13 symbol_kind_at,
14};
15
16enum IndexReadyNotification {}
19impl tower_lsp_server::ls_types::notification::Notification for IndexReadyNotification {
20 type Params = ();
21 const METHOD: &'static str = "$/php-lsp/indexReady";
22}
23
24enum ReferencesPartialResult {}
32#[derive(serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "camelCase")]
34struct ReferencesPartialResultParams {
35 token: tower_lsp_server::ls_types::NumberOrString,
36 value: Vec<tower_lsp_server::ls_types::Location>,
37}
38impl tower_lsp_server::ls_types::notification::Notification for ReferencesPartialResult {
39 type Params = ReferencesPartialResultParams;
40 const METHOD: &'static str = "$/progress";
41}
42
43pub(crate) async fn send_references_partial_result(
47 client: &Client,
48 token: tower_lsp_server::ls_types::NumberOrString,
49 locations: Vec<tower_lsp_server::ls_types::Location>,
50) {
51 client
52 .send_notification::<ReferencesPartialResult>(ReferencesPartialResultParams {
53 token,
54 value: locations,
55 })
56 .await;
57}
58
59use tower_lsp_server::Client;
60use tower_lsp_server::ls_types::*;
61
62#[derive(Debug, serde::Serialize, serde::Deserialize)]
64pub struct DebugStats {
65 pub parses: u64,
67 pub ref_index_locks: u64,
71 pub warm_sweeps_completed: u64,
74 pub workspace_symbol_index_ready: bool,
77 pub workspace_index_walks: u64,
81 pub reachability_scan_passes: u64,
88 pub analysis_compute_count: u64,
93 pub warm_start_replays_completed: u64,
98 pub debug_gate_held: Option<String>,
102 pub lifecycle_intents: u64,
105 pub workspace_file_count: u64,
108 pub token_cache_len: u64,
110 pub text_cache_len: u64,
113 pub parsed_cache_len: u64,
115 pub analysis_cache_len: u64,
117 pub owned_program_cache_len: u64,
119 pub mir_ref_query_cache_hits: u64,
122 pub mir_subtype_query_cache_hits: u64,
125 pub mir_mention_scans_recorded: u64,
131}
132
133#[cfg(feature = "test-hooks")]
135#[derive(Debug, serde::Serialize, serde::Deserialize)]
136pub struct DebugHoldGateParams {
137 pub section: String,
139}
140
141pub fn build_lsp_service() -> (LifecycleService, ClientLoopback) {
146 let builder = tower_lsp_server::LspService::build(Backend::new)
147 .custom_method("$/php-lsp/debugStats", Backend::debug_stats);
148 #[cfg(feature = "test-hooks")]
149 let builder = builder
150 .custom_method("$/php-lsp/debugHoldGate", Backend::debug_hold_gate)
151 .custom_method("$/php-lsp/debugReleaseGate", Backend::debug_release_gate);
152 let (service, socket) = builder.finish();
153 let spellings = drive_letter::ClientSpellings::default();
154 (
155 LifecycleService {
156 inner: service,
157 spellings: spellings.clone(),
158 },
159 ClientLoopback { socket, spellings },
160 )
161}
162
163pub struct LifecycleService {
167 inner: tower_lsp_server::LspService<Backend>,
168 spellings: drive_letter::ClientSpellings,
169}
170
171tokio::task_local! {
172 static LIFECYCLE_GENERATION: u64;
173}
174
175fn lifecycle_generation() -> u64 {
176 LIFECYCLE_GENERATION
177 .try_with(|generation| *generation)
178 .expect("document lifecycle handler called outside LifecycleService")
179}
180
181struct LifecycleIntentGuard {
182 open_files: OpenFiles,
183 uri: Uri,
184 generation: u64,
185}
186
187impl Drop for LifecycleIntentGuard {
188 fn drop(&mut self) {
189 self.open_files.finish_lifecycle_intent(&self.uri);
190 }
191}
192
193impl Service<JsonRpcRequest> for LifecycleService {
194 type Response = Option<JsonRpcResponse>;
195 type Error = tower_lsp_server::ExitedError;
196 type Future =
197 Pin<Box<dyn Future<Output = Result<Option<JsonRpcResponse>, Self::Error>> + Send>>;
198
199 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
200 self.inner.poll_ready(cx)
201 }
202
203 fn call(&mut self, request: JsonRpcRequest) -> Self::Future {
204 let request = self.spellings.normalize_request(request);
205 let lifecycle = self.inner.inner().note_lifecycle_intent(&request);
206 let future = self.inner.call(request);
207 let spellings = self.spellings.clone();
208 let future = async move {
209 let response = future.await?;
210 Ok(response.map(|response| spellings.restore_response(response)))
211 };
212 match lifecycle {
213 Some(guard) => {
214 let generation = guard.generation;
215 Box::pin(LIFECYCLE_GENERATION.scope(generation, async move {
216 let _guard = guard;
217 future.await
218 }))
219 }
220 None => Box::pin(future),
221 }
222 }
223}
224
225use crate::document::ast::ParsedDoc;
226use crate::document::document_store::DocumentStore;
227use crate::document::open_files::OpenFiles;
228use crate::lang::autoload::Psr4Map;
229use crate::lang::config::LspConfig;
230use crate::laravel::LaravelIndex;
231
232use crate::analysis::diagnostics::merge_file_diagnostics;
233use crate::document::open_files::{compute_open_file_diagnostics, is_diagnostics_excluded};
234
235pub struct Backend {
236 client: Client,
237 docs: Arc<DocumentStore>,
238 open_files: OpenFiles,
242 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
243 psr4: Arc<ArcSwap<Psr4Map>>,
244 laravel: Arc<ArcSwap<LaravelIndex>>,
245 config: Arc<ArcSwap<LspConfig>>,
246 client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
252 code_lens_scopes: Arc<crate::analysis::code_lens::CodeLensScopeCache>,
253 external_runs: crate::analysis::external::RunRegistry,
254 debug_gate: Arc<debug_gate::DebugGate>,
256}
257
258impl Backend {
259 pub fn new(client: Client) -> Self {
260 let docs = Arc::new(DocumentStore::new());
265 let psr4 = docs.psr4_arc();
266 Backend {
267 client,
268 docs,
269 open_files: OpenFiles::new(),
270 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
271 psr4,
272 laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
273 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
274 client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
275 code_lens_scopes: Arc::default(),
276 external_runs: Default::default(),
277 debug_gate: Arc::new(debug_gate::DebugGate::new()),
278 }
279 }
280
281 fn note_lifecycle_intent(&self, request: &JsonRpcRequest) -> Option<LifecycleIntentGuard> {
284 let is_open = match request.method() {
285 "textDocument/didOpen" => true,
286 "textDocument/didClose" => false,
287 _ => return None,
288 };
289 let uri = request
290 .params()
291 .and_then(|params| serde_json::to_value(params).ok())
292 .and_then(|params| {
293 params
294 .pointer("/textDocument/uri")?
295 .as_str()
296 .map(str::to_owned)
297 })
298 .and_then(|uri| uri.parse::<Uri>().ok())?;
299 let generation = self.open_files.note_lifecycle_intent(uri.clone(), is_open);
300 Some(LifecycleIntentGuard {
301 open_files: self.open_files.clone(),
302 uri,
303 generation,
304 })
305 }
306
307 pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
311 Ok(DebugStats {
312 parses: self.docs.parse_count(),
313 ref_index_locks: self.docs.ref_index_lock_count(),
314 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
315 workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
316 workspace_index_walks: self.docs.workspace_index_walks(),
317 reachability_scan_passes: self.docs.reachability_scan_passes(),
318 analysis_compute_count: self.docs.analysis_compute_count(),
319 warm_start_replays_completed: self.docs.warm_start_replays_completed(),
320 debug_gate_held: self.debug_gate.held_section(),
321 lifecycle_intents: self.open_files.lifecycle_intent_count() as u64,
322 workspace_file_count: self.docs.workspace_file_count(),
323 token_cache_len: self.docs.token_cache_len(),
324 text_cache_len: self.docs.text_cache_len(),
325 parsed_cache_len: self.docs.parsed_cache_len(),
326 analysis_cache_len: self.docs.analysis_cache_len(),
327 owned_program_cache_len: self.docs.owned_program_cache_len(),
328 mir_ref_query_cache_hits: self.docs.mir_ref_query_cache_hits(),
329 mir_subtype_query_cache_hits: self.docs.mir_subtype_query_cache_hits(),
330 mir_mention_scans_recorded: self.docs.mir_mention_scans_recorded(),
331 })
332 }
333
334 #[cfg(feature = "test-hooks")]
337 pub async fn debug_hold_gate(
338 &self,
339 params: DebugHoldGateParams,
340 ) -> tower_lsp_server::jsonrpc::Result<()> {
341 self.debug_gate.arm(¶ms.section);
342 Ok(())
343 }
344
345 #[cfg(feature = "test-hooks")]
348 pub async fn debug_release_gate(&self) -> tower_lsp_server::jsonrpc::Result<()> {
349 self.debug_gate.release();
350 Ok(())
351 }
352
353 fn set_open_text(&self, uri: Uri, text: String) -> u64 {
354 self.open_files.set_open_text(&self.docs, uri, text)
355 }
356
357 fn close_open_file(&self, uri: &Uri) {
358 self.open_files.close(&self.docs, uri);
359 }
360
361 fn ingest_if_not_open(&self, uri: Uri, text: &str) {
365 if !self.open_files.contains(&uri) {
366 self.docs.ingest(uri, text);
367 }
368 }
369
370 fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
371 self.open_files.text(uri)
372 }
373
374 async fn blocking<F, R>(&self, label: &'static str, f: F) -> Option<R>
377 where
378 F: FnOnce() -> R + Send + 'static,
379 R: Send + 'static,
380 {
381 offload::run(label, f).await
382 }
383
384 async fn blocking_gated<F, R>(&self, section: &'static str, f: F) -> Option<R>
387 where
388 F: FnOnce() -> R + Send + 'static,
389 R: Send + 'static,
390 {
391 offload::run_gated(&self.debug_gate, section, f).await
392 }
393
394 fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
395 self.open_files.set_parse_diagnostics(uri, diagnostics);
396 }
397
398 fn all_open_files_with_diagnostic_versions(&self) -> Vec<(Uri, Option<i64>)> {
399 self.open_files.all_with_versions()
400 }
401
402 fn open_urls(&self) -> Vec<Uri> {
403 self.open_files.urls()
404 }
405
406 fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
407 self.open_files.get_doc(&self.docs, uri)
408 }
409
410 fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
411 self.open_files.get_doc_stale(&self.docs, uri)
412 }
413
414 pub(crate) fn remirror_open_files(&self) {
423 for uri in self.open_files.urls() {
424 if let Some(text) = self.open_files.text(&uri) {
425 self.docs.mirror_text_arc(&uri, text);
426 }
427 }
428 }
429
430 fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
434 self.docs
435 .get_doc_salsa(uri)
436 .map(|doc| doc.file_imports())
437 .unwrap_or_default()
438 }
439
440 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
443 let roots = self.root_paths.load();
444 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
445 }
446}
447
448fn resolve_reference_symbol(
457 doc_opt: Option<&Arc<ParsedDoc>>,
458 source: &str,
459 position: Position,
460 word: String,
461) -> (
462 String,
463 Option<crate::navigation::references::SymbolKind>,
464 Option<String>,
465) {
466 use crate::navigation::references::SymbolKind;
467 let mut constant_owner: Option<String> = None;
468 let (word, kind) = if let Some(doc) = doc_opt
469 && let Some(prop_name) =
470 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
471 {
472 (prop_name, Some(SymbolKind::Property))
473 } else if let Some(doc) = doc_opt {
474 let stmts = &doc.program().stmts;
475 if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
476 (prop_name, Some(SymbolKind::Property))
477 } else if let Some((const_name, owner)) =
478 cursor_is_on_constant_decl(doc.source(), stmts, position)
479 {
480 constant_owner = owner;
481 (const_name, Some(SymbolKind::Constant))
482 } else {
483 let k = symbol_kind_at(source, position, &word);
484 if matches!(k, Some(SymbolKind::Constant))
489 && let Some(raw) = class_before_double_colon(source, position)
490 {
491 constant_owner = Some(match raw.as_str() {
492 "self" | "static" => {
493 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
494 .unwrap_or(raw)
495 }
496 "parent" => {
503 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
504 .and_then(|child| {
505 crate::types::type_map::parent_class_name(doc, &child)
506 })
507 .unwrap_or(raw)
508 }
509 _ => raw,
510 });
511 }
512 (word, k)
513 }
514 } else {
515 let k = symbol_kind_at(source, position, &word);
516 (word, k)
517 };
518 (word, kind, constant_owner)
519}
520
521fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
529 let line = source.lines().nth(position.line as usize)?;
530 let chars: Vec<char> = line.chars().collect();
531 let col = position.character as usize;
532
533 let mut utf16_col = 0usize;
534 let mut char_idx = 0usize;
535 for ch in &chars {
536 if utf16_col >= col {
537 break;
538 }
539 utf16_col += ch.len_utf16();
540 char_idx += 1;
541 }
542
543 let is_word = |c: char| c.is_alphanumeric() || c == '_';
544 while char_idx > 0 && is_word(chars[char_idx - 1]) {
545 char_idx -= 1;
546 }
547
548 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
549 return None;
550 }
551
552 let class_end = char_idx - 2;
553 let mut class_start = class_end;
554 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
555 class_start -= 1;
556 }
557
558 let name: String = chars[class_start..class_end].iter().collect();
559 if name.is_empty() { None } else { Some(name) }
560}
561
562async fn compute_dependent_publishes_owned(
567 docs: Arc<DocumentStore>,
568 open_files: OpenFiles,
569 changed_uri: Uri,
570 diag_cfg: crate::lang::config::DiagnosticsConfig,
571 is_laravel: bool,
572) -> Vec<(Uri, Vec<Diagnostic>)> {
573 tokio::task::spawn_blocking(move || {
574 if !docs.is_index_ready() {
579 return Vec::new();
580 }
581
582 let open_set: Vec<std::sync::Arc<str>> = open_files
588 .urls()
589 .into_iter()
590 .filter(|u| u != &changed_uri && !is_diagnostics_excluded(u, &diag_cfg))
591 .map(|u| std::sync::Arc::from(u.as_str()))
592 .collect();
593 if open_set.is_empty() {
594 return Vec::new();
595 }
596
597 let _interactive = docs.interactive_read_guard();
600 let cancel = docs.begin_reanalyze();
604 let Some(analyses) = docs.reanalyze_files(&open_set, &cancel) else {
608 return Vec::new();
609 };
610 if cancel.is_cancelled() || analyses.is_empty() {
611 return Vec::new();
612 }
613
614 let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
615 .into_iter()
616 .filter_map(|(file, analysis)| {
617 let url = (file.as_ref()).parse::<Uri>().ok()?;
618 Some((url, analysis))
619 })
620 .collect();
621 if dependents.is_empty() {
622 return Vec::new();
623 }
624
625 let dep_files: Vec<Arc<str>> = dependents
629 .iter()
630 .map(|(u, _)| Arc::from(u.as_str()))
631 .collect();
632 let Ok(class_issues) = docs.with_snapshot(
633 |session| session.prepare_for_query(None),
634 |snap| snap.class_issues(&dep_files),
635 ) else {
636 return Vec::new();
637 };
638 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
639 std::collections::HashMap::new();
640 for issue in class_issues {
641 if issue.suppressed {
642 continue;
643 }
644 let file = issue.location.file.clone();
645 class_issues_by_file.entry(file).or_default().push(issue);
646 }
647
648 let mut out: Vec<(Uri, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
649 for (url, analysis) in dependents {
650 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
651 let mut issues: Vec<mir_issues::Issue> = analysis
652 .issues
653 .into_iter()
654 .filter(|i| !i.suppressed)
655 .collect();
656 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
657 issues.extend(extra);
658 }
659 let semantic =
660 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
661 let mut diags = merge_file_diagnostics(parse, semantic);
662 diags.extend(crate::laravel::unguarded_model_diagnostics(
663 &docs, &url, is_laravel,
664 ));
665 diags.extend(open_files.external_diagnostics(&url));
666 out.push((url, diags));
667 }
668 out
669 })
670 .await
671 .unwrap_or_default()
672}
673
674pub(super) async fn republish_after_index_ready(
687 client: Client,
688 docs: Arc<DocumentStore>,
689 open_files: OpenFiles,
690 diag_cfg: crate::lang::config::DiagnosticsConfig,
691 is_laravel: bool,
692) {
693 let uris = open_files.urls();
694 if uris.is_empty() {
695 return;
696 }
697 let docs_ref = Arc::clone(&docs);
698 let open_files_ref = open_files.clone();
699 let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
700 uris.into_iter()
701 .map(|uri| {
702 let diags = compute_open_file_diagnostics(
703 &docs_ref,
704 &open_files_ref,
705 &uri,
706 &diag_cfg,
707 is_laravel,
708 );
709 (uri, diags)
710 })
711 .collect()
712 })
713 .await
714 .unwrap_or_default();
715 for (uri, diags) in all {
716 let hash = diagnostics_content_hash(&diags);
717 if open_files.published_hash(&uri) == Some(hash) {
718 continue;
719 }
720 open_files.note_published(&uri, hash);
721 client.publish_diagnostics(uri, diags, None).await;
722 }
723}
724
725pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
728 use std::collections::hash_map::DefaultHasher;
729 use std::hash::{Hash, Hasher};
730 let mut hasher = DefaultHasher::new();
731 serde_json::to_string(diagnostics)
732 .unwrap_or_default()
733 .hash(&mut hasher);
734 hasher.finish()
735}
736
737pub(super) enum DiagnosticPublish {
746 Open { version: i32, generation: u64 },
747 Other(Option<i32>),
748}
749
750pub(super) async fn publish_with_dependents(
751 client: Client,
752 docs: Arc<DocumentStore>,
753 open_files: OpenFiles,
754 uri: Uri,
755 publish: DiagnosticPublish,
756 diag_cfg: crate::lang::config::DiagnosticsConfig,
757 is_laravel: bool,
758) {
759 let (version, expected_open_generation) = match publish {
760 DiagnosticPublish::Open {
761 version,
762 generation,
763 } => (Some(version), Some(generation)),
764 DiagnosticPublish::Other(version) => (version, None),
765 };
766 let docs_ref = Arc::clone(&docs);
767 let open_files_ref = open_files.clone();
768 let uri_ref = uri.clone();
769 let diag_cfg_ref = diag_cfg.clone();
770 let all_diags = tokio::task::spawn_blocking(move || {
771 compute_open_file_diagnostics(
772 &docs_ref,
773 &open_files_ref,
774 &uri_ref,
775 &diag_cfg_ref,
776 is_laravel,
777 )
778 })
779 .await
780 .unwrap_or_default();
781 if expected_open_generation
782 .is_some_and(|generation| !open_files.should_apply_lifecycle(&uri, generation, true))
783 {
784 return;
785 }
786 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
787 client
788 .publish_diagnostics(uri.clone(), all_diags, version)
789 .await;
790 let dependents =
791 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg, is_laravel)
792 .await;
793 for (dep_uri, dep_diags) in dependents {
794 let hash = diagnostics_content_hash(&dep_diags);
795 if open_files.published_hash(&dep_uri) == Some(hash) {
796 continue;
797 }
798 open_files.note_published(&dep_uri, hash);
799 client.publish_diagnostics(dep_uri, dep_diags, None).await;
800 }
801}
802
803fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
806 use std::collections::hash_map::DefaultHasher;
807 use std::hash::{Hash, Hasher};
808
809 let mut hasher = DefaultHasher::new();
810 uri.hash(&mut hasher);
811 diagnostics.len().hash(&mut hasher);
812
813 for diag in diagnostics {
814 diag.range.start.line.hash(&mut hasher);
815 diag.range.start.character.hash(&mut hasher);
816 diag.range.end.line.hash(&mut hasher);
817 diag.range.end.character.hash(&mut hasher);
818 diag.message.hash(&mut hasher);
819 let severity_val = match diag.severity {
820 Some(tower_lsp_server::ls_types::DiagnosticSeverity::ERROR) => 1,
821 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
822 Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
823 Some(tower_lsp_server::ls_types::DiagnosticSeverity::HINT) => 4,
824 None => 0,
825 _ => 5, };
827 severity_val.hash(&mut hasher);
828 if let Some(code) = &diag.code {
829 format!("{:?}", code).hash(&mut hasher);
830 }
831 if let Some(source) = &diag.source {
832 source.hash(&mut hasher);
833 }
834 if let Some(tags) = &diag.tags {
835 for tag in tags {
836 let tag_val = match *tag {
837 tower_lsp_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
838 tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
839 _ => 3,
840 };
841 tag_val.hash(&mut hasher);
842 }
843 }
844 }
845
846 format!("v1:{:x}", hasher.finish())
847}
848
849pub mod debug_gate;
850mod drive_letter;
851pub use drive_letter::ClientLoopback;
852mod handlers;
853mod helpers;
854pub mod offload;
855pub mod panic_guard;
856mod server;
857#[cfg(test)]
858mod tests;