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
19use crate::document::ast::ParsedDoc;
20use crate::document::document_store::DocumentStore;
21use crate::document::open_files::OpenFiles;
22use crate::lang::autoload::Psr4Map;
23use crate::lang::config::LspConfig;
24use crate::lang::phpstorm_meta::PhpStormMeta;
25use crate::text::fqn_short_name;
26
27use crate::navigation::references::find_constructor_references;
28
29use crate::analysis::diagnostics::merge_file_diagnostics;
30use crate::document::open_files::compute_open_file_diagnostics;
31
32pub struct Backend {
33 client: Client,
34 docs: Arc<DocumentStore>,
35 open_files: OpenFiles,
39 root_paths: Arc<ArcSwap<Vec<PathBuf>>>,
40 psr4: Arc<ArcSwap<Psr4Map>>,
41 meta: Arc<ArcSwap<PhpStormMeta>>,
42 config: Arc<ArcSwap<LspConfig>>,
43}
44
45impl Backend {
46 pub fn new(client: Client) -> Self {
47 let docs = Arc::new(DocumentStore::new());
52 let psr4 = docs.psr4_arc();
53 Backend {
54 client,
55 docs,
56 open_files: OpenFiles::new(),
57 root_paths: Arc::new(ArcSwap::from_pointee(Vec::new())),
58 psr4,
59 meta: Arc::new(ArcSwap::from_pointee(PhpStormMeta::default())),
60 config: Arc::new(ArcSwap::from_pointee(LspConfig::default())),
61 }
62 }
63
64 fn set_open_text(&self, uri: Url, text: String) -> u64 {
65 self.open_files.set_open_text(&self.docs, uri, text)
66 }
67
68 fn close_open_file(&self, uri: &Url) {
69 self.open_files.close(&self.docs, uri);
70 }
71
72 fn ingest_if_not_open(&self, uri: Url, text: &str) {
76 if !self.open_files.contains(&uri) {
77 self.docs.ingest(uri, text);
78 }
79 }
80
81 fn ingest_from_doc_if_not_open(&self, uri: Url, doc: &ParsedDoc) {
83 if !self.open_files.contains(&uri) {
84 self.docs.ingest_from_doc(uri, doc);
85 }
86 }
87
88 fn get_open_text(&self, uri: &Url) -> Option<String> {
89 self.open_files.text(uri)
90 }
91
92 fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
93 self.open_files.set_parse_diagnostics(uri, diagnostics);
94 }
95
96 fn get_parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
97 self.open_files.parse_diagnostics(uri)
98 }
99
100 fn all_open_files_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
101 self.open_files.all_with_diagnostics()
102 }
103
104 fn open_urls(&self) -> Vec<Url> {
105 self.open_files.urls()
106 }
107
108 fn get_doc(&self, uri: &Url) -> Option<Arc<ParsedDoc>> {
109 self.open_files.get_doc(&self.docs, uri)
110 }
111
112 fn file_imports(&self, uri: &Url) -> std::collections::HashMap<String, String> {
114 self.docs
115 .get_doc_salsa(uri)
116 .map(|doc| crate::references::collect_file_imports(&doc))
117 .unwrap_or_default()
118 }
119
120 fn construct_references(
133 &self,
134 uri: &Url,
135 source: &str,
136 position: Position,
137 class_name: &str,
138 include_declaration: bool,
139 ) -> Vec<Location> {
140 let short_name = fqn_short_name(class_name).to_owned();
141 let class_fqn = class_name.contains('\\').then_some(class_name);
142 let candidate_docs = self.docs.candidate_docs_for(&short_name);
144 let mut locations = find_constructor_references(&short_name, &candidate_docs, class_fqn);
148 if include_declaration && let Some(range) = crate::text::word_range_at(source, position) {
153 locations.push(Location {
154 uri: uri.clone(),
155 range,
156 });
157 }
158 locations
159 }
160
161 fn resolve_reference_target_fqn(
167 &self,
168 uri: &Url,
169 doc_opt: Option<&Arc<ParsedDoc>>,
170 word: &str,
171 kind: Option<crate::navigation::references::SymbolKind>,
172 position: Position,
173 constant_owner: Option<String>,
174 ) -> Option<String> {
175 use crate::navigation::references::SymbolKind;
176 let doc = doc_opt?;
177 let imports = self.file_imports(uri);
178 match kind {
179 Some(SymbolKind::Function) | Some(SymbolKind::Class) => {
180 let resolved = crate::navigation::moniker::resolve_fqn(doc, word, &imports);
181 resolved.contains('\\').then_some(resolved)
182 }
183 Some(SymbolKind::Method) => {
184 let short_owner =
186 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
187 Some(crate::navigation::moniker::resolve_fqn(
189 doc,
190 &short_owner,
191 &imports,
192 ))
193 }
194 Some(SymbolKind::Property) => {
195 let stmts = &doc.program().stmts;
201 crate::backend::helpers::cursor_is_on_property_decl(doc.source(), stmts, position)?;
202 let short_owner =
203 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)?;
204 Some(crate::navigation::moniker::resolve_fqn(
205 doc,
206 &short_owner,
207 &imports,
208 ))
209 }
210 Some(SymbolKind::Constant) => {
211 if constant_owner.is_some() {
212 constant_owner
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 session_method_references(
238 &self,
239 word: &str,
240 kind: Option<crate::navigation::references::SymbolKind>,
241 target_fqn: Option<&str>,
242 owner_short: Option<&str>,
243 ) -> Option<Vec<Location>> {
244 if !matches!(
245 kind,
246 Some(crate::navigation::references::SymbolKind::Method)
247 ) {
248 return None;
249 }
250 let sym = build_mir_symbol(word, kind, target_fqn)?;
251 let locs = self
252 .docs
253 .session_references_to(&sym)
254 .into_iter()
255 .filter_map(|tuple| {
256 let loc = crate::references::session_tuple_to_location(tuple)?;
257 if let Some(short) = owner_short {
258 let mentions = self
259 .docs
260 .source_text(&loc.uri)
261 .as_ref()
262 .map(|src| src.contains(short))
263 .unwrap_or(true);
264 if !mentions {
265 return None;
266 }
267 }
268 Some(loc)
269 })
270 .collect();
271 Some(locs)
272 }
273
274 fn resolve_php_version(&self, explicit: Option<&str>) -> (String, &'static str) {
277 let roots = self.root_paths.load();
278 crate::lang::autoload::resolve_php_version_from_roots(&roots, explicit)
279 }
280}
281
282fn build_mir_symbol(
288 word: &str,
289 kind: Option<crate::navigation::references::SymbolKind>,
290 target_fqn: Option<&str>,
291) -> Option<mir_analyzer::Name> {
292 use crate::navigation::references::SymbolKind;
293 use std::sync::Arc as StdArc;
294 match kind {
295 Some(SymbolKind::Function) => {
296 target_fqn.map(|fqn| mir_analyzer::Name::Function(StdArc::from(fqn)))
297 }
298 Some(SymbolKind::Class) => {
299 target_fqn.map(|fqn| mir_analyzer::Name::Class(StdArc::from(fqn)))
300 }
301 Some(SymbolKind::Method) => target_fqn.map(|owning| mir_analyzer::Name::Method {
302 class: StdArc::from(owning),
303 name: StdArc::from(word.to_ascii_lowercase()),
306 }),
307 Some(SymbolKind::Property) => target_fqn.map(|owning| mir_analyzer::Name::Property {
308 class: StdArc::from(owning),
309 name: StdArc::from(word),
310 }),
311 Some(SymbolKind::Constant) | None => None,
312 }
313}
314
315fn resolve_reference_symbol(
324 doc_opt: Option<&Arc<ParsedDoc>>,
325 source: &str,
326 position: Position,
327 word: String,
328) -> (
329 String,
330 Option<crate::navigation::references::SymbolKind>,
331 Option<String>,
332) {
333 use crate::navigation::references::SymbolKind;
334 let mut constant_owner: Option<String> = None;
335 let (word, kind) = if let Some(doc) = doc_opt
336 && let Some(prop_name) =
337 promoted_property_at_cursor(doc.source(), &doc.program().stmts, position)
338 {
339 (prop_name, Some(SymbolKind::Property))
340 } else if let Some(doc) = doc_opt {
341 let stmts = &doc.program().stmts;
342 if cursor_is_on_method_decl(doc.source(), stmts, position) {
343 (word, Some(SymbolKind::Method))
344 } else if let Some(prop_name) = cursor_is_on_property_decl(doc.source(), stmts, position) {
345 (prop_name, Some(SymbolKind::Property))
346 } else if let Some((const_name, owner)) =
347 cursor_is_on_constant_decl(doc.source(), stmts, position)
348 {
349 constant_owner = owner;
350 (const_name, Some(SymbolKind::Constant))
351 } else {
352 let k = symbol_kind_at(source, position, &word);
353 if matches!(k, Some(SymbolKind::Constant))
358 && let Some(raw) = class_before_double_colon(source, position)
359 {
360 constant_owner = Some(match raw.as_str() {
361 "self" | "static" => {
362 crate::types::type_map::enclosing_class_at(doc.source(), doc, position)
363 .unwrap_or(raw)
364 }
365 _ => raw,
366 });
367 }
368 (word, k)
369 }
370 } else {
371 let k = symbol_kind_at(source, position, &word);
372 (word, k)
373 };
374 (word, kind, constant_owner)
375}
376
377fn class_before_double_colon(source: &str, position: Position) -> Option<String> {
385 let line = source.lines().nth(position.line as usize)?;
386 let chars: Vec<char> = line.chars().collect();
387 let col = position.character as usize;
388
389 let mut utf16_col = 0usize;
390 let mut char_idx = 0usize;
391 for ch in &chars {
392 if utf16_col >= col {
393 break;
394 }
395 utf16_col += ch.len_utf16();
396 char_idx += 1;
397 }
398
399 let is_word = |c: char| c.is_alphanumeric() || c == '_';
400 while char_idx > 0 && is_word(chars[char_idx - 1]) {
401 char_idx -= 1;
402 }
403
404 if char_idx < 2 || chars[char_idx - 1] != ':' || chars[char_idx - 2] != ':' {
405 return None;
406 }
407
408 let class_end = char_idx - 2;
409 let mut class_start = class_end;
410 while class_start > 0 && (is_word(chars[class_start - 1]) || chars[class_start - 1] == '\\') {
411 class_start -= 1;
412 }
413
414 let name: String = chars[class_start..class_end].iter().collect();
415 if name.is_empty() { None } else { Some(name) }
416}
417
418async fn compute_dependent_publishes_owned(
423 docs: Arc<DocumentStore>,
424 open_files: OpenFiles,
425 changed_uri: Url,
426 diag_cfg: crate::lang::config::DiagnosticsConfig,
427) -> Vec<(Url, Vec<Diagnostic>)> {
428 tokio::task::spawn_blocking(move || {
429 let php_version = docs.workspace_php_version();
437 let session = docs.analysis_session(php_version);
438 let analyses = session.reanalyze_dependents(changed_uri.as_str());
439 if analyses.is_empty() {
440 return Vec::new();
441 }
442
443 let open_urls: std::collections::HashSet<Url> = open_files
446 .urls()
447 .into_iter()
448 .filter(|u| u != &changed_uri)
449 .collect();
450 let dependents: Vec<(Url, mir_analyzer::FileAnalysis)> = analyses
451 .into_iter()
452 .filter_map(|(file, analysis)| {
453 let url = Url::parse(file.as_ref()).ok()?;
454 open_urls.contains(&url).then_some((url, analysis))
455 })
456 .collect();
457 if dependents.is_empty() {
458 return Vec::new();
459 }
460
461 let dep_files: Vec<Arc<str>> = dependents
465 .iter()
466 .map(|(u, _)| Arc::from(u.as_str()))
467 .collect();
468 let class_issues = session.class_issues(&dep_files);
469 let mut class_issues_by_file: std::collections::HashMap<Arc<str>, Vec<mir_issues::Issue>> =
470 std::collections::HashMap::new();
471 for issue in class_issues {
472 if issue.suppressed {
473 continue;
474 }
475 let file = issue.location.file.clone();
476 class_issues_by_file.entry(file).or_default().push(issue);
477 }
478
479 let mut out: Vec<(Url, Vec<Diagnostic>)> = Vec::with_capacity(dependents.len());
480 for (url, analysis) in dependents {
481 let parse = open_files.parse_diagnostics(&url).unwrap_or_default();
482 let mut issues: Vec<mir_issues::Issue> = analysis
483 .issues
484 .into_iter()
485 .filter(|i| !i.suppressed)
486 .collect();
487 if let Some(extra) = class_issues_by_file.remove(&Arc::<str>::from(url.as_str())) {
488 issues.extend(extra);
489 }
490 let semantic =
491 crate::semantic_diagnostics::issues_to_diagnostics(&issues, &url, &diag_cfg);
492 out.push((url, merge_file_diagnostics(parse, semantic)));
493 }
494 out
495 })
496 .await
497 .unwrap_or_default()
498}
499
500pub(super) async fn publish_with_dependents(
504 client: Client,
505 docs: Arc<DocumentStore>,
506 open_files: OpenFiles,
507 uri: Url,
508 diag_cfg: crate::lang::config::DiagnosticsConfig,
509) {
510 let docs_ref = Arc::clone(&docs);
511 let open_files_ref = open_files.clone();
512 let uri_ref = uri.clone();
513 let diag_cfg_ref = diag_cfg.clone();
514 let all_diags = tokio::task::spawn_blocking(move || {
515 compute_open_file_diagnostics(&docs_ref, &open_files_ref, &uri_ref, &diag_cfg_ref)
516 })
517 .await
518 .unwrap_or_default();
519 client
520 .publish_diagnostics(uri.clone(), all_diags, None)
521 .await;
522 let dependents = compute_dependent_publishes_owned(docs, open_files, uri, diag_cfg).await;
523 for (dep_uri, dep_diags) in dependents {
524 client.publish_diagnostics(dep_uri, dep_diags, None).await;
525 }
526}
527
528fn compute_diagnostic_result_id(diagnostics: &[Diagnostic], uri: &str) -> String {
531 use std::collections::hash_map::DefaultHasher;
532 use std::hash::{Hash, Hasher};
533
534 let mut hasher = DefaultHasher::new();
535 uri.hash(&mut hasher);
536 diagnostics.len().hash(&mut hasher);
537
538 for diag in diagnostics {
539 diag.range.start.line.hash(&mut hasher);
540 diag.range.start.character.hash(&mut hasher);
541 diag.range.end.line.hash(&mut hasher);
542 diag.range.end.character.hash(&mut hasher);
543 diag.message.hash(&mut hasher);
544 let severity_val = match diag.severity {
545 Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR) => 1,
546 Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING) => 2,
547 Some(tower_lsp::lsp_types::DiagnosticSeverity::INFORMATION) => 3,
548 Some(tower_lsp::lsp_types::DiagnosticSeverity::HINT) => 4,
549 None => 0,
550 _ => 5, };
552 severity_val.hash(&mut hasher);
553 if let Some(code) = &diag.code {
554 format!("{:?}", code).hash(&mut hasher);
555 }
556 if let Some(source) = &diag.source {
557 source.hash(&mut hasher);
558 }
559 if let Some(tags) = &diag.tags {
560 for tag in tags {
561 let tag_val = match *tag {
562 tower_lsp::lsp_types::DiagnosticTag::UNNECESSARY => 1,
563 tower_lsp::lsp_types::DiagnosticTag::DEPRECATED => 2,
564 _ => 3,
565 };
566 tag_val.hash(&mut hasher);
567 }
568 }
569 }
570
571 format!("v1:{:x}", hasher.finish())
572}
573
574mod handlers;
575mod helpers;
576pub mod panic_guard;
577mod server;
578#[cfg(test)]
579mod tests;