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,
28 pub warm_sweeps_completed: u64,
31}
32
33use crate::document::ast::ParsedDoc;
34use crate::document::document_store::DocumentStore;
35use crate::document::open_files::OpenFiles;
36use crate::lang::autoload::Psr4Map;
37use crate::lang::config::LspConfig;
38use crate::lang::phpstorm_meta::PhpStormMeta;
39
40use crate::analysis::diagnostics::merge_file_diagnostics;
41use crate::document::open_files::compute_open_file_diagnostics;
42
43pub struct Backend {
44 client: Client,
45 docs: Arc<DocumentStore>,
46 open_files: OpenFiles,
50 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
51 psr4: Arc<ArcSwap<Psr4Map>>,
52 meta: Arc<ArcSwap<PhpStormMeta>>,
53 config: Arc<ArcSwap<LspConfig>>,
54}
55
56impl Backend {
57 pub fn new(client: Client) -> Self {
58 let docs = Arc::new(DocumentStore::new());
63 let psr4 = docs.psr4_arc();
64 Backend {
65 client,
66 docs,
67 open_files: OpenFiles::new(),
68 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
69 psr4,
70 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
71 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
72 }
73 }
74
75 pub async fn debug_stats(&self) -> tower_lsp::jsonrpc::Result<DebugStats> {
79 Ok(DebugStats {
80 parses: self.docs.parse_count(),
81 ref_index_locks: self.docs.ref_index_lock_count(),
82 warm_sweeps_completed: self.docs.warm_sweeps_completed(),
83 })
84 }
85
86 fn set_open_text(&self, uri: Url, text: String) -> u64 {
87 self.open_files.set_open_text(&self.docs, uri, text)
88 }
89
90 fn close_open_file(&self, uri: &Url) {
91 self.open_files.close(&self.docs, uri);
92 }
93
94 fn ingest_if_not_open(&self, uri: Url, text: &str) {
98 if !self.open_files.contains(&uri) {
99 self.docs.ingest(uri, text);
100 }
101 }
102
103 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
105 if !self.open_files.contains(&uri) {
106 self.docs.ingest_from_doc(uri, doc);
107 }
108 }
109
110 fn get_open_text(&self, uri: &Url) -> Option<String> {
111 self.open_files.text(uri)
112 }
113
114 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
115 self.open_files.set_parse_diagnostics(uri, diagnostics);
116 }
117
118 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
119 self.open_files.parse_diagnostics(uri)
120 }
121
122 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
123 self.open_files.all_with_diagnostics()
124 }
125
126 fn open_urls(&self) -> Vec<Url> {
127 self.open_files.urls()
128 }
129
130 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
131 self.open_files.get_doc(&self.docs, uri)
132 }
133
134 fn get_doc_stale(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
135 self.open_files.get_doc_stale(&self.docs, uri)
136 }
137
138 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
140 self.docs
141 .get_doc_salsa(uri)
142 .map(|doc| crate::references::collect_file_imports(&doc))
143 .unwrap_or_default()
144 }
145
146 fn resolve_reference_target_fqn(
152 &self,
153 uri: &Url,
154 doc_opt: Option<&Arc<ParsedDoc>>,
155 word: &str,
156 kind: Option<crate::navigation::references::SymbolKind>,
157 position: Position,
158 constant_owner: Option<String>,
159 ) -> Option<String> {
160 use crate::navigation::references::SymbolKind;
161 let doc = doc_opt?;
162 let imports = self.file_imports(uri);
163 match kind {
164 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
165 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
166 resolved.contains('\\').then_some(resolved)
167 }
168 Some(SymbolKind::Method) => {
169 let short_owner =
171 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
172 Some(crate::navigation::moniker::resolve_fqn(
174 doc,
175 &short_owner,
176 &imports,
177 ))
178 }
179 Some(SymbolKind::Property) => {
180 let stmts = &doc.program().stmts;
185 if crate::backend::helpers::cursor_is_on_property_decl(
186 doc.source(),
187 stmts,
188 position,
189 )
190 .is_none()
191 && promoted_property_at_cursor(doc.source(), stmts, position).is_none()
192 {
193 return None;
194 }
195 let short_owner =
196 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
197 Some(crate::navigation::moniker::resolve_fqn(
198 doc,
199 &short_owner,
200 &imports,
201 ))
202 }
203 Some(SymbolKind::Constant) => {
204 if let Some(owner) = constant_owner {
205 if owner.contains('\\') {
208 Some(owner.trim_start_matches('\\').to_string())
209 } else {
210 Some(crate::navigation::moniker::resolve_fqn(
211 doc, &owner, &imports,
212 ))
213 }
214 } else {
215 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
218 fqn.contains('\\').then_some(fqn)
219 }
220 }
221 _ => None,
222 }
223 }
224
225 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
228 let roots = self.root_paths.load();
229 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
230 }
231}
232
233fn resolve_reference_symbol(
242 doc_opt: Option<&Arc<ParsedDoc>>,
243 source: &str,
244 position: Position,
245 word: String,
246) -> (
247 String,
248 Option<crate::navigation::references::SymbolKind>,
249 Option<String>,
250) {
251 use crate::navigation::references::SymbolKind;
252 let mut constant_owner: Option<String> = None;
253 let (word, kind) = if let Some(doc) = doc_opt
254 && let Some(prop_name) =
255 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
256 {
257 (prop_name, Some(SymbolKind::Property))
258 } else if let Some(doc) = doc_opt {
259 let stmts = &doc.program().stmts;
260 if cursor_is_on_method_decl(doc.source(), stmts, position) {
261 (word, Some(SymbolKind::Method))
262 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
263 (prop_name, Some(SymbolKind::Property))
264 } else if let Some((const_name, owner)) =
265 cursor_is_on_constant_decl(doc.source(), stmts, position)
266 {
267 constant_owner = owner;
268 (const_name, Some(SymbolKind::Constant))
269 } else {
270 let k = symbol_kind_at(source, position, &word);
271 if matches!(k, Some(SymbolKind::Constant))
276 && let Some(raw) = class_before_double_colon(source, position)
277 {
278 constant_owner = Some(match raw.as_str() {
279 "self" | "static" => {
280 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
281 .unwrap_or(raw)
282 }
283 _ => raw,
284 });
285 }
286 (word, k)
287 }
288 } else {
289 let k = symbol_kind_at(source, position, &word);
290 (word, k)
291 };
292 (word, kind, constant_owner)
293}
294
295fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
303 let line = source.lines().nth(position.line as usize)?;
304 let chars: Vec<char> = line.chars().collect();
305 let col = position.character as usize;
306
307 let mut utf16_col = 0usize;
308 let mut char_idx = 0usize;
309 for ch in &chars {
310 if utf16_col >= col {
311 break;
312 }
313 utf16_col += ch.len_utf16();
314 char_idx += 1;
315 }
316
317 let is_word = |c: char| c.is_alphanumeric() || c == '_';
318 while char_idx > 0 && is_word(chars[char_idx - 1]) {
319 char_idx -= 1;
320 }
321
322 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
323 return None;
324 }
325
326 let class_end = char_idx - 2;
327 let mut class_start = class_end;
328 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
329 class_start -= 1;
330 }
331
332 let name: String = chars[class_start..class_end].iter().collect();
333 if name.is_empty() { None } else { Some(name) }
334}
335
336async fn compute_dependent_publishes_owned(
341 docs: Arc<DocumentStore>,
342 open_files: OpenFiles,
343 changed_uri: Url,
344 diag_cfg: crate::lang::config::DiagnosticsConfig,
345) -> Vec<(Url, Vec<Diagnostic>)> {
346 tokio::task::spawn_blocking(move || {
347 let open_set: Vec<std::sync::Arc<str>> = open_files
353 .urls()
354 .into_iter()
355 .filter(|u| u != &changed_uri)
356 .map(|u| std::sync::Arc::from(u.as_str()))
357 .collect();
358 if open_set.is_empty() {
359 return Vec::new();
360 }
361
362 let php_version = docs.workspace_php_version();
363 let session = docs.analysis_session(php_version);
364 let _interactive = docs.interactive_read_guard();
367 let cancel = docs.begin_reanalyze();
371 let analyses = session.reanalyze_files_cancellable(&open_set, &cancel);
372 if cancel.is_cancelled() || analyses.is_empty() {
376 return Vec::new();
377 }
378
379 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
380 .into_iter()
381 .filter_map(|(file, analysis)| {
382 let url = Url::parse(file.as_ref()).ok()?;
383 Some((url, analysis))
384 })
385 .collect();
386 if dependents.is_empty() {
387 return Vec::new();
388 }
389
390 let dep_files: Vec<Arc<str>> = dependents
394 .iter()
395 .map(|(u, _)| Arc::from(u.as_str()))
396 .collect();
397 let class_issues = session.class_issues(&dep_files);
398 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
399 std::collections::HashMap::new();
400 for issue in class_issues {
401 if issue.suppressed {
402 continue;
403 }
404 let file = issue.location.file.clone();
405 class_issues_by_file.entry(file).or_default().push(issue);
406 }
407
408 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
409 for (url, analysis) in dependents {
410 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
411 let mut issues: Vec<mir_issues::Issue> = analysis
412 .issues
413 .into_iter()
414 .filter(|i| !i.suppressed)
415 .collect();
416 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
417 issues.extend(extra);
418 }
419 let semantic =
420 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
421 out.push((url, merge_file_diagnostics(parse, semantic)));
422 }
423 out
424 })
425 .await
426 .unwrap_or_default()
427}
428
429pub(super) fn diagnostics_content_hash(diagnostics: &[Diagnostic]) -> u64 {
432 use std::collections::hash_map::DefaultHasher;
433 use std::hash::{Hash, Hasher};
434 let mut hasher = DefaultHasher::new();
435 serde_json::to_string(diagnostics)
436 .unwrap_or_default()
437 .hash(&mut hasher);
438 hasher.finish()
439}
440
441pub(super) async fn publish_with_dependents(
450 client: Client,
451 docs: Arc<DocumentStore>,
452 open_files: OpenFiles,
453 uri: Url,
454 diag_cfg: crate::lang::config::DiagnosticsConfig,
455) {
456 let docs_ref = Arc::clone(&docs);
457 let open_files_ref = open_files.clone();
458 let uri_ref = uri.clone();
459 let diag_cfg_ref = diag_cfg.clone();
460 let all_diags = tokio::task::spawn_blocking(move || {
461 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
462 })
463 .await
464 .unwrap_or_default();
465 open_files.note_published(&uri, diagnostics_content_hash(&all_diags));
466 client
467 .publish_diagnostics(uri.clone(), all_diags, None)
468 .await;
469 let dependents =
470 compute_dependent_publishes_owned(docs, open_files.clone(), uri, diag_cfg).await;
471 for (dep_uri, dep_diags) in dependents {
472 let hash = diagnostics_content_hash(&dep_diags);
473 if open_files.published_hash(&dep_uri) == Some(hash) {
474 continue;
475 }
476 open_files.note_published(&dep_uri, hash);
477 client.publish_diagnostics(dep_uri, dep_diags, None).await;
478 }
479}
480
481fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
484 use std::collections::hash_map::DefaultHasher;
485 use std::hash::{Hash, Hasher};
486
487 let mut hasher = DefaultHasher::new();
488 uri.hash(&mut hasher);
489 diagnostics.len().hash(&mut hasher);
490
491 for diag in diagnostics {
492 diag.range.start.line.hash(&mut hasher);
493 diag.range.start.character.hash(&mut hasher);
494 diag.range.end.line.hash(&mut hasher);
495 diag.range.end.character.hash(&mut hasher);
496 diag.message.hash(&mut hasher);
497 let severity_val = match diag.severity {
498 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
499 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
500 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
501 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
502 None => 0,
503 _ => 5, };
505 severity_val.hash(&mut hasher);
506 if let Some(code) = &diag.code {
507 format!("{:?}", code).hash(&mut hasher);
508 }
509 if let Some(source) = &diag.source {
510 source.hash(&mut hasher);
511 }
512 if let Some(tags) = &diag.tags {
513 for tag in tags {
514 let tag_val = match *tag {
515 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
516 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
517 _ => 3,
518 };
519 tag_val.hash(&mut hasher);
520 }
521 }
522 }
523
524 format!("v1:{:x}", hasher.finish())
525}
526
527mod handlers;
528mod helpers;
529pub mod panic_guard;
530mod server;
531#[cfg(test)]
532mod tests;