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::lsp_types::notification::Notification for IndexReadyNotification {
13 type Params = ();
14 const METHOD: &'static str = "$/php-lsp/indexReady";
15}
16use tower_lsp::Client;
17use tower_lsp::lsp_types::*;
18
19#[derive(Debug, serde::Serialize, serde::Deserialize)]
21pub struct DebugStats {
22 pub parses: u64,
24 pub ref_index_locks: u64,
27}
28
29use crate::document::ast::ParsedDoc;
30use crate::document::document_store::DocumentStore;
31use crate::document::open_files::OpenFiles;
32use crate::lang::autoload::Psr4Map;
33use crate::lang::config::LspConfig;
34use crate::lang::phpstorm_meta::PhpStormMeta;
35use crate::text::fqn_short_name;
36
37use crate::navigation::references::find_constructor_references;
38
39use crate::analysis::diagnostics::merge_file_diagnostics;
40use crate::document::open_files::compute_open_file_diagnostics;
41
42pub struct Backend {
43 client: Client,
44 docs: Arc<DocumentStore>,
45 open_files: OpenFiles,
49 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
50 psr4: Arc<ArcSwap<Psr4Map>>,
51 meta: Arc<ArcSwap<PhpStormMeta>>,
52 config: Arc<ArcSwap<LspConfig>>,
53}
54
55impl Backend {
56 pub fn new(client: Client) -> Self {
57 let docs = Arc::new(DocumentStore::new());
62 let psr4 = docs.psr4_arc();
63 Backend {
64 client,
65 docs,
66 open_files: OpenFiles::new(),
67 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
68 psr4,
69 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
70 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
71 }
72 }
73
74 pub async fn debug_stats(&self) -> tower_lsp::jsonrpc::Result<DebugStats> {
78 Ok(DebugStats {
79 parses: self.docs.parse_count(),
80 ref_index_locks: self.docs.ref_index_lock_count(),
81 })
82 }
83
84 fn set_open_text(&self, uri: Url, text: String) -> u64 {
85 self.open_files.set_open_text(&self.docs, uri, text)
86 }
87
88 fn close_open_file(&self, uri: &Url) {
89 self.open_files.close(&self.docs, uri);
90 }
91
92 fn ingest_if_not_open(&self, uri: Url, text: &str) {
96 if !self.open_files.contains(&uri) {
97 self.docs.ingest(uri, text);
98 }
99 }
100
101 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
103 if !self.open_files.contains(&uri) {
104 self.docs.ingest_from_doc(uri, doc);
105 }
106 }
107
108 fn get_open_text(&self, uri: &Url) -> Option<String> {
109 self.open_files.text(uri)
110 }
111
112 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
113 self.open_files.set_parse_diagnostics(uri, diagnostics);
114 }
115
116 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
117 self.open_files.parse_diagnostics(uri)
118 }
119
120 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
121 self.open_files.all_with_diagnostics()
122 }
123
124 fn open_urls(&self) -> Vec<Url> {
125 self.open_files.urls()
126 }
127
128 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
129 self.open_files.get_doc(&self.docs, uri)
130 }
131
132 fn get_doc_stale(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
133 self.open_files.get_doc_stale(&self.docs, uri)
134 }
135
136 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
138 self.docs
139 .get_doc_salsa(uri)
140 .map(|doc| crate::references::collect_file_imports(&doc))
141 .unwrap_or_default()
142 }
143
144 fn construct_references(
157 &self,
158 uri: &Url,
159 source: &str,
160 position: Position,
161 class_name: &str,
162 include_declaration: bool,
163 ) -> Vec<Location> {
164 let short_name = fqn_short_name(class_name).to_owned();
165 let class_fqn = class_name.contains('\\').then_some(class_name);
166 let candidate_docs = self.docs.candidate_docs_for(&short_name);
168 let mut locations = find_constructor_references(&short_name, &candidate_docs, class_fqn);
172 if include_declaration && let Some(range) = crate::text::word_range_at(source, position) {
177 locations.push(Location {
178 uri: uri.clone(),
179 range,
180 });
181 }
182 locations
183 }
184
185 fn resolve_reference_target_fqn(
191 &self,
192 uri: &Url,
193 doc_opt: Option<&Arc<ParsedDoc>>,
194 word: &str,
195 kind: Option<crate::navigation::references::SymbolKind>,
196 position: Position,
197 constant_owner: Option<String>,
198 ) -> Option<String> {
199 use crate::navigation::references::SymbolKind;
200 let doc = doc_opt?;
201 let imports = self.file_imports(uri);
202 match kind {
203 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
204 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
205 resolved.contains('\\').then_some(resolved)
206 }
207 Some(SymbolKind::Method) => {
208 let short_owner =
210 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
211 Some(crate::navigation::moniker::resolve_fqn(
213 doc,
214 &short_owner,
215 &imports,
216 ))
217 }
218 Some(SymbolKind::Property) => {
219 let stmts = &doc.program().stmts;
225 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
226 let short_owner =
227 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
228 Some(crate::navigation::moniker::resolve_fqn(
229 doc,
230 &short_owner,
231 &imports,
232 ))
233 }
234 Some(SymbolKind::Constant) => {
235 if constant_owner.is_some() {
236 constant_owner
238 } else {
239 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
242 fqn.contains('\\').then_some(fqn)
243 }
244 }
245 _ => None,
246 }
247 }
248
249 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
252 let roots = self.root_paths.load();
253 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
254 }
255}
256
257fn resolve_reference_symbol(
266 doc_opt: Option<&Arc<ParsedDoc>>,
267 source: &str,
268 position: Position,
269 word: String,
270) -> (
271 String,
272 Option<crate::navigation::references::SymbolKind>,
273 Option<String>,
274) {
275 use crate::navigation::references::SymbolKind;
276 let mut constant_owner: Option<String> = None;
277 let (word, kind) = if let Some(doc) = doc_opt
278 && let Some(prop_name) =
279 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
280 {
281 (prop_name, Some(SymbolKind::Property))
282 } else if let Some(doc) = doc_opt {
283 let stmts = &doc.program().stmts;
284 if cursor_is_on_method_decl(doc.source(), stmts, position) {
285 (word, Some(SymbolKind::Method))
286 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
287 (prop_name, Some(SymbolKind::Property))
288 } else if let Some((const_name, owner)) =
289 cursor_is_on_constant_decl(doc.source(), stmts, position)
290 {
291 constant_owner = owner;
292 (const_name, Some(SymbolKind::Constant))
293 } else {
294 let k = symbol_kind_at(source, position, &word);
295 if matches!(k, Some(SymbolKind::Constant))
300 && let Some(raw) = class_before_double_colon(source, position)
301 {
302 constant_owner = Some(match raw.as_str() {
303 "self" | "static" => {
304 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
305 .unwrap_or(raw)
306 }
307 _ => raw,
308 });
309 }
310 (word, k)
311 }
312 } else {
313 let k = symbol_kind_at(source, position, &word);
314 (word, k)
315 };
316 (word, kind, constant_owner)
317}
318
319fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
327 let line = source.lines().nth(position.line as usize)?;
328 let chars: Vec<char> = line.chars().collect();
329 let col = position.character as usize;
330
331 let mut utf16_col = 0usize;
332 let mut char_idx = 0usize;
333 for ch in &chars {
334 if utf16_col >= col {
335 break;
336 }
337 utf16_col += ch.len_utf16();
338 char_idx += 1;
339 }
340
341 let is_word = |c: char| c.is_alphanumeric() || c == '_';
342 while char_idx > 0 && is_word(chars[char_idx - 1]) {
343 char_idx -= 1;
344 }
345
346 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
347 return None;
348 }
349
350 let class_end = char_idx - 2;
351 let mut class_start = class_end;
352 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
353 class_start -= 1;
354 }
355
356 let name: String = chars[class_start..class_end].iter().collect();
357 if name.is_empty() { None } else { Some(name) }
358}
359
360async fn compute_dependent_publishes_owned(
365 docs: Arc<DocumentStore>,
366 open_files: OpenFiles,
367 changed_uri: Url,
368 diag_cfg: crate::lang::config::DiagnosticsConfig,
369) -> Vec<(Url, Vec<Diagnostic>)> {
370 tokio::task::spawn_blocking(move || {
371 let open_set: Vec<std::sync::Arc<str>> = open_files
377 .urls()
378 .into_iter()
379 .filter(|u| u != &changed_uri)
380 .map(|u| std::sync::Arc::from(u.as_str()))
381 .collect();
382 if open_set.is_empty() {
383 return Vec::new();
384 }
385
386 let php_version = docs.workspace_php_version();
387 let session = docs.analysis_session(php_version);
388 let _interactive = docs.interactive_read_guard();
391 let cancel = docs.begin_reanalyze();
395 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
396 if cancel.is_cancelled() || analyses.is_empty() {
400 return Vec::new();
401 }
402
403 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
404 .into_iter()
405 .filter_map(|(file, analysis)| {
406 let url = Url::parse(file.as_ref()).ok()?;
407 Some((url, analysis))
408 })
409 .collect();
410 if dependents.is_empty() {
411 return Vec::new();
412 }
413
414 let dep_files: Vec<Arc<str>> = dependents
418 .iter()
419 .map(|(u, _)| Arc::from(u.as_str()))
420 .collect();
421 let class_issues = session.class_issues(&dep_files);
422 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
423 std::collections::HashMap::new();
424 for issue in class_issues {
425 if issue.suppressed {
426 continue;
427 }
428 let file = issue.location.file.clone();
429 class_issues_by_file.entry(file).or_default().push(issue);
430 }
431
432 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
433 for (url, analysis) in dependents {
434 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
435 let mut issues: Vec<mir_issues::Issue> = analysis
436 .issues
437 .into_iter()
438 .filter(|i| !i.suppressed)
439 .collect();
440 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
441 issues.extend(extra);
442 }
443 let semantic =
444 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
445 out.push((url, merge_file_diagnostics(parse, semantic)));
446 }
447 out
448 })
449 .await
450 .unwrap_or_default()
451}
452
453pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
456 use std::collections::hash_map::DefaultHasher;
457 use std::hash::{Hash, Hasher};
458 let mut hasher = DefaultHasher::new();
459 serde_json::to_string(diagnostics)
460 .unwrap_or_default()
461 .hash(&mut hasher);
462 hasher.finish()
463}
464
465pub(super) async fn publish_with_dependents(
474 client: Client,
475 docs: Arc<DocumentStore>,
476 open_files: OpenFiles,
477 uri: Url,
478 diag_cfg: crate::lang::config::DiagnosticsConfig,
479) {
480 let docs_ref = Arc::clone(&docs);
481 let open_files_ref = open_files.clone();
482 let uri_ref = uri.clone();
483 let diag_cfg_ref = diag_cfg.clone();
484 let all_diags = tokio::task::spawn_blocking(move || {
485 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
486 })
487 .await
488 .unwrap_or_default();
489 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
490 client
491 .publish_diagnostics(uri.clone(), all_diags, None)
492 .await;
493 let dependents =
494 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg).await;
495 for (dep_uri, dep_diags) in dependents {
496 let hash = diagnostics_content_hash(&dep_diags);
497 if open_files.published_hash(&dep_uri) == Some(hash) {
498 continue;
499 }
500 open_files.note_published(&dep_uri, hash);
501 client.publish_diagnostics(dep_uri, dep_diags, None).await;
502 }
503}
504
505fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
508 use std::collections::hash_map::DefaultHasher;
509 use std::hash::{Hash, Hasher};
510
511 let mut hasher = DefaultHasher::new();
512 uri.hash(&mut hasher);
513 diagnostics.len().hash(&mut hasher);
514
515 for diag in diagnostics {
516 diag.range.start.line.hash(&mut hasher);
517 diag.range.start.character.hash(&mut hasher);
518 diag.range.end.line.hash(&mut hasher);
519 diag.range.end.character.hash(&mut hasher);
520 diag.message.hash(&mut hasher);
521 let severity_val = match diag.severity {
522 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
523 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
524 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
525 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
526 None => 0,
527 _ => 5, };
529 severity_val.hash(&mut hasher);
530 if let Some(code) = &diag.code {
531 format!("{:?}", code).hash(&mut hasher);
532 }
533 if let Some(source) = &diag.source {
534 source.hash(&mut hasher);
535 }
536 if let Some(tags) = &diag.tags {
537 for tag in tags {
538 let tag_val = match *tag {
539 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
540 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
541 _ => 3,
542 };
543 tag_val.hash(&mut hasher);
544 }
545 }
546 }
547
548 format!("v1:{:x}", hasher.finish())
549}
550
551mod handlers;
552mod helpers;
553pub mod panic_guard;
554mod server;
555#[cfg(test)]
556mod tests;