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 pub warm_start_untrusted_reanalyzed: u64,
95 pub vendor_warm_sweeps_completed: u64,
99 pub debug_gate_held: Option<String>,
103 pub workspace_file_count: u64,
106 pub token_cache_len: u64,
108 pub text_cache_len: u64,
111 pub parsed_cache_len: u64,
113 pub analysis_cache_len: u64,
115 pub owned_program_cache_len: u64,
117 pub decl_fingerprints_len: u64,
120 pub vendor_index_cache_len: u64,
124 pub mir_ref_query_cache_hits: u64,
127 pub mir_subtype_query_cache_hits: u64,
130 pub mir_mention_scans_recorded: u64,
136}
137
138#[cfg(feature = "test-hooks")]
140#[derive(Debug, serde::Serialize, serde::Deserialize)]
141pub struct DebugHoldGateParams {
142 pub section: String,
144}
145
146pub fn build_lsp_service() -> (
151 tower_lsp_server::LspService<Backend>,
152 tower_lsp_server::ClientSocket,
153) {
154 let builder = tower_lsp_server::LspService::build(Backend::new)
155 .custom_method("$/php-lsp/debugStats", Backend::debug_stats);
156 #[cfg(feature = "test-hooks")]
157 let builder = builder
158 .custom_method("$/php-lsp/debugHoldGate", Backend::debug_hold_gate)
159 .custom_method("$/php-lsp/debugReleaseGate", Backend::debug_release_gate);
160 builder.finish()
161}
162
163use crate::document::ast::ParsedDoc;
164use crate::document::document_store::DocumentStore;
165use crate::document::open_files::OpenFiles;
166use crate::lang::autoload::Psr4Map;
167use crate::lang::config::LspConfig;
168use crate::laravel::LaravelIndex;
169
170use crate::analysis::diagnostics::merge_file_diagnostics;
171use crate::document::open_files::compute_open_file_diagnostics;
172
173pub struct Backend {
174 client: Client,
175 docs: Arc<DocumentStore>,
176 open_files: OpenFiles,
180 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
181 psr4: Arc<ArcSwap<Psr4Map>>,
182 laravel: Arc<ArcSwap<LaravelIndex>>,
183 config: Arc<ArcSwap<LspConfig>>,
184 client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
190 debug_gate: Arc<debug_gate::DebugGate>,
192}
193
194impl Backend {
195 pub fn new(client: Client) -> Self {
196 let docs = Arc::new(DocumentStore::new());
201 let psr4 = docs.psr4_arc();
202 Backend {
203 client,
204 docs,
205 open_files: OpenFiles::new(),
206 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
207 psr4,
208 laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
209 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
210 client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
211 debug_gate: Arc::new(debug_gate::DebugGate::new()),
212 }
213 }
214
215 pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
219 Ok(DebugStats {
220 parses: self.docs.parse_count(),
221 ref_index_locks: self.docs.ref_index_lock_count(),
222 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
223 workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
224 workspace_index_walks: self.docs.workspace_index_walks(),
225 reachability_scan_passes: self.docs.reachability_scan_passes(),
226 analysis_compute_count: self.docs.analysis_compute_count(),
227 warm_start_replays_completed: self.docs.warm_start_replays_completed(),
228 warm_start_untrusted_reanalyzed: self.docs.warm_start_untrusted_reanalyzed(),
229 vendor_warm_sweeps_completed: self.docs.vendor_warm_sweeps_completed(),
230 debug_gate_held: self.debug_gate.held_section(),
231 workspace_file_count: self.docs.workspace_file_count(),
232 token_cache_len: self.docs.token_cache_len(),
233 text_cache_len: self.docs.text_cache_len(),
234 parsed_cache_len: self.docs.parsed_cache_len(),
235 analysis_cache_len: self.docs.analysis_cache_len(),
236 owned_program_cache_len: self.docs.owned_program_cache_len(),
237 decl_fingerprints_len: self.docs.decl_fingerprints_len(),
238 vendor_index_cache_len: self.docs.vendor_index_cache_len(),
239 mir_ref_query_cache_hits: self.docs.mir_ref_query_cache_hits(),
240 mir_subtype_query_cache_hits: self.docs.mir_subtype_query_cache_hits(),
241 mir_mention_scans_recorded: self.docs.mir_mention_scans_recorded(),
242 })
243 }
244
245 #[cfg(feature = "test-hooks")]
248 pub async fn debug_hold_gate(
249 &self,
250 params: DebugHoldGateParams,
251 ) -> tower_lsp_server::jsonrpc::Result<()> {
252 self.debug_gate.arm(¶ms.section);
253 Ok(())
254 }
255
256 #[cfg(feature = "test-hooks")]
259 pub async fn debug_release_gate(&self) -> tower_lsp_server::jsonrpc::Result<()> {
260 self.debug_gate.release();
261 Ok(())
262 }
263
264 fn set_open_text(&self, uri: Uri, text: String) -> u64 {
265 self.open_files.set_open_text(&self.docs, uri, text)
266 }
267
268 fn close_open_file(&self, uri: &Uri) {
269 self.open_files.close(&self.docs, uri);
270 }
271
272 fn ingest_if_not_open(&self, uri: Uri, text: &str) {
276 if !self.open_files.contains(&uri) {
277 self.docs.ingest(uri, text);
278 }
279 }
280
281 fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
282 self.open_files.text(uri)
283 }
284
285 async fn blocking<F, R>(&self, label: &'static str, f: F) -> Option<R>
288 where
289 F: FnOnce() -> R + Send + 'static,
290 R: Send + 'static,
291 {
292 offload::run(label, f).await
293 }
294
295 async fn blocking_gated<F, R>(&self, section: &'static str, f: F) -> Option<R>
298 where
299 F: FnOnce() -> R + Send + 'static,
300 R: Send + 'static,
301 {
302 offload::run_gated(&self.debug_gate, section, f).await
303 }
304
305 fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
306 self.open_files.set_parse_diagnostics(uri, diagnostics);
307 }
308
309 fn get_parse_diagnostics(&self, uri: &Uri) -> Option<Vec<Diagnostic>> {
310 self.open_files.parse_diagnostics(uri)
311 }
312
313 fn all_open_files_with_diagnostics(&self) -> Vec<(Uri, Vec<Diagnostic>, Option<i64>)> {
314 self.open_files.all_with_diagnostics()
315 }
316
317 fn open_urls(&self) -> Vec<Uri> {
318 self.open_files.urls()
319 }
320
321 fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
322 self.open_files.get_doc(&self.docs, uri)
323 }
324
325 fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
326 self.open_files.get_doc_stale(&self.docs, uri)
327 }
328
329 pub(crate) fn remirror_open_files(&self) {
338 for uri in self.open_files.urls() {
339 if let Some(text) = self.open_files.text(&uri) {
340 self.docs.mirror_text_arc(&uri, text);
341 }
342 }
343 }
344
345 fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
349 self.docs
350 .get_doc_salsa(uri)
351 .map(|doc| doc.file_imports())
352 .unwrap_or_default()
353 }
354
355 fn resolve_reference_target_fqn(
361 &self,
362 uri: &Uri,
363 doc_opt: Option<&Arc<ParsedDoc>>,
364 word: &str,
365 kind: Option<crate::navigation::references::SymbolKind>,
366 position: Position,
367 constant_owner: Option<String>,
368 ) -> Option<String> {
369 use crate::navigation::references::SymbolKind;
370 let doc = doc_opt?;
371 let imports = self.file_imports(uri);
372 match kind {
373 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
374 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
375 resolved.contains('\\').then_some(resolved)
376 }
377 Some(SymbolKind::Method) => {
378 let short_owner =
380 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
381 Some(crate::navigation::moniker::resolve_fqn(
383 doc,
384 &short_owner,
385 &imports,
386 ))
387 }
388 Some(SymbolKind::Property) => {
389 let stmts = &doc.program().stmts;
394 if crate::backend::helpers::cursor_is_on_property_decl(
395 doc.source(),
396 stmts,
397 position,
398 )
399 .is_none()
400 && promoted_property_at_cursor(doc.source(), stmts, position).is_none()
401 {
402 return None;
403 }
404 let short_owner =
405 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
406 Some(crate::navigation::moniker::resolve_fqn(
407 doc,
408 &short_owner,
409 &imports,
410 ))
411 }
412 Some(SymbolKind::Constant) => {
413 if let Some(owner) = constant_owner {
414 if owner.contains('\\') {
417 Some(owner.trim_start_matches('\\').to_string())
418 } else {
419 Some(crate::navigation::moniker::resolve_fqn(
420 doc, &owner, &imports,
421 ))
422 }
423 } else {
424 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
427 fqn.contains('\\').then_some(fqn)
428 }
429 }
430 _ => None,
431 }
432 }
433
434 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
437 let roots = self.root_paths.load();
438 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
439 }
440}
441
442fn resolve_reference_symbol(
451 doc_opt: Option<&Arc<ParsedDoc>>,
452 source: &str,
453 position: Position,
454 word: String,
455) -> (
456 String,
457 Option<crate::navigation::references::SymbolKind>,
458 Option<String>,
459) {
460 use crate::navigation::references::SymbolKind;
461 let mut constant_owner: Option<String> = None;
462 let (word, kind) = if let Some(doc) = doc_opt
463 && let Some(prop_name) =
464 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
465 {
466 (prop_name, Some(SymbolKind::Property))
467 } else if let Some(doc) = doc_opt {
468 let stmts = &doc.program().stmts;
469 if cursor_is_on_method_decl(doc.source(), stmts, position) {
470 (word, Some(SymbolKind::Method))
471 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
472 (prop_name, Some(SymbolKind::Property))
473 } else if let Some((const_name, owner)) =
474 cursor_is_on_constant_decl(doc.source(), stmts, position)
475 {
476 constant_owner = owner;
477 (const_name, Some(SymbolKind::Constant))
478 } else {
479 let k = symbol_kind_at(source, position, &word);
480 if matches!(k, Some(SymbolKind::Constant))
485 && let Some(raw) = class_before_double_colon(source, position)
486 {
487 constant_owner = Some(match raw.as_str() {
488 "self" | "static" => {
489 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
490 .unwrap_or(raw)
491 }
492 "parent" => {
499 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
500 .and_then(|child| {
501 crate::types::type_map::parent_class_name(doc, &child)
502 })
503 .unwrap_or(raw)
504 }
505 _ => raw,
506 });
507 }
508 (word, k)
509 }
510 } else {
511 let k = symbol_kind_at(source, position, &word);
512 (word, k)
513 };
514 (word, kind, constant_owner)
515}
516
517fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
525 let line = source.lines().nth(position.line as usize)?;
526 let chars: Vec<char> = line.chars().collect();
527 let col = position.character as usize;
528
529 let mut utf16_col = 0usize;
530 let mut char_idx = 0usize;
531 for ch in &chars {
532 if utf16_col >= col {
533 break;
534 }
535 utf16_col += ch.len_utf16();
536 char_idx += 1;
537 }
538
539 let is_word = |c: char| c.is_alphanumeric() || c == '_';
540 while char_idx > 0 && is_word(chars[char_idx - 1]) {
541 char_idx -= 1;
542 }
543
544 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
545 return None;
546 }
547
548 let class_end = char_idx - 2;
549 let mut class_start = class_end;
550 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
551 class_start -= 1;
552 }
553
554 let name: String = chars[class_start..class_end].iter().collect();
555 if name.is_empty() { None } else { Some(name) }
556}
557
558async fn compute_dependent_publishes_owned(
563 docs: Arc<DocumentStore>,
564 open_files: OpenFiles,
565 changed_uri: Uri,
566 diag_cfg: crate::lang::config::DiagnosticsConfig,
567 is_laravel: bool,
568) -> Vec<(Uri, Vec<Diagnostic>)> {
569 tokio::task::spawn_blocking(move || {
570 if !docs.is_index_ready() {
575 return Vec::new();
576 }
577
578 let open_set: Vec<std::sync::Arc<str>> = open_files
584 .urls()
585 .into_iter()
586 .filter(|u| u != &changed_uri)
587 .map(|u| std::sync::Arc::from(u.as_str()))
588 .collect();
589 if open_set.is_empty() {
590 return Vec::new();
591 }
592
593 let session = docs.current_analysis_session();
594 let _interactive = docs.interactive_read_guard();
597 let cancel = docs.begin_reanalyze();
601 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
602 if cancel.is_cancelled() || analyses.is_empty() {
606 return Vec::new();
607 }
608
609 let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
610 .into_iter()
611 .filter_map(|(file, analysis)| {
612 let url = (file.as_ref()).parse::<Uri>().ok()?;
613 Some((url, analysis))
614 })
615 .collect();
616 if dependents.is_empty() {
617 return Vec::new();
618 }
619
620 let dep_files: Vec<Arc<str>> = dependents
624 .iter()
625 .map(|(u, _)| Arc::from(u.as_str()))
626 .collect();
627 let class_issues = session.class_issues(&dep_files);
628 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
629 std::collections::HashMap::new();
630 for issue in class_issues {
631 if issue.suppressed {
632 continue;
633 }
634 let file = issue.location.file.clone();
635 class_issues_by_file.entry(file).or_default().push(issue);
636 }
637
638 let mut out: Vec<(Uri, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
639 for (url, analysis) in dependents {
640 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
641 let mut issues: Vec<mir_issues::Issue> = analysis
642 .issues
643 .into_iter()
644 .filter(|i| !i.suppressed)
645 .collect();
646 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
647 issues.extend(extra);
648 }
649 let semantic =
650 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
651 let mut diags = merge_file_diagnostics(parse, semantic);
652 diags.extend(crate::laravel::unguarded_model_diagnostics(
653 &docs, &url, is_laravel,
654 ));
655 diags.extend(open_files.external_diagnostics(&url));
656 out.push((url, diags));
657 }
658 out
659 })
660 .await
661 .unwrap_or_default()
662}
663
664pub(super) async fn republish_after_index_ready(
677 client: Client,
678 docs: Arc<DocumentStore>,
679 open_files: OpenFiles,
680 diag_cfg: crate::lang::config::DiagnosticsConfig,
681 is_laravel: bool,
682) {
683 let uris = open_files.urls();
684 if uris.is_empty() {
685 return;
686 }
687 let docs_ref = Arc::clone(&docs);
688 let open_files_ref = open_files.clone();
689 let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
690 uris.into_iter()
691 .map(|uri| {
692 let diags = compute_open_file_diagnostics(
693 &docs_ref,
694 &open_files_ref,
695 &uri,
696 &diag_cfg,
697 is_laravel,
698 );
699 (uri, diags)
700 })
701 .collect()
702 })
703 .await
704 .unwrap_or_default();
705 for (uri, diags) in all {
706 let hash = diagnostics_content_hash(&diags);
707 if open_files.published_hash(&uri) == Some(hash) {
708 continue;
709 }
710 open_files.note_published(&uri, hash);
711 client.publish_diagnostics(uri, diags, None).await;
712 }
713}
714
715pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
718 use std::collections::hash_map::DefaultHasher;
719 use std::hash::{Hash, Hasher};
720 let mut hasher = DefaultHasher::new();
721 serde_json::to_string(diagnostics)
722 .unwrap_or_default()
723 .hash(&mut hasher);
724 hasher.finish()
725}
726
727pub(super) async fn publish_with_dependents(
736 client: Client,
737 docs: Arc<DocumentStore>,
738 open_files: OpenFiles,
739 uri: Uri,
740 diag_cfg: crate::lang::config::DiagnosticsConfig,
741 is_laravel: bool,
742) {
743 let docs_ref = Arc::clone(&docs);
744 let open_files_ref = open_files.clone();
745 let uri_ref = uri.clone();
746 let diag_cfg_ref = diag_cfg.clone();
747 let all_diags = tokio::task::spawn_blocking(move || {
748 compute_open_file_diagnostics(
749 &docs_ref,
750 &open_files_ref,
751 &uri_ref,
752 &diag_cfg_ref,
753 is_laravel,
754 )
755 })
756 .await
757 .unwrap_or_default();
758 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
759 client
760 .publish_diagnostics(uri.clone(), all_diags, None)
761 .await;
762 let dependents =
763 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg, is_laravel)
764 .await;
765 for (dep_uri, dep_diags) in dependents {
766 let hash = diagnostics_content_hash(&dep_diags);
767 if open_files.published_hash(&dep_uri) == Some(hash) {
768 continue;
769 }
770 open_files.note_published(&dep_uri, hash);
771 client.publish_diagnostics(dep_uri, dep_diags, None).await;
772 }
773}
774
775fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
778 use std::collections::hash_map::DefaultHasher;
779 use std::hash::{Hash, Hasher};
780
781 let mut hasher = DefaultHasher::new();
782 uri.hash(&mut hasher);
783 diagnostics.len().hash(&mut hasher);
784
785 for diag in diagnostics {
786 diag.range.start.line.hash(&mut hasher);
787 diag.range.start.character.hash(&mut hasher);
788 diag.range.end.line.hash(&mut hasher);
789 diag.range.end.character.hash(&mut hasher);
790 diag.message.hash(&mut hasher);
791 let severity_val = match diag.severity {
792 Some(tower_lsp_server::ls_types::DiagnosticSeverity::ERROR) => 1,
793 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
794 Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
795 Some(tower_lsp_server::ls_types::DiagnosticSeverity::HINT) => 4,
796 None => 0,
797 _ => 5, };
799 severity_val.hash(&mut hasher);
800 if let Some(code) = &diag.code {
801 format!("{:?}", code).hash(&mut hasher);
802 }
803 if let Some(source) = &diag.source {
804 source.hash(&mut hasher);
805 }
806 if let Some(tags) = &diag.tags {
807 for tag in tags {
808 let tag_val = match *tag {
809 tower_lsp_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
810 tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
811 _ => 3,
812 };
813 tag_val.hash(&mut hasher);
814 }
815 }
816 }
817
818 format!("v1:{:x}", hasher.finish())
819}
820
821pub mod debug_gate;
822mod handlers;
823mod helpers;
824pub mod offload;
825pub mod panic_guard;
826mod server;
827#[cfg(test)]
828mod tests;