1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6
7#[allow(unused_imports)]
8use self::helpers::*;
9
10use arc_swap::ArcSwap;
11use futures::future::Either;
12use tower::Service;
13use tower_lsp_server::jsonrpc::{Request as JsonRpcRequest, Response as JsonRpcResponse};
14
15enum IndexReadyNotification {}
18impl tower_lsp_server::ls_types::notification::Notification for IndexReadyNotification {
19 type Params = ();
20 const METHOD: &'static str = "$/php-lsp/indexReady";
21}
22
23enum ReferencesPartialResult {}
31#[derive(serde::Serialize, serde::Deserialize)]
32#[serde(rename_all = "camelCase")]
33struct ReferencesPartialResultParams {
34 token: tower_lsp_server::ls_types::NumberOrString,
35 value: Vec<tower_lsp_server::ls_types::Location>,
36}
37impl tower_lsp_server::ls_types::notification::Notification for ReferencesPartialResult {
38 type Params = ReferencesPartialResultParams;
39 const METHOD: &'static str = "$/progress";
40}
41
42pub(crate) async fn send_references_partial_result(
46 client: &Client,
47 token: tower_lsp_server::ls_types::NumberOrString,
48 locations: Vec<tower_lsp_server::ls_types::Location>,
49) {
50 client
51 .send_notification::<ReferencesPartialResult>(ReferencesPartialResultParams {
52 token,
53 value: locations,
54 })
55 .await;
56}
57
58use tower_lsp_server::Client;
59use tower_lsp_server::ls_types::*;
60
61#[derive(Debug, serde::Serialize, serde::Deserialize)]
63pub struct DebugStats {
64 pub parses: u64,
66 pub ref_index_locks: u64,
70 pub warm_sweeps_completed: u64,
73 pub workspace_symbol_index_ready: bool,
76 pub workspace_index_walks: u64,
80 pub reachability_scan_passes: u64,
87 pub analysis_compute_count: u64,
92 pub warm_start_replays_completed: u64,
97 pub debug_gate_held: Option<String>,
101 pub lifecycle_intents: u64,
104 pub workspace_file_count: u64,
107 pub token_cache_len: u64,
109 pub text_cache_len: u64,
112 pub parsed_cache_len: u64,
114 pub analysis_cache_len: u64,
116 pub owned_program_cache_len: u64,
118 pub mir_ref_query_cache_hits: u64,
121 pub mir_subtype_query_cache_hits: u64,
124 pub mir_mention_scans_recorded: u64,
130}
131
132#[cfg(feature = "test-hooks")]
134#[derive(Debug, serde::Serialize, serde::Deserialize)]
135pub struct DebugHoldGateParams {
136 pub section: String,
138}
139
140pub fn build_lsp_service() -> (LifecycleService, tower_lsp_server::ClientSocket) {
145 let builder = tower_lsp_server::LspService::build(Backend::new)
146 .custom_method("$/php-lsp/debugStats", Backend::debug_stats);
147 #[cfg(feature = "test-hooks")]
148 let builder = builder
149 .custom_method("$/php-lsp/debugHoldGate", Backend::debug_hold_gate)
150 .custom_method("$/php-lsp/debugReleaseGate", Backend::debug_release_gate);
151 let (service, socket) = builder.finish();
152 (LifecycleService { inner: service }, socket)
153}
154
155pub struct LifecycleService {
159 inner: tower_lsp_server::LspService<Backend>,
160}
161
162tokio::task_local! {
163 static LIFECYCLE_GENERATION: u64;
164}
165
166fn lifecycle_generation() -> u64 {
167 LIFECYCLE_GENERATION
168 .try_with(|generation| *generation)
169 .expect("document lifecycle handler called outside LifecycleService")
170}
171
172struct LifecycleIntentGuard {
173 open_files: OpenFiles,
174 uri: Uri,
175 generation: u64,
176}
177
178impl Drop for LifecycleIntentGuard {
179 fn drop(&mut self) {
180 self.open_files.finish_lifecycle_intent(&self.uri);
181 }
182}
183
184impl Service<JsonRpcRequest> for LifecycleService {
185 type Response = Option<JsonRpcResponse>;
186 type Error = tower_lsp_server::ExitedError;
187 type Future = Either<
188 Pin<
189 Box<
190 dyn Future<Output = Result<Option<JsonRpcResponse>, tower_lsp_server::ExitedError>>
191 + Send,
192 >,
193 >,
194 <tower_lsp_server::LspService<Backend> as Service<JsonRpcRequest>>::Future,
195 >;
196
197 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
198 self.inner.poll_ready(cx)
199 }
200
201 fn call(&mut self, request: JsonRpcRequest) -> Self::Future {
202 let lifecycle = self.inner.inner().note_lifecycle_intent(&request);
203 let future = self.inner.call(request);
204 match lifecycle {
205 Some(guard) => {
206 let generation = guard.generation;
207 Either::Left(Box::pin(LIFECYCLE_GENERATION.scope(
208 generation,
209 async move {
210 let _guard = guard;
211 future.await
212 },
213 )))
214 }
215 None => Either::Right(future),
216 }
217 }
218}
219
220use crate::document::ast::ParsedDoc;
221use crate::document::document_store::DocumentStore;
222use crate::document::open_files::OpenFiles;
223use crate::lang::autoload::Psr4Map;
224use crate::lang::config::LspConfig;
225use crate::laravel::LaravelIndex;
226
227use crate::analysis::diagnostics::merge_file_diagnostics;
228use crate::document::open_files::compute_open_file_diagnostics;
229
230pub struct Backend {
231 client: Client,
232 docs: Arc<DocumentStore>,
233 open_files: OpenFiles,
237 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
238 psr4: Arc<ArcSwap<Psr4Map>>,
239 laravel: Arc<ArcSwap<LaravelIndex>>,
240 config: Arc<ArcSwap<LspConfig>>,
241 client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
247 debug_gate: Arc<debug_gate::DebugGate>,
249}
250
251impl Backend {
252 pub fn new(client: Client) -> Self {
253 let docs = Arc::new(DocumentStore::new());
258 let psr4 = docs.psr4_arc();
259 Backend {
260 client,
261 docs,
262 open_files: OpenFiles::new(),
263 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
264 psr4,
265 laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
266 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
267 client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
268 debug_gate: Arc::new(debug_gate::DebugGate::new()),
269 }
270 }
271
272 fn note_lifecycle_intent(&self, request: &JsonRpcRequest) -> Option<LifecycleIntentGuard> {
275 let is_open = match request.method() {
276 "textDocument/didOpen" => true,
277 "textDocument/didClose" => false,
278 _ => return None,
279 };
280 let uri = request
281 .params()
282 .and_then(|params| serde_json::to_value(params).ok())
283 .and_then(|params| {
284 params
285 .pointer("/textDocument/uri")?
286 .as_str()
287 .map(str::to_owned)
288 })
289 .and_then(|uri| uri.parse::<Uri>().ok())?;
290 let generation = self.open_files.note_lifecycle_intent(uri.clone(), is_open);
291 Some(LifecycleIntentGuard {
292 open_files: self.open_files.clone(),
293 uri,
294 generation,
295 })
296 }
297
298 pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
302 Ok(DebugStats {
303 parses: self.docs.parse_count(),
304 ref_index_locks: self.docs.ref_index_lock_count(),
305 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
306 workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
307 workspace_index_walks: self.docs.workspace_index_walks(),
308 reachability_scan_passes: self.docs.reachability_scan_passes(),
309 analysis_compute_count: self.docs.analysis_compute_count(),
310 warm_start_replays_completed: self.docs.warm_start_replays_completed(),
311 debug_gate_held: self.debug_gate.held_section(),
312 lifecycle_intents: self.open_files.lifecycle_intent_count() as u64,
313 workspace_file_count: self.docs.workspace_file_count(),
314 token_cache_len: self.docs.token_cache_len(),
315 text_cache_len: self.docs.text_cache_len(),
316 parsed_cache_len: self.docs.parsed_cache_len(),
317 analysis_cache_len: self.docs.analysis_cache_len(),
318 owned_program_cache_len: self.docs.owned_program_cache_len(),
319 mir_ref_query_cache_hits: self.docs.mir_ref_query_cache_hits(),
320 mir_subtype_query_cache_hits: self.docs.mir_subtype_query_cache_hits(),
321 mir_mention_scans_recorded: self.docs.mir_mention_scans_recorded(),
322 })
323 }
324
325 #[cfg(feature = "test-hooks")]
328 pub async fn debug_hold_gate(
329 &self,
330 params: DebugHoldGateParams,
331 ) -> tower_lsp_server::jsonrpc::Result<()> {
332 self.debug_gate.arm(¶ms.section);
333 Ok(())
334 }
335
336 #[cfg(feature = "test-hooks")]
339 pub async fn debug_release_gate(&self) -> tower_lsp_server::jsonrpc::Result<()> {
340 self.debug_gate.release();
341 Ok(())
342 }
343
344 fn set_open_text(&self, uri: Uri, text: String) -> u64 {
345 self.open_files.set_open_text(&self.docs, uri, text)
346 }
347
348 fn close_open_file(&self, uri: &Uri) {
349 self.open_files.close(&self.docs, uri);
350 }
351
352 fn ingest_if_not_open(&self, uri: Uri, text: &str) {
356 if !self.open_files.contains(&uri) {
357 self.docs.ingest(uri, text);
358 }
359 }
360
361 fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
362 self.open_files.text(uri)
363 }
364
365 async fn blocking<F, R>(&self, label: &'static str, f: F) -> Option<R>
368 where
369 F: FnOnce() -> R + Send + 'static,
370 R: Send + 'static,
371 {
372 offload::run(label, f).await
373 }
374
375 async fn blocking_gated<F, R>(&self, section: &'static str, f: F) -> Option<R>
378 where
379 F: FnOnce() -> R + Send + 'static,
380 R: Send + 'static,
381 {
382 offload::run_gated(&self.debug_gate, section, f).await
383 }
384
385 fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
386 self.open_files.set_parse_diagnostics(uri, diagnostics);
387 }
388
389 fn all_open_files_with_diagnostic_versions(&self) -> Vec<(Uri, Option<i64>)> {
390 self.open_files.all_with_versions()
391 }
392
393 fn open_urls(&self) -> Vec<Uri> {
394 self.open_files.urls()
395 }
396
397 fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
398 self.open_files.get_doc(&self.docs, uri)
399 }
400
401 fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
402 self.open_files.get_doc_stale(&self.docs, uri)
403 }
404
405 pub(crate) fn remirror_open_files(&self) {
414 for uri in self.open_files.urls() {
415 if let Some(text) = self.open_files.text(&uri) {
416 self.docs.mirror_text_arc(&uri, text);
417 }
418 }
419 }
420
421 fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
425 self.docs
426 .get_doc_salsa(uri)
427 .map(|doc| doc.file_imports())
428 .unwrap_or_default()
429 }
430
431 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
434 let roots = self.root_paths.load();
435 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
436 }
437}
438
439fn resolve_reference_symbol(
448 doc_opt: Option<&Arc<ParsedDoc>>,
449 source: &str,
450 position: Position,
451 word: String,
452) -> (
453 String,
454 Option<crate::navigation::references::SymbolKind>,
455 Option<String>,
456) {
457 use crate::navigation::references::SymbolKind;
458 let mut constant_owner: Option<String> = None;
459 let (word, kind) = if let Some(doc) = doc_opt
460 && let Some(prop_name) =
461 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
462 {
463 (prop_name, Some(SymbolKind::Property))
464 } else if let Some(doc) = doc_opt {
465 let stmts = &doc.program().stmts;
466 if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
467 (prop_name, Some(SymbolKind::Property))
468 } else if let Some((const_name, owner)) =
469 cursor_is_on_constant_decl(doc.source(), stmts, position)
470 {
471 constant_owner = owner;
472 (const_name, Some(SymbolKind::Constant))
473 } else {
474 let k = symbol_kind_at(source, position, &word);
475 if matches!(k, Some(SymbolKind::Constant))
480 && let Some(raw) = class_before_double_colon(source, position)
481 {
482 constant_owner = Some(match raw.as_str() {
483 "self" | "static" => {
484 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
485 .unwrap_or(raw)
486 }
487 "parent" => {
494 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
495 .and_then(|child| {
496 crate::types::type_map::parent_class_name(doc, &child)
497 })
498 .unwrap_or(raw)
499 }
500 _ => raw,
501 });
502 }
503 (word, k)
504 }
505 } else {
506 let k = symbol_kind_at(source, position, &word);
507 (word, k)
508 };
509 (word, kind, constant_owner)
510}
511
512fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
520 let line = source.lines().nth(position.line as usize)?;
521 let chars: Vec<char> = line.chars().collect();
522 let col = position.character as usize;
523
524 let mut utf16_col = 0usize;
525 let mut char_idx = 0usize;
526 for ch in &chars {
527 if utf16_col >= col {
528 break;
529 }
530 utf16_col += ch.len_utf16();
531 char_idx += 1;
532 }
533
534 let is_word = |c: char| c.is_alphanumeric() || c == '_';
535 while char_idx > 0 && is_word(chars[char_idx - 1]) {
536 char_idx -= 1;
537 }
538
539 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
540 return None;
541 }
542
543 let class_end = char_idx - 2;
544 let mut class_start = class_end;
545 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
546 class_start -= 1;
547 }
548
549 let name: String = chars[class_start..class_end].iter().collect();
550 if name.is_empty() { None } else { Some(name) }
551}
552
553async fn compute_dependent_publishes_owned(
558 docs: Arc<DocumentStore>,
559 open_files: OpenFiles,
560 changed_uri: Uri,
561 diag_cfg: crate::lang::config::DiagnosticsConfig,
562 is_laravel: bool,
563) -> Vec<(Uri, Vec<Diagnostic>)> {
564 tokio::task::spawn_blocking(move || {
565 if !docs.is_index_ready() {
570 return Vec::new();
571 }
572
573 let open_set: Vec<std::sync::Arc<str>> = open_files
579 .urls()
580 .into_iter()
581 .filter(|u| u != &changed_uri)
582 .map(|u| std::sync::Arc::from(u.as_str()))
583 .collect();
584 if open_set.is_empty() {
585 return Vec::new();
586 }
587
588 let _interactive = docs.interactive_read_guard();
591 let cancel = docs.begin_reanalyze();
595 let Some(analyses) = docs.reanalyze_files(&open_set, &cancel) else {
599 return Vec::new();
600 };
601 if cancel.is_cancelled() || analyses.is_empty() {
602 return Vec::new();
603 }
604
605 let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
606 .into_iter()
607 .filter_map(|(file, analysis)| {
608 let url = (file.as_ref()).parse::<Uri>().ok()?;
609 Some((url, analysis))
610 })
611 .collect();
612 if dependents.is_empty() {
613 return Vec::new();
614 }
615
616 let dep_files: Vec<Arc<str>> = dependents
620 .iter()
621 .map(|(u, _)| Arc::from(u.as_str()))
622 .collect();
623 let Ok(class_issues) = docs.with_snapshot(
624 |session| session.prepare_for_query(None),
625 |snap| snap.class_issues(&dep_files),
626 ) else {
627 return Vec::new();
628 };
629 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
630 std::collections::HashMap::new();
631 for issue in class_issues {
632 if issue.suppressed {
633 continue;
634 }
635 let file = issue.location.file.clone();
636 class_issues_by_file.entry(file).or_default().push(issue);
637 }
638
639 let mut out: Vec<(Uri, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
640 for (url, analysis) in dependents {
641 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
642 let mut issues: Vec<mir_issues::Issue> = analysis
643 .issues
644 .into_iter()
645 .filter(|i| !i.suppressed)
646 .collect();
647 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
648 issues.extend(extra);
649 }
650 let semantic =
651 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
652 let mut diags = merge_file_diagnostics(parse, semantic);
653 diags.extend(crate::laravel::unguarded_model_diagnostics(
654 &docs, &url, is_laravel,
655 ));
656 diags.extend(open_files.external_diagnostics(&url));
657 out.push((url, diags));
658 }
659 out
660 })
661 .await
662 .unwrap_or_default()
663}
664
665pub(super) async fn republish_after_index_ready(
678 client: Client,
679 docs: Arc<DocumentStore>,
680 open_files: OpenFiles,
681 diag_cfg: crate::lang::config::DiagnosticsConfig,
682 is_laravel: bool,
683) {
684 let uris = open_files.urls();
685 if uris.is_empty() {
686 return;
687 }
688 let docs_ref = Arc::clone(&docs);
689 let open_files_ref = open_files.clone();
690 let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
691 uris.into_iter()
692 .map(|uri| {
693 let diags = compute_open_file_diagnostics(
694 &docs_ref,
695 &open_files_ref,
696 &uri,
697 &diag_cfg,
698 is_laravel,
699 );
700 (uri, diags)
701 })
702 .collect()
703 })
704 .await
705 .unwrap_or_default();
706 for (uri, diags) in all {
707 let hash = diagnostics_content_hash(&diags);
708 if open_files.published_hash(&uri) == Some(hash) {
709 continue;
710 }
711 open_files.note_published(&uri, hash);
712 client.publish_diagnostics(uri, diags, None).await;
713 }
714}
715
716pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
719 use std::collections::hash_map::DefaultHasher;
720 use std::hash::{Hash, Hasher};
721 let mut hasher = DefaultHasher::new();
722 serde_json::to_string(diagnostics)
723 .unwrap_or_default()
724 .hash(&mut hasher);
725 hasher.finish()
726}
727
728pub(super) enum DiagnosticPublish {
737 Open { version: i32, generation: u64 },
738 Other(Option<i32>),
739}
740
741pub(super) async fn publish_with_dependents(
742 client: Client,
743 docs: Arc<DocumentStore>,
744 open_files: OpenFiles,
745 uri: Uri,
746 publish: DiagnosticPublish,
747 diag_cfg: crate::lang::config::DiagnosticsConfig,
748 is_laravel: bool,
749) {
750 let (version, expected_open_generation) = match publish {
751 DiagnosticPublish::Open {
752 version,
753 generation,
754 } => (Some(version), Some(generation)),
755 DiagnosticPublish::Other(version) => (version, None),
756 };
757 let docs_ref = Arc::clone(&docs);
758 let open_files_ref = open_files.clone();
759 let uri_ref = uri.clone();
760 let diag_cfg_ref = diag_cfg.clone();
761 let all_diags = tokio::task::spawn_blocking(move || {
762 compute_open_file_diagnostics(
763 &docs_ref,
764 &open_files_ref,
765 &uri_ref,
766 &diag_cfg_ref,
767 is_laravel,
768 )
769 })
770 .await
771 .unwrap_or_default();
772 if expected_open_generation
773 .is_some_and(|generation| !open_files.should_apply_lifecycle(&uri, generation, true))
774 {
775 return;
776 }
777 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
778 client
779 .publish_diagnostics(uri.clone(), all_diags, version)
780 .await;
781 let dependents =
782 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg, is_laravel)
783 .await;
784 for (dep_uri, dep_diags) in dependents {
785 let hash = diagnostics_content_hash(&dep_diags);
786 if open_files.published_hash(&dep_uri) == Some(hash) {
787 continue;
788 }
789 open_files.note_published(&dep_uri, hash);
790 client.publish_diagnostics(dep_uri, dep_diags, None).await;
791 }
792}
793
794fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
797 use std::collections::hash_map::DefaultHasher;
798 use std::hash::{Hash, Hasher};
799
800 let mut hasher = DefaultHasher::new();
801 uri.hash(&mut hasher);
802 diagnostics.len().hash(&mut hasher);
803
804 for diag in diagnostics {
805 diag.range.start.line.hash(&mut hasher);
806 diag.range.start.character.hash(&mut hasher);
807 diag.range.end.line.hash(&mut hasher);
808 diag.range.end.character.hash(&mut hasher);
809 diag.message.hash(&mut hasher);
810 let severity_val = match diag.severity {
811 Some(tower_lsp_server::ls_types::DiagnosticSeverity::ERROR) => 1,
812 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
813 Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
814 Some(tower_lsp_server::ls_types::DiagnosticSeverity::HINT) => 4,
815 None => 0,
816 _ => 5, };
818 severity_val.hash(&mut hasher);
819 if let Some(code) = &diag.code {
820 format!("{:?}", code).hash(&mut hasher);
821 }
822 if let Some(source) = &diag.source {
823 source.hash(&mut hasher);
824 }
825 if let Some(tags) = &diag.tags {
826 for tag in tags {
827 let tag_val = match *tag {
828 tower_lsp_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
829 tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
830 _ => 3,
831 };
832 tag_val.hash(&mut hasher);
833 }
834 }
835 }
836
837 format!("v1:{:x}", hasher.finish())
838}
839
840pub mod debug_gate;
841mod handlers;
842mod helpers;
843pub mod offload;
844pub mod panic_guard;
845mod server;
846#[cfg(test)]
847mod tests;