1use std::sync::Arc;
2
3use super::*;
4
5impl AnalysisSession {
6 pub fn analyze_file_diagnostics(&mut self, file: &str, source: &str) -> crate::FileAnalysis {
13 let file: Arc<str> = Arc::from(file);
14 self.ingest_file(file.clone(), Arc::from(source));
15
16 let (mut issues, parsed) = {
19 let view = self.db_view();
20 let db = view.db();
21 let Some(sf) = db.lookup_source_file(file.as_ref()) else {
22 return crate::FileAnalysis {
23 issues: Vec::new(),
24 symbols: Vec::new(),
25 };
26 };
27 let defs = crate::db::collect_file_definitions(db, sf);
28 let prepared = crate::db::prepare_analysis_file(db, sf);
29 let parsed = (!prepared.has_hard_parse_errors)
30 .then(|| (prepared.text.clone(), prepared.parsed.0.clone()));
31 (Arc::unwrap_or_clone(defs.issues.clone()), parsed)
32 };
33
34 if let Some((text, parsed)) = parsed {
35 let analysis = crate::FileAnalyzer::new(self).analyze_diagnostics_only(
36 file.clone(),
37 text.as_ref(),
38 &parsed.program,
39 &parsed.source_map,
40 );
41 issues.extend(analysis.issues);
42 }
43
44 self.apply_suppressions_and_emit_unused(&mut issues, std::slice::from_ref(&file));
45 crate::FileAnalysis {
46 issues,
47 symbols: Vec::new(),
48 }
49 }
50
51 pub fn name_at(&mut self, file: &str, byte_offset: u32) -> Option<crate::Name> {
57 crate::FileAnalyzer::new(self).name_at(Arc::from(file), byte_offset)
58 }
59
60 pub fn outgoing_calls(
69 &mut self,
70 file: &str,
71 byte_offset: u32,
72 ) -> Vec<(crate::Name, crate::Range)> {
73 crate::FileAnalyzer::new(self).outgoing_calls(Arc::from(file), byte_offset)
74 }
75
76 pub fn symbol_at(&mut self, file: &str, byte_offset: u32) -> Option<crate::ResolvedSymbol> {
85 crate::FileAnalyzer::new(self).symbol_at(Arc::from(file), byte_offset)
86 }
87
88 pub fn definition_of(
101 &mut self,
102 symbol: &crate::Name,
103 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
104 self.load_symbol_owner(symbol);
105 self.definition_of_cached(symbol)
106 }
107
108 fn load_symbol_owner(&mut self, symbol: &crate::Name) {
111 match symbol {
112 crate::Name::Class(fqn) | crate::Name::Function(fqn) => {
113 let _ = self.load_class(fqn.as_ref());
114 }
115 crate::Name::Method { class, .. }
116 | crate::Name::Property { class, .. }
117 | crate::Name::ClassConstant { class, .. } => {
118 let _ = self.load_class(class.as_ref());
119 }
120 crate::Name::GlobalConstant(_) => {}
121 }
122 }
123
124 pub fn definition_of_cached(
128 &self,
129 symbol: &crate::Name,
130 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
131 self.query_snapshot(|snap| snap.definition_of_cached(symbol))
132 }
133
134 #[doc(hidden)]
138 pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
139 use crate::db::MirDatabase;
140 let db = self.snapshot_db();
141 db.reference_locations(symbol)
142 }
143
144 pub fn subtype_files(&mut self, class_fqn: &str, include_trait_users: bool) -> Vec<Arc<str>> {
156 self.prepare_for_query(None);
157 self.query_snapshot(|snap| snap.subtype_files(class_fqn, include_trait_users))
158 }
159
160 pub fn indexed_use_import_locations(
165 &mut self,
166 symbol: &crate::Name,
167 files: &[Arc<str>],
168 ) -> Vec<(Arc<str>, crate::Range)> {
169 self.indexed_references_to(
170 symbol,
171 files,
172 false,
173 crate::ReferenceIncludes::UseImports,
174 &|| false,
175 )
176 .unwrap_or_default()
177 }
178
179 pub fn indexed_references_to(
187 &mut self,
188 symbol: &crate::Name,
189 files: &[Arc<str>],
190 include_declaration: bool,
191 includes: crate::ReferenceIncludes,
192 should_cancel: &(dyn Fn() -> bool + Sync),
193 ) -> Option<Vec<(Arc<str>, crate::Range)>> {
194 if self.db.salsa.needs_index_settle()
198 && !self.settle_workspace_index_cancellable(should_cancel)
199 {
200 return None;
201 }
202 let key = {
206 let snap = self.db_view();
207 let key = snap.reference_query_key(symbol, files, include_declaration, includes);
208 if let Some(hit) = self.index.ref_queries.get(&key) {
209 return Some(hit);
210 }
211 key
212 };
213 if should_cancel() {
214 return None;
215 }
216 let stale = self
217 .db_view()
218 .stale_reference_candidates_unguarded(symbol, files);
219
220 if !stale.is_empty() {
221 if !self.prepare_stale_candidates(&stale, should_cancel) {
222 return None;
223 }
224 self.query_snapshot(|snap| snap.commit_reference_candidates(&stale));
225 }
226
227 if include_declaration {
228 self.load_symbol_owner(symbol);
229 }
230 let snap = self.db_view();
231 let out = snap.read_references(symbol, files, include_declaration, includes);
232 snap.memoize_references(key, &out);
233 Some(out)
234 }
235
236 pub fn prepare_references_query(
252 &mut self,
253 symbol: &crate::Name,
254 stale: &[Arc<str>],
255 include_declaration: bool,
256 should_cancel: &(dyn Fn() -> bool + Sync),
257 ) -> bool {
258 if self.db.salsa.needs_index_settle()
259 && !self.settle_workspace_index_cancellable(should_cancel)
260 {
261 return false;
262 }
263 if !self.prepare_stale_candidates(stale, should_cancel) {
264 return false;
265 }
266 if include_declaration {
267 self.load_symbol_owner(symbol);
268 }
269 true
270 }
271
272 fn prepare_stale_candidates(
276 &mut self,
277 stale: &[Arc<str>],
278 should_cancel: &(dyn Fn() -> bool + Sync),
279 ) -> bool {
280 {
281 let mut session = self.defer_revision_bumps();
282 for path in stale {
283 if !session.prepare_file_for_analysis_cancellable(path, should_cancel) {
284 return false;
285 }
286 }
287 }
288 !should_cancel()
289 }
290
291 pub fn declaration_name_range(
295 &mut self,
296 symbol: &crate::Name,
297 ) -> Option<(Arc<str>, crate::Range)> {
298 self.load_symbol_owner(symbol);
299 self.db_view().declaration_name_range(symbol)
300 }
301
302 pub fn indexed_subtype_classes(
304 &mut self,
305 class_fqn: &str,
306 files: &[Arc<str>],
307 include_trait_users: bool,
308 ) -> Vec<SubtypeClassSite> {
309 self.prepare_for_query(None);
310 self.query_snapshot(|snap| {
311 snap.indexed_subtype_classes(class_fqn, files, include_trait_users)
312 })
313 }
314
315 pub fn indexed_direct_subtype_classes(
317 &mut self,
318 class_fqn: &str,
319 files: &[Arc<str>],
320 include_trait_users: bool,
321 ) -> Vec<SubtypeClassSite> {
322 self.prepare_for_query(None);
323 self.query_snapshot(|snap| {
324 snap.indexed_direct_subtype_classes(class_fqn, files, include_trait_users)
325 })
326 }
327
328 pub fn indexed_method_implementations(
330 &mut self,
331 class_fqn: &str,
332 method: &str,
333 files: &[Arc<str>],
334 ) -> Vec<(Arc<str>, Arc<str>, crate::Range)> {
335 self.prepare_for_query(None);
336 self.query_snapshot(|snap| snap.indexed_method_implementations(class_fqn, method, files))
337 }
338
339 pub fn class_issues(&mut self, files: &[Arc<str>]) -> Vec<crate::Issue> {
343 self.prepare_for_query(None);
344 self.query_snapshot(|snap| snap.class_issues(files))
345 }
346
347 pub fn collector_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
351 self.query_snapshot(|snap| snap.collector_issues(files))
352 }
353
354 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
360 use crate::symbol::{DeclarationKind, DocumentSymbol};
361
362 let db = self.snapshot_db();
363 let Some(sf) = db.lookup_source_file(file) else {
364 return Vec::new();
365 };
366 let defs = crate::db::collect_file_definitions(&db, sf);
367 let mut out: Vec<DocumentSymbol> = Vec::new();
368
369 let class_children = |methods: &mir_codebase::definitions::MemberMap<
370 Arc<mir_codebase::definitions::MethodDef>,
371 >,
372 props: Option<
373 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
374 >,
375 consts: &mir_codebase::definitions::MemberMap<
376 mir_codebase::definitions::ConstantDef,
377 >,
378 is_enum: bool|
379 -> Vec<DocumentSymbol> {
380 let mut out: Vec<DocumentSymbol> = Vec::new();
381 for (_, m) in methods.iter() {
382 out.push(DocumentSymbol {
383 name: m.name.clone(),
384 kind: DeclarationKind::Method,
385 location: m.location.clone(),
386 children: Vec::new(),
387 });
388 }
389 if let Some(props) = props {
390 for (_, p) in props.iter() {
391 out.push(DocumentSymbol {
392 name: p.name.clone(),
393 kind: DeclarationKind::Property,
394 location: p.location.clone(),
395 children: Vec::new(),
396 });
397 }
398 }
399 let const_kind = if is_enum {
400 DeclarationKind::EnumCase
401 } else {
402 DeclarationKind::Constant
403 };
404 for (_, c) in consts.iter() {
405 out.push(DocumentSymbol {
406 name: c.name.clone(),
407 kind: const_kind,
408 location: c.location.clone(),
409 children: Vec::new(),
410 });
411 }
412 out
413 };
414
415 for c in defs.slice.classes.iter() {
416 out.push(DocumentSymbol {
417 name: c.fqcn.clone(),
418 kind: DeclarationKind::Class,
419 location: c.location.clone(),
420 children: class_children(
421 &c.own_methods,
422 Some(&c.own_properties),
423 &c.own_constants,
424 false,
425 ),
426 });
427 }
428 for i in defs.slice.interfaces.iter() {
429 out.push(DocumentSymbol {
430 name: i.fqcn.clone(),
431 kind: DeclarationKind::Interface,
432 location: i.location.clone(),
433 children: class_children(&i.own_methods, None, &i.own_constants, false),
434 });
435 }
436 for t in defs.slice.traits.iter() {
437 out.push(DocumentSymbol {
438 name: t.fqcn.clone(),
439 kind: DeclarationKind::Trait,
440 location: t.location.clone(),
441 children: class_children(
442 &t.own_methods,
443 Some(&t.own_properties),
444 &t.own_constants,
445 false,
446 ),
447 });
448 }
449 for e in defs.slice.enums.iter() {
450 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
451 for (_, case) in e.cases.iter() {
452 children.push(DocumentSymbol {
453 name: case.name.clone(),
454 kind: DeclarationKind::EnumCase,
455 location: case.location.clone(),
456 children: Vec::new(),
457 });
458 }
459 out.push(DocumentSymbol {
460 name: e.fqcn.clone(),
461 kind: DeclarationKind::Enum,
462 location: e.location.clone(),
463 children,
464 });
465 }
466 for f in defs.slice.functions.iter() {
467 out.push(DocumentSymbol {
468 name: f.fqn.clone(),
469 kind: DeclarationKind::Function,
470 location: f.location.clone(),
471 children: Vec::new(),
472 });
473 }
474 for (name, _) in defs.slice.constants.iter() {
475 out.push(DocumentSymbol {
476 name: name.clone(),
477 kind: DeclarationKind::Constant,
478 location: None,
479 children: Vec::new(),
480 });
481 }
482 out
483 }
484}
485
486#[derive(Debug, Clone)]
489pub struct SubtypeClassSite {
490 pub fqcn: Arc<str>,
492 pub kind: crate::db::ClassLikeKind,
493 pub is_abstract: bool,
494 pub file: Arc<str>,
495 pub range: crate::Range,
497}
498
499pub(super) fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
502 crate::Range {
503 start: crate::Position {
504 line,
505 column: col_start,
506 },
507 end: crate::Position {
508 line,
509 column: col_end,
510 },
511 }
512}
513
514pub(super) fn identifier_char_col(
518 line: &str,
519 needle: &str,
520 min_col: usize,
521 case_insensitive: bool,
522) -> Option<u32> {
523 if needle.is_empty() {
524 return None;
525 }
526 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
527 let chars: Vec<char> = line.chars().collect();
528 let needle_chars: Vec<char> = needle.chars().collect();
529 let n = needle_chars.len();
530 if chars.len() < n {
531 return None;
532 }
533 for start in min_col..=chars.len().saturating_sub(n) {
534 let matches = chars[start..start + n]
535 .iter()
536 .zip(needle_chars.iter())
537 .all(|(a, b)| {
538 if case_insensitive {
539 a.eq_ignore_ascii_case(b)
540 } else {
541 a == b
542 }
543 });
544 if !matches {
545 continue;
546 }
547 let before_ok = start == 0 || !is_ident(chars[start - 1]);
548 let after = start + n;
549 let after_ok = after >= chars.len() || !is_ident(chars[after]);
550 if before_ok && after_ok {
551 return Some(start as u32);
552 }
553 }
554 None
555}
556
557#[cfg(test)]
568fn mentions_identifier(hay: &str, needle: &str) -> bool {
569 let hay = hay.as_bytes();
570 let needle = needle.as_bytes();
571 let n = needle.len();
572 if n == 0 || hay.len() < n {
573 return false;
574 }
575 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
576 let first = needle[0].to_ascii_lowercase();
577 for i in 0..=(hay.len() - n) {
578 if hay[i].to_ascii_lowercase() != first || !hay[i..i + n].eq_ignore_ascii_case(needle) {
579 continue;
580 }
581 if (i == 0 || !is_ident(hay[i - 1])) && (i + n == hay.len() || !is_ident(hay[i + n])) {
582 return true;
583 }
584 }
585 false
586}
587
588fn short(fqn: &str) -> &str {
589 fqn.rsplit('\\').next().unwrap_or(fqn)
590}
591
592pub(super) struct ReferenceGate {
600 pub(super) idents: Vec<String>,
601 pub(super) raw: Vec<String>,
602}
603
604impl ReferenceGate {
605 pub(super) fn for_symbol(db: &MirDbStorage, symbol: &crate::Name) -> Self {
631 if let crate::Name::Method { class, name } = symbol {
632 if name.as_ref() == "__construct" && !class.is_empty() {
633 return Self {
634 idents: reference_gate_needles(symbol),
635 raw: vec!["->__construct".to_string(), "::__construct".to_string()],
636 };
637 }
638 if name.as_ref() != "__construct" && name.as_ref() != "__invoke" && !class.is_empty() {
639 let here = crate::db::Fqcn::from_str(db, class.as_ref());
640 let is_static = crate::db::find_method_in_chain(db, here, name)
641 .map(|(_, m)| m.is_static)
642 .unwrap_or(false);
643 if is_static || crate::db::class_exists(db, class.as_ref()) {
644 return Self {
645 idents: vec![name.to_string()],
646 raw: Vec::new(),
647 };
648 }
649 }
650 }
651 Self {
652 idents: reference_gate_needles(symbol),
653 raw: Vec::new(),
654 }
655 }
656}
657
658fn reference_gate_needles(symbol: &crate::Name) -> Vec<String> {
664 let mut needles = match symbol {
665 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
666 vec![short(f).to_string()]
667 }
668 crate::Name::Method { class, name } if name.as_ref() == "__construct" => {
680 if class.is_empty() {
681 vec![name.to_string()]
684 } else {
685 vec![short(class).to_string()]
686 }
687 }
688 crate::Name::Method { class, name }
689 | crate::Name::Property { class, name }
690 | crate::Name::ClassConstant { class, name } => {
691 let mut v = vec![name.to_string()];
692 if !class.is_empty() {
693 v.push(short(class).to_string());
694 }
695 v
696 }
697 };
698 needles.retain(|n| !n.is_empty());
701 needles
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn mentions_identifier_is_case_insensitive_and_word_bounded() {
710 assert!(mentions_identifier("$this->save();", "save"));
711 assert!(mentions_identifier("new COLOR()", "Color"));
712 assert!(mentions_identifier("use App\\Color as Paint;", "color"));
713 assert!(!mentions_identifier("$this->saveAll();", "save"));
714 assert!(!mentions_identifier("return $unsaved;", "save"));
715 assert!(!mentions_identifier("no occurrence", "save"));
716 assert!(!mentions_identifier("anything", ""));
717 assert!(!mentions_identifier("function xÉclairFoo() {}", "Éclair"));
720 assert!(mentions_identifier("implements Éclair {}", "Éclair"));
721 }
722
723 #[test]
724 fn mention_scanner_membership_equals_per_needle_scans() {
725 use crate::db::MentionScanner;
730 use std::sync::Arc;
731 let universe = ["Color", "save", "ColorPicker", "Éclair", "C1", "_Wrap"];
732 let names: Vec<(mir_types::Name, bool)> = universe
733 .iter()
734 .map(|s| (mir_types::Name::new(s).ascii_lowercase(), false))
735 .collect();
736 let scanner = Arc::new(MentionScanner::build(1, names).unwrap());
737 let hays = [
738 "$this->save();",
739 "new COLOR()",
740 "use App\\Color as Paint;",
741 "$this->saveAll();",
742 "return $unsaved;",
743 "new ColorPicker(); Color::save();",
744 "function xÉclairFoo() {}",
745 "implements Éclair {}",
746 "colorsave savecolor color_save",
747 "class C1 extends _Wrap {}",
748 "",
749 ];
750 for hay in hays {
751 let scanned = scanner.scan(hay);
752 for needle in universe {
753 let expected = mentions_identifier(hay, needle);
754 let name = mir_types::Name::new(needle).ascii_lowercase();
755 assert_eq!(
756 scanned.binary_search(&name).is_ok(),
757 expected,
758 "needle {needle:?} on {hay:?}"
759 );
760 }
761 }
762 }
763
764 #[test]
765 fn gate_needles_cover_member_and_owner_class() {
766 let n = reference_gate_needles(&crate::Name::method("App\\Job", "run"));
769 assert!(n.contains(&"run".to_string()) && n.contains(&"Job".to_string()));
770 let n = reference_gate_needles(&crate::Name::class("App\\Ui\\Color"));
771 assert_eq!(n, vec!["Color".to_string()]);
772 let n = reference_gate_needles(&crate::Name::method("", "run"));
774 assert_eq!(n, vec!["run".to_string()]);
775 }
776
777 #[test]
778 fn gate_needles_for_constructor_scope_to_owner_class_only() {
779 let n = reference_gate_needles(&crate::Name::method("App\\Job", "__construct"));
784 assert_eq!(n, vec!["Job".to_string()]);
785 let n = reference_gate_needles(&crate::Name::method("", "__construct"));
787 assert_eq!(n, vec!["__construct".to_string()]);
788 }
789
790 fn session_with(files: &[(&str, &str)]) -> crate::AnalysisSession {
791 let mut session = crate::AnalysisSession::new(crate::PhpVersion::LATEST);
792 for (path, text) in files {
793 session.set_file_text(Arc::from(*path), Arc::from(*text));
794 }
795 session
796 }
797
798 #[test]
799 fn gate_static_method_is_member_name_only() {
800 let session = session_with(&[
805 (
806 "owner.php",
807 "<?php\nclass Owner { public static function m(): void {} }\n",
808 ),
809 ("sub.php", "<?php\nclass Sub extends Owner {}\n"),
810 ]);
811 let gate = session
812 .snapshot()
813 .reference_gate(&crate::Name::method("Owner", "m"));
814 assert_eq!(gate.idents, vec!["m".to_string()]);
815 assert!(gate.raw.is_empty());
816 }
817
818 #[test]
819 fn gate_instance_method_is_member_name_only() {
820 let session = session_with(&[(
821 "owner.php",
822 "<?php\nclass Owner { public function m(): void {} }\n",
823 )]);
824 let gate = session
825 .snapshot()
826 .reference_gate(&crate::Name::method("Owner", "m"));
827 assert_eq!(gate.idents, vec!["m".to_string()]);
828 assert!(gate.raw.is_empty());
829 }
830
831 #[test]
832 fn gate_invoke_keeps_owner_needle() {
833 let session = session_with(&[(
834 "owner.php",
835 "<?php\nclass Owner { public function __invoke(): void {} }\n",
836 )]);
837 let gate = session
838 .snapshot()
839 .reference_gate(&crate::Name::method("Owner", "__invoke"));
840 assert_eq!(
841 gate.idents,
842 reference_gate_needles(&crate::Name::method("Owner", "__invoke"))
843 );
844 assert!(gate.raw.is_empty());
845 }
846
847 #[test]
848 fn gate_constructor_adds_raw_call_tokens() {
849 let session = session_with(&[(
854 "owner.php",
855 "<?php\nclass Owner { public function __construct() {} }\n",
856 )]);
857 let gate = session
858 .snapshot()
859 .reference_gate(&crate::Name::method("Owner", "__construct"));
860 assert_eq!(
861 gate.idents,
862 reference_gate_needles(&crate::Name::method("Owner", "__construct"))
863 );
864 assert_eq!(
865 gate.raw,
866 vec!["->__construct".to_string(), "::__construct".to_string()]
867 );
868 }
869
870 #[test]
871 fn gate_unresolvable_owner_falls_back_to_general_needles() {
872 let session = session_with(&[(
873 "owner.php",
874 "<?php\nclass Owner { public static function m(): void {} }\n",
875 )]);
876 let gate = session
877 .snapshot()
878 .reference_gate(&crate::Name::method("Nonexistent\\Missing", "m"));
879 assert_eq!(
880 gate.idents,
881 reference_gate_needles(&crate::Name::method("Nonexistent\\Missing", "m"))
882 );
883 assert!(gate.raw.is_empty());
884 }
885}