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 debug_gate_held: Option<String>,
95}
96
97#[cfg(feature = "test-hooks")]
99#[derive(Debug, serde::Serialize, serde::Deserialize)]
100pub struct DebugHoldGateParams {
101 pub section: String,
103}
104
105pub fn build_lsp_service() -> (
110 tower_lsp_server::LspService<Backend>,
111 tower_lsp_server::ClientSocket,
112) {
113 let builder = tower_lsp_server::LspService::build(Backend::new)
114 .custom_method("$/php-lsp/debugStats", Backend::debug_stats);
115 #[cfg(feature = "test-hooks")]
116 let builder = builder
117 .custom_method("$/php-lsp/debugHoldGate", Backend::debug_hold_gate)
118 .custom_method("$/php-lsp/debugReleaseGate", Backend::debug_release_gate);
119 builder.finish()
120}
121
122use crate::document::ast::ParsedDoc;
123use crate::document::document_store::DocumentStore;
124use crate::document::open_files::OpenFiles;
125use crate::lang::autoload::Psr4Map;
126use crate::lang::config::LspConfig;
127use crate::laravel::LaravelIndex;
128
129use crate::analysis::diagnostics::merge_file_diagnostics;
130use crate::document::open_files::compute_open_file_diagnostics;
131
132pub struct Backend {
133 client: Client,
134 docs: Arc<DocumentStore>,
135 open_files: OpenFiles,
139 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
140 psr4: Arc<ArcSwap<Psr4Map>>,
141 laravel: Arc<ArcSwap<LaravelIndex>>,
142 config: Arc<ArcSwap<LspConfig>>,
143 client_capabilities: Arc<ArcSwap<ClientCapabilities>>,
149 debug_gate: Arc<debug_gate::DebugGate>,
151}
152
153impl Backend {
154 pub fn new(client: Client) -> Self {
155 let docs = Arc::new(DocumentStore::new());
160 let psr4 = docs.psr4_arc();
161 Backend {
162 client,
163 docs,
164 open_files: OpenFiles::new(),
165 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
166 psr4,
167 laravel: Arc::new(ArcSwap::from_pointee(LaravelIndex::default())),
168 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
169 client_capabilities: Arc::new(ArcSwap::from_pointee(ClientCapabilities::default())),
170 debug_gate: Arc::new(debug_gate::DebugGate::new()),
171 }
172 }
173
174 pub async fn debug_stats(&self) -> tower_lsp_server::jsonrpc::Result<DebugStats> {
178 Ok(DebugStats {
179 parses: self.docs.parse_count(),
180 ref_index_locks: self.docs.ref_index_lock_count(),
181 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
182 workspace_symbol_index_ready: self.docs.workspace_symbol_index_ready(),
183 workspace_index_walks: self.docs.workspace_index_walks(),
184 reachability_scan_passes: self.docs.reachability_scan_passes(),
185 analysis_compute_count: self.docs.analysis_compute_count(),
186 warm_start_replays_completed: self.docs.warm_start_replays_completed(),
187 debug_gate_held: self.debug_gate.held_section(),
188 })
189 }
190
191 #[cfg(feature = "test-hooks")]
194 pub async fn debug_hold_gate(
195 &self,
196 params: DebugHoldGateParams,
197 ) -> tower_lsp_server::jsonrpc::Result<()> {
198 self.debug_gate.arm(¶ms.section);
199 Ok(())
200 }
201
202 #[cfg(feature = "test-hooks")]
205 pub async fn debug_release_gate(&self) -> tower_lsp_server::jsonrpc::Result<()> {
206 self.debug_gate.release();
207 Ok(())
208 }
209
210 fn set_open_text(&self, uri: Uri, text: String) -> u64 {
211 self.open_files.set_open_text(&self.docs, uri, text)
212 }
213
214 fn close_open_file(&self, uri: &Uri) {
215 self.open_files.close(&self.docs, uri);
216 }
217
218 fn ingest_if_not_open(&self, uri: Uri, text: &str) {
222 if !self.open_files.contains(&uri) {
223 self.docs.ingest(uri, text);
224 }
225 }
226
227 fn get_open_text(&self, uri: &Uri) -> Option<Arc<str>> {
228 self.open_files.text(uri)
229 }
230
231 async fn blocking<F, R>(&self, label: &'static str, f: F) -> Option<R>
234 where
235 F: FnOnce() -> R + Send + 'static,
236 R: Send + 'static,
237 {
238 offload::run(label, f).await
239 }
240
241 async fn blocking_gated<F, R>(&self, section: &'static str, f: F) -> Option<R>
244 where
245 F: FnOnce() -> R + Send + 'static,
246 R: Send + 'static,
247 {
248 offload::run_gated(&self.debug_gate, section, f).await
249 }
250
251 fn set_parse_diagnostics(&self, uri: &Uri, diagnostics: Vec<Diagnostic>) {
252 self.open_files.set_parse_diagnostics(uri, diagnostics);
253 }
254
255 fn get_parse_diagnostics(&self, uri: &Uri) -> Option<Vec<Diagnostic>> {
256 self.open_files.parse_diagnostics(uri)
257 }
258
259 fn all_open_files_with_diagnostics(&self) -> Vec<(Uri, Vec<Diagnostic>, Option<i64>)> {
260 self.open_files.all_with_diagnostics()
261 }
262
263 fn open_urls(&self) -> Vec<Uri> {
264 self.open_files.urls()
265 }
266
267 fn get_doc(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
268 self.open_files.get_doc(&self.docs, uri)
269 }
270
271 fn get_doc_stale(&self, uri: &Uri) -> Option<Arc<ParsedDoc>> {
272 self.open_files.get_doc_stale(&self.docs, uri)
273 }
274
275 pub(crate) fn remirror_open_files(&self) {
284 for uri in self.open_files.urls() {
285 if let Some(text) = self.open_files.text(&uri) {
286 self.docs.mirror_text_arc(&uri, text);
287 }
288 }
289 }
290
291 fn file_imports(&self, uri: &Uri) -> Arc<std::collections::HashMap<String, String>> {
295 self.docs
296 .get_doc_salsa(uri)
297 .map(|doc| doc.file_imports())
298 .unwrap_or_default()
299 }
300
301 fn resolve_reference_target_fqn(
307 &self,
308 uri: &Uri,
309 doc_opt: Option<&Arc<ParsedDoc>>,
310 word: &str,
311 kind: Option<crate::navigation::references::SymbolKind>,
312 position: Position,
313 constant_owner: Option<String>,
314 ) -> Option<String> {
315 use crate::navigation::references::SymbolKind;
316 let doc = doc_opt?;
317 let imports = self.file_imports(uri);
318 match kind {
319 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
320 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
321 resolved.contains('\\').then_some(resolved)
322 }
323 Some(SymbolKind::Method) => {
324 let short_owner =
326 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
327 Some(crate::navigation::moniker::resolve_fqn(
329 doc,
330 &short_owner,
331 &imports,
332 ))
333 }
334 Some(SymbolKind::Property) => {
335 let stmts = &doc.program().stmts;
340 if crate::backend::helpers::cursor_is_on_property_decl(
341 doc.source(),
342 stmts,
343 position,
344 )
345 .is_none()
346 && promoted_property_at_cursor(doc.source(), stmts, position).is_none()
347 {
348 return None;
349 }
350 let short_owner =
351 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
352 Some(crate::navigation::moniker::resolve_fqn(
353 doc,
354 &short_owner,
355 &imports,
356 ))
357 }
358 Some(SymbolKind::Constant) => {
359 if let Some(owner) = constant_owner {
360 if owner.contains('\\') {
363 Some(owner.trim_start_matches('\\').to_string())
364 } else {
365 Some(crate::navigation::moniker::resolve_fqn(
366 doc, &owner, &imports,
367 ))
368 }
369 } else {
370 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
373 fqn.contains('\\').then_some(fqn)
374 }
375 }
376 _ => None,
377 }
378 }
379
380 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
383 let roots = self.root_paths.load();
384 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
385 }
386}
387
388fn resolve_reference_symbol(
397 doc_opt: Option<&Arc<ParsedDoc>>,
398 source: &str,
399 position: Position,
400 word: String,
401) -> (
402 String,
403 Option<crate::navigation::references::SymbolKind>,
404 Option<String>,
405) {
406 use crate::navigation::references::SymbolKind;
407 let mut constant_owner: Option<String> = None;
408 let (word, kind) = if let Some(doc) = doc_opt
409 && let Some(prop_name) =
410 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
411 {
412 (prop_name, Some(SymbolKind::Property))
413 } else if let Some(doc) = doc_opt {
414 let stmts = &doc.program().stmts;
415 if cursor_is_on_method_decl(doc.source(), stmts, position) {
416 (word, Some(SymbolKind::Method))
417 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
418 (prop_name, Some(SymbolKind::Property))
419 } else if let Some((const_name, owner)) =
420 cursor_is_on_constant_decl(doc.source(), stmts, position)
421 {
422 constant_owner = owner;
423 (const_name, Some(SymbolKind::Constant))
424 } else {
425 let k = symbol_kind_at(source, position, &word);
426 if matches!(k, Some(SymbolKind::Constant))
431 && let Some(raw) = class_before_double_colon(source, position)
432 {
433 constant_owner = Some(match raw.as_str() {
434 "self" | "static" => {
435 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
436 .unwrap_or(raw)
437 }
438 "parent" => {
445 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
446 .and_then(|child| {
447 crate::types::type_map::parent_class_name(doc, &child)
448 })
449 .unwrap_or(raw)
450 }
451 _ => raw,
452 });
453 }
454 (word, k)
455 }
456 } else {
457 let k = symbol_kind_at(source, position, &word);
458 (word, k)
459 };
460 (word, kind, constant_owner)
461}
462
463fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
471 let line = source.lines().nth(position.line as usize)?;
472 let chars: Vec<char> = line.chars().collect();
473 let col = position.character as usize;
474
475 let mut utf16_col = 0usize;
476 let mut char_idx = 0usize;
477 for ch in &chars {
478 if utf16_col >= col {
479 break;
480 }
481 utf16_col += ch.len_utf16();
482 char_idx += 1;
483 }
484
485 let is_word = |c: char| c.is_alphanumeric() || c == '_';
486 while char_idx > 0 && is_word(chars[char_idx - 1]) {
487 char_idx -= 1;
488 }
489
490 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
491 return None;
492 }
493
494 let class_end = char_idx - 2;
495 let mut class_start = class_end;
496 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
497 class_start -= 1;
498 }
499
500 let name: String = chars[class_start..class_end].iter().collect();
501 if name.is_empty() { None } else { Some(name) }
502}
503
504async fn compute_dependent_publishes_owned(
509 docs: Arc<DocumentStore>,
510 open_files: OpenFiles,
511 changed_uri: Uri,
512 diag_cfg: crate::lang::config::DiagnosticsConfig,
513) -> Vec<(Uri, Vec<Diagnostic>)> {
514 tokio::task::spawn_blocking(move || {
515 let open_set: Vec<std::sync::Arc<str>> = open_files
521 .urls()
522 .into_iter()
523 .filter(|u| u != &changed_uri)
524 .map(|u| std::sync::Arc::from(u.as_str()))
525 .collect();
526 if open_set.is_empty() {
527 return Vec::new();
528 }
529
530 let session = docs.current_analysis_session();
531 let _interactive = docs.interactive_read_guard();
534 let cancel = docs.begin_reanalyze();
538 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
539 if cancel.is_cancelled() || analyses.is_empty() {
543 return Vec::new();
544 }
545
546 let dependents: Vec<(Uri, mir_analyzer::FileAnalysis)> = analyses
547 .into_iter()
548 .filter_map(|(file, analysis)| {
549 let url = (file.as_ref()).parse::<Uri>().ok()?;
550 Some((url, analysis))
551 })
552 .collect();
553 if dependents.is_empty() {
554 return Vec::new();
555 }
556
557 let dep_files: Vec<Arc<str>> = dependents
561 .iter()
562 .map(|(u, _)| Arc::from(u.as_str()))
563 .collect();
564 let class_issues = session.class_issues(&dep_files);
565 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
566 std::collections::HashMap::new();
567 for issue in class_issues {
568 if issue.suppressed {
569 continue;
570 }
571 let file = issue.location.file.clone();
572 class_issues_by_file.entry(file).or_default().push(issue);
573 }
574
575 let mut out: Vec<(Uri, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
576 for (url, analysis) in dependents {
577 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
578 let mut issues: Vec<mir_issues::Issue> = analysis
579 .issues
580 .into_iter()
581 .filter(|i| !i.suppressed)
582 .collect();
583 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
584 issues.extend(extra);
585 }
586 let semantic =
587 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
588 let mut diags = merge_file_diagnostics(parse, semantic);
589 diags.extend(open_files.external_diagnostics(&url));
590 out.push((url, diags));
591 }
592 out
593 })
594 .await
595 .unwrap_or_default()
596}
597
598pub(super) async fn republish_after_index_ready(
611 client: Client,
612 docs: Arc<DocumentStore>,
613 open_files: OpenFiles,
614 diag_cfg: crate::lang::config::DiagnosticsConfig,
615 is_laravel: bool,
616) {
617 let uris = open_files.urls();
618 if uris.is_empty() {
619 return;
620 }
621 let docs_ref = Arc::clone(&docs);
622 let open_files_ref = open_files.clone();
623 let all: Vec<(Uri, Vec<Diagnostic>)> = tokio::task::spawn_blocking(move || {
624 uris.into_iter()
625 .map(|uri| {
626 let diags = compute_open_file_diagnostics(
627 &docs_ref,
628 &open_files_ref,
629 &uri,
630 &diag_cfg,
631 is_laravel,
632 );
633 (uri, diags)
634 })
635 .collect()
636 })
637 .await
638 .unwrap_or_default();
639 for (uri, diags) in all {
640 let hash = diagnostics_content_hash(&diags);
641 if open_files.published_hash(&uri) == Some(hash) {
642 continue;
643 }
644 open_files.note_published(&uri, hash);
645 client.publish_diagnostics(uri, diags, None).await;
646 }
647}
648
649pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
652 use std::collections::hash_map::DefaultHasher;
653 use std::hash::{Hash, Hasher};
654 let mut hasher = DefaultHasher::new();
655 serde_json::to_string(diagnostics)
656 .unwrap_or_default()
657 .hash(&mut hasher);
658 hasher.finish()
659}
660
661pub(super) async fn publish_with_dependents(
670 client: Client,
671 docs: Arc<DocumentStore>,
672 open_files: OpenFiles,
673 uri: Uri,
674 diag_cfg: crate::lang::config::DiagnosticsConfig,
675 is_laravel: bool,
676) {
677 let docs_ref = Arc::clone(&docs);
678 let open_files_ref = open_files.clone();
679 let uri_ref = uri.clone();
680 let diag_cfg_ref = diag_cfg.clone();
681 let all_diags = tokio::task::spawn_blocking(move || {
682 compute_open_file_diagnostics(
683 &docs_ref,
684 &open_files_ref,
685 &uri_ref,
686 &diag_cfg_ref,
687 is_laravel,
688 )
689 })
690 .await
691 .unwrap_or_default();
692 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
693 client
694 .publish_diagnostics(uri.clone(), all_diags, None)
695 .await;
696 let dependents =
697 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg).await;
698 for (dep_uri, dep_diags) in dependents {
699 let hash = diagnostics_content_hash(&dep_diags);
700 if open_files.published_hash(&dep_uri) == Some(hash) {
701 continue;
702 }
703 open_files.note_published(&dep_uri, hash);
704 client.publish_diagnostics(dep_uri, dep_diags, None).await;
705 }
706}
707
708fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
711 use std::collections::hash_map::DefaultHasher;
712 use std::hash::{Hash, Hasher};
713
714 let mut hasher = DefaultHasher::new();
715 uri.hash(&mut hasher);
716 diagnostics.len().hash(&mut hasher);
717
718 for diag in diagnostics {
719 diag.range.start.line.hash(&mut hasher);
720 diag.range.start.character.hash(&mut hasher);
721 diag.range.end.line.hash(&mut hasher);
722 diag.range.end.character.hash(&mut hasher);
723 diag.message.hash(&mut hasher);
724 let severity_val = match diag.severity {
725 Some(tower_lsp_server::ls_types::DiagnosticSeverity::ERROR) => 1,
726 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING) => 2,
727 Some(tower_lsp_server::ls_types::DiagnosticSeverity::INFORMATION) => 3,
728 Some(tower_lsp_server::ls_types::DiagnosticSeverity::HINT) => 4,
729 None => 0,
730 _ => 5, };
732 severity_val.hash(&mut hasher);
733 if let Some(code) = &diag.code {
734 format!("{:?}", code).hash(&mut hasher);
735 }
736 if let Some(source) = &diag.source {
737 source.hash(&mut hasher);
738 }
739 if let Some(tags) = &diag.tags {
740 for tag in tags {
741 let tag_val = match *tag {
742 tower_lsp_server::ls_types::DiagnosticTag::UNNECESSARY => 1,
743 tower_lsp_server::ls_types::DiagnosticTag::DEPRECATED => 2,
744 _ => 3,
745 };
746 tag_val.hash(&mut hasher);
747 }
748 }
749 }
750
751 format!("v1:{:x}", hasher.finish())
752}
753
754pub mod debug_gate;
755mod handlers;
756mod helpers;
757pub mod offload;
758pub mod panic_guard;
759mod server;
760#[cfg(test)]
761mod tests;