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}
25
26use crate::document::ast::ParsedDoc;
27use crate::document::document_store::DocumentStore;
28use crate::document::open_files::OpenFiles;
29use crate::lang::autoload::Psr4Map;
30use crate::lang::config::LspConfig;
31use crate::lang::phpstorm_meta::PhpStormMeta;
32use crate::text::fqn_short_name;
33
34use crate::navigation::references::find_constructor_references;
35
36use crate::analysis::diagnostics::merge_file_diagnostics;
37use crate::document::open_files::compute_open_file_diagnostics;
38
39pub struct Backend {
40 client: Client,
41 docs: Arc<DocumentStore>,
42 open_files: OpenFiles,
46 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
47 psr4: Arc<ArcSwap<Psr4Map>>,
48 meta: Arc<ArcSwap<PhpStormMeta>>,
49 config: Arc<ArcSwap<LspConfig>>,
50}
51
52impl Backend {
53 pub fn new(client: Client) -> Self {
54 let docs = Arc::new(DocumentStore::new());
59 let psr4 = docs.psr4_arc();
60 Backend {
61 client,
62 docs,
63 open_files: OpenFiles::new(),
64 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
65 psr4,
66 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
67 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
68 }
69 }
70
71 pub async fn debug_stats(&self) -> tower_lsp::jsonrpc::Result<DebugStats> {
75 Ok(DebugStats {
76 parses: self.docs.parse_count(),
77 })
78 }
79
80 fn set_open_text(&self, uri: Url, text: String) -> u64 {
81 self.open_files.set_open_text(&self.docs, uri, text)
82 }
83
84 fn close_open_file(&self, uri: &Url) {
85 self.open_files.close(&self.docs, uri);
86 }
87
88 fn ingest_if_not_open(&self, uri: Url, text: &str) {
92 if !self.open_files.contains(&uri) {
93 self.docs.ingest(uri, text);
94 }
95 }
96
97 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
99 if !self.open_files.contains(&uri) {
100 self.docs.ingest_from_doc(uri, doc);
101 }
102 }
103
104 fn get_open_text(&self, uri: &Url) -> Option<String> {
105 self.open_files.text(uri)
106 }
107
108 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
109 self.open_files.set_parse_diagnostics(uri, diagnostics);
110 }
111
112 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
113 self.open_files.parse_diagnostics(uri)
114 }
115
116 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
117 self.open_files.all_with_diagnostics()
118 }
119
120 fn open_urls(&self) -> Vec<Url> {
121 self.open_files.urls()
122 }
123
124 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
125 self.open_files.get_doc(&self.docs, uri)
126 }
127
128 fn get_doc_stale(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
129 self.open_files.get_doc_stale(&self.docs, uri)
130 }
131
132 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
134 self.docs
135 .get_doc_salsa(uri)
136 .map(|doc| crate::references::collect_file_imports(&doc))
137 .unwrap_or_default()
138 }
139
140 fn construct_references(
153 &self,
154 uri: &Url,
155 source: &str,
156 position: Position,
157 class_name: &str,
158 include_declaration: bool,
159 ) -> Vec<Location> {
160 let short_name = fqn_short_name(class_name).to_owned();
161 let class_fqn = class_name.contains('\\').then_some(class_name);
162 let candidate_docs = self.docs.candidate_docs_for(&short_name);
164 let mut locations = find_constructor_references(&short_name, &candidate_docs, class_fqn);
168 if include_declaration && let Some(range) = crate::text::word_range_at(source, position) {
173 locations.push(Location {
174 uri: uri.clone(),
175 range,
176 });
177 }
178 locations
179 }
180
181 fn resolve_reference_target_fqn(
187 &self,
188 uri: &Url,
189 doc_opt: Option<&Arc<ParsedDoc>>,
190 word: &str,
191 kind: Option<crate::navigation::references::SymbolKind>,
192 position: Position,
193 constant_owner: Option<String>,
194 ) -> Option<String> {
195 use crate::navigation::references::SymbolKind;
196 let doc = doc_opt?;
197 let imports = self.file_imports(uri);
198 match kind {
199 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
200 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
201 resolved.contains('\\').then_some(resolved)
202 }
203 Some(SymbolKind::Method) => {
204 let short_owner =
206 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
207 Some(crate::navigation::moniker::resolve_fqn(
209 doc,
210 &short_owner,
211 &imports,
212 ))
213 }
214 Some(SymbolKind::Property) => {
215 let stmts = &doc.program().stmts;
221 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
222 let short_owner =
223 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
224 Some(crate::navigation::moniker::resolve_fqn(
225 doc,
226 &short_owner,
227 &imports,
228 ))
229 }
230 Some(SymbolKind::Constant) => {
231 if constant_owner.is_some() {
232 constant_owner
234 } else {
235 let fqn = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
238 fqn.contains('\\').then_some(fqn)
239 }
240 }
241 _ => None,
242 }
243 }
244
245 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
248 let roots = self.root_paths.load();
249 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
250 }
251}
252
253fn resolve_reference_symbol(
262 doc_opt: Option<&Arc<ParsedDoc>>,
263 source: &str,
264 position: Position,
265 word: String,
266) -> (
267 String,
268 Option<crate::navigation::references::SymbolKind>,
269 Option<String>,
270) {
271 use crate::navigation::references::SymbolKind;
272 let mut constant_owner: Option<String> = None;
273 let (word, kind) = if let Some(doc) = doc_opt
274 && let Some(prop_name) =
275 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
276 {
277 (prop_name, Some(SymbolKind::Property))
278 } else if let Some(doc) = doc_opt {
279 let stmts = &doc.program().stmts;
280 if cursor_is_on_method_decl(doc.source(), stmts, position) {
281 (word, Some(SymbolKind::Method))
282 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
283 (prop_name, Some(SymbolKind::Property))
284 } else if let Some((const_name, owner)) =
285 cursor_is_on_constant_decl(doc.source(), stmts, position)
286 {
287 constant_owner = owner;
288 (const_name, Some(SymbolKind::Constant))
289 } else {
290 let k = symbol_kind_at(source, position, &word);
291 if matches!(k, Some(SymbolKind::Constant))
296 && let Some(raw) = class_before_double_colon(source, position)
297 {
298 constant_owner = Some(match raw.as_str() {
299 "self" | "static" => {
300 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
301 .unwrap_or(raw)
302 }
303 _ => raw,
304 });
305 }
306 (word, k)
307 }
308 } else {
309 let k = symbol_kind_at(source, position, &word);
310 (word, k)
311 };
312 (word, kind, constant_owner)
313}
314
315fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
323 let line = source.lines().nth(position.line as usize)?;
324 let chars: Vec<char> = line.chars().collect();
325 let col = position.character as usize;
326
327 let mut utf16_col = 0usize;
328 let mut char_idx = 0usize;
329 for ch in &chars {
330 if utf16_col >= col {
331 break;
332 }
333 utf16_col += ch.len_utf16();
334 char_idx += 1;
335 }
336
337 let is_word = |c: char| c.is_alphanumeric() || c == '_';
338 while char_idx > 0 && is_word(chars[char_idx - 1]) {
339 char_idx -= 1;
340 }
341
342 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
343 return None;
344 }
345
346 let class_end = char_idx - 2;
347 let mut class_start = class_end;
348 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
349 class_start -= 1;
350 }
351
352 let name: String = chars[class_start..class_end].iter().collect();
353 if name.is_empty() { None } else { Some(name) }
354}
355
356async fn compute_dependent_publishes_owned(
361 docs: Arc<DocumentStore>,
362 open_files: OpenFiles,
363 changed_uri: Url,
364 diag_cfg: crate::lang::config::DiagnosticsConfig,
365) -> Vec<(Url, Vec<Diagnostic>)> {
366 tokio::task::spawn_blocking(move || {
367 let php_version = docs.workspace_php_version();
375 let session = docs.analysis_session(php_version);
376 let cancel = docs.begin_reanalyze();
380 let analyses = session.reanalyze_dependents_cancellable(changed_uri.as_str(), &cancel);
381 if cancel.is_cancelled() || analyses.is_empty() {
385 return Vec::new();
386 }
387
388 let open_urls: std::collections::HashSet<Url> = open_files
391 .urls()
392 .into_iter()
393 .filter(|u| u != &changed_uri)
394 .collect();
395 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
396 .into_iter()
397 .filter_map(|(file, analysis)| {
398 let url = Url::parse(file.as_ref()).ok()?;
399 open_urls.contains(&url).then_some((url, analysis))
400 })
401 .collect();
402 if dependents.is_empty() {
403 return Vec::new();
404 }
405
406 let dep_files: Vec<Arc<str>> = dependents
410 .iter()
411 .map(|(u, _)| Arc::from(u.as_str()))
412 .collect();
413 let class_issues = session.class_issues(&dep_files);
414 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
415 std::collections::HashMap::new();
416 for issue in class_issues {
417 if issue.suppressed {
418 continue;
419 }
420 let file = issue.location.file.clone();
421 class_issues_by_file.entry(file).or_default().push(issue);
422 }
423
424 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
425 for (url, analysis) in dependents {
426 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
427 let mut issues: Vec<mir_issues::Issue> = analysis
428 .issues
429 .into_iter()
430 .filter(|i| !i.suppressed)
431 .collect();
432 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
433 issues.extend(extra);
434 }
435 let semantic =
436 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
437 out.push((url, merge_file_diagnostics(parse, semantic)));
438 }
439 out
440 })
441 .await
442 .unwrap_or_default()
443}
444
445pub(super) async fn publish_with_dependents(
449 client: Client,
450 docs: Arc<DocumentStore>,
451 open_files: OpenFiles,
452 uri: Url,
453 diag_cfg: crate::lang::config::DiagnosticsConfig,
454) {
455 let docs_ref = Arc::clone(&docs);
456 let open_files_ref = open_files.clone();
457 let uri_ref = uri.clone();
458 let diag_cfg_ref = diag_cfg.clone();
459 let all_diags = tokio::task::spawn_blocking(move || {
460 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
461 })
462 .await
463 .unwrap_or_default();
464 client
465 .publish_diagnostics(uri.clone(), all_diags, None)
466 .await;
467 let dependents = compute_dependent_publishes_owned(docs, open_files, uri, diag_cfg).await;
468 for (dep_uri, dep_diags) in dependents {
469 client.publish_diagnostics(dep_uri, dep_diags, None).await;
470 }
471}
472
473fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
476 use std::collections::hash_map::DefaultHasher;
477 use std::hash::{Hash, Hasher};
478
479 let mut hasher = DefaultHasher::new();
480 uri.hash(&mut hasher);
481 diagnostics.len().hash(&mut hasher);
482
483 for diag in diagnostics {
484 diag.range.start.line.hash(&mut hasher);
485 diag.range.start.character.hash(&mut hasher);
486 diag.range.end.line.hash(&mut hasher);
487 diag.range.end.character.hash(&mut hasher);
488 diag.message.hash(&mut hasher);
489 let severity_val = match diag.severity {
490 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
491 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
492 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
493 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
494 None => 0,
495 _ => 5, };
497 severity_val.hash(&mut hasher);
498 if let Some(code) = &diag.code {
499 format!("{:?}", code).hash(&mut hasher);
500 }
501 if let Some(source) = &diag.source {
502 source.hash(&mut hasher);
503 }
504 if let Some(tags) = &diag.tags {
505 for tag in tags {
506 let tag_val = match *tag {
507 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
508 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
509 _ => 3,
510 };
511 tag_val.hash(&mut hasher);
512 }
513 }
514 }
515
516 format!("v1:{:x}", hasher.finish())
517}
518
519mod handlers;
520mod helpers;
521pub mod panic_guard;
522mod server;
523#[cfg(test)]
524mod tests;