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