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 symbol_at(&mut self, file: &str, byte_offset: u32) -> Option<crate::ResolvedSymbol> {
69 crate::FileAnalyzer::new(self).symbol_at(Arc::from(file), byte_offset)
70 }
71
72 pub fn definition_of(
85 &mut self,
86 symbol: &crate::Name,
87 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
88 self.load_symbol_owner(symbol);
89 self.definition_of_cached(symbol)
90 }
91
92 fn load_symbol_owner(&mut self, symbol: &crate::Name) {
95 match symbol {
96 crate::Name::Class(fqn) | crate::Name::Function(fqn) => {
97 let _ = self.load_class(fqn.as_ref());
98 }
99 crate::Name::Method { class, .. }
100 | crate::Name::Property { class, .. }
101 | crate::Name::ClassConstant { class, .. } => {
102 let _ = self.load_class(class.as_ref());
103 }
104 crate::Name::GlobalConstant(_) => {}
105 }
106 }
107
108 pub fn definition_of_cached(
112 &self,
113 symbol: &crate::Name,
114 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
115 self.query_snapshot(|snap| snap.definition_of_cached(symbol))
116 }
117
118 #[doc(hidden)]
122 pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
123 use crate::db::MirDatabase;
124 let db = self.snapshot_db();
125 db.reference_locations(symbol)
126 }
127
128 pub fn subtype_files(&mut self, class_fqn: &str) -> Vec<Arc<str>> {
139 self.prepare_for_query(None);
140 self.query_snapshot(|snap| snap.subtype_files(class_fqn))
141 }
142
143 pub fn indexed_use_import_locations(
148 &mut self,
149 symbol: &crate::Name,
150 files: &[Arc<str>],
151 ) -> Vec<(Arc<str>, crate::Range)> {
152 self.indexed_references_to(
153 symbol,
154 files,
155 false,
156 crate::ReferenceIncludes::UseImports,
157 &|| false,
158 )
159 .unwrap_or_default()
160 }
161
162 pub fn indexed_references_to(
170 &mut self,
171 symbol: &crate::Name,
172 files: &[Arc<str>],
173 include_declaration: bool,
174 includes: crate::ReferenceIncludes,
175 should_cancel: &(dyn Fn() -> bool + Sync),
176 ) -> Option<Vec<(Arc<str>, crate::Range)>> {
177 if self.db.salsa.needs_index_settle()
181 && !self.settle_workspace_index_cancellable(should_cancel)
182 {
183 return None;
184 }
185 let key = {
189 let snap = self.db_view();
190 let key = snap.reference_query_key(symbol, files, include_declaration, includes);
191 if let Some(hit) = self.index.ref_queries.get(&key) {
192 return Some(hit);
193 }
194 key
195 };
196 if should_cancel() {
197 return None;
198 }
199 let stale = self
200 .db_view()
201 .stale_reference_candidates_unguarded(symbol, files);
202
203 if !stale.is_empty() {
204 if !self.prepare_stale_candidates(&stale, should_cancel) {
205 return None;
206 }
207 self.query_snapshot(|snap| snap.commit_reference_candidates(&stale));
208 }
209
210 if include_declaration {
211 self.load_symbol_owner(symbol);
212 }
213 let snap = self.db_view();
214 let out = snap.read_references(symbol, files, include_declaration, includes);
215 snap.memoize_references(key, &out);
216 Some(out)
217 }
218
219 pub fn prepare_references_query(
235 &mut self,
236 symbol: &crate::Name,
237 stale: &[Arc<str>],
238 include_declaration: bool,
239 should_cancel: &(dyn Fn() -> bool + Sync),
240 ) -> bool {
241 if self.db.salsa.needs_index_settle()
242 && !self.settle_workspace_index_cancellable(should_cancel)
243 {
244 return false;
245 }
246 if !self.prepare_stale_candidates(stale, should_cancel) {
247 return false;
248 }
249 if include_declaration {
250 self.load_symbol_owner(symbol);
251 }
252 true
253 }
254
255 fn prepare_stale_candidates(
259 &mut self,
260 stale: &[Arc<str>],
261 should_cancel: &(dyn Fn() -> bool + Sync),
262 ) -> bool {
263 {
264 let mut session = self.defer_revision_bumps();
265 for path in stale {
266 if !session.prepare_file_for_analysis_cancellable(path, should_cancel) {
267 return false;
268 }
269 }
270 }
271 !should_cancel()
272 }
273
274 pub fn declaration_name_range(
278 &mut self,
279 symbol: &crate::Name,
280 ) -> Option<(Arc<str>, crate::Range)> {
281 self.load_symbol_owner(symbol);
282 self.db_view().declaration_name_range(symbol)
283 }
284
285 pub fn indexed_subtype_classes(
287 &mut self,
288 class_fqn: &str,
289 files: &[Arc<str>],
290 include_trait_users: bool,
291 ) -> Vec<SubtypeClassSite> {
292 self.prepare_for_query(None);
293 self.query_snapshot(|snap| {
294 snap.indexed_subtype_classes(class_fqn, files, include_trait_users)
295 })
296 }
297
298 pub fn indexed_method_implementations(
300 &mut self,
301 class_fqn: &str,
302 method: &str,
303 files: &[Arc<str>],
304 ) -> Vec<(Arc<str>, Arc<str>, crate::Range)> {
305 self.prepare_for_query(None);
306 self.query_snapshot(|snap| snap.indexed_method_implementations(class_fqn, method, files))
307 }
308
309 pub fn class_issues(&mut self, files: &[Arc<str>]) -> Vec<crate::Issue> {
313 self.prepare_for_query(None);
314 self.query_snapshot(|snap| snap.class_issues(files))
315 }
316
317 pub fn collector_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
321 self.query_snapshot(|snap| snap.collector_issues(files))
322 }
323
324 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
330 use crate::symbol::{DeclarationKind, DocumentSymbol};
331
332 let db = self.snapshot_db();
333 let Some(sf) = db.lookup_source_file(file) else {
334 return Vec::new();
335 };
336 let defs = crate::db::collect_file_definitions(&db, sf);
337 let mut out: Vec<DocumentSymbol> = Vec::new();
338
339 let class_children = |methods: &mir_codebase::definitions::MemberMap<
340 Arc<mir_codebase::definitions::MethodDef>,
341 >,
342 props: Option<
343 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
344 >,
345 consts: &mir_codebase::definitions::MemberMap<
346 mir_codebase::definitions::ConstantDef,
347 >,
348 is_enum: bool|
349 -> Vec<DocumentSymbol> {
350 let mut out: Vec<DocumentSymbol> = Vec::new();
351 for (_, m) in methods.iter() {
352 out.push(DocumentSymbol {
353 name: m.name.clone(),
354 kind: DeclarationKind::Method,
355 location: m.location.clone(),
356 children: Vec::new(),
357 });
358 }
359 if let Some(props) = props {
360 for (_, p) in props.iter() {
361 out.push(DocumentSymbol {
362 name: p.name.clone(),
363 kind: DeclarationKind::Property,
364 location: p.location.clone(),
365 children: Vec::new(),
366 });
367 }
368 }
369 let const_kind = if is_enum {
370 DeclarationKind::EnumCase
371 } else {
372 DeclarationKind::Constant
373 };
374 for (_, c) in consts.iter() {
375 out.push(DocumentSymbol {
376 name: c.name.clone(),
377 kind: const_kind,
378 location: c.location.clone(),
379 children: Vec::new(),
380 });
381 }
382 out
383 };
384
385 for c in defs.slice.classes.iter() {
386 out.push(DocumentSymbol {
387 name: c.fqcn.clone(),
388 kind: DeclarationKind::Class,
389 location: c.location.clone(),
390 children: class_children(
391 &c.own_methods,
392 Some(&c.own_properties),
393 &c.own_constants,
394 false,
395 ),
396 });
397 }
398 for i in defs.slice.interfaces.iter() {
399 out.push(DocumentSymbol {
400 name: i.fqcn.clone(),
401 kind: DeclarationKind::Interface,
402 location: i.location.clone(),
403 children: class_children(&i.own_methods, None, &i.own_constants, false),
404 });
405 }
406 for t in defs.slice.traits.iter() {
407 out.push(DocumentSymbol {
408 name: t.fqcn.clone(),
409 kind: DeclarationKind::Trait,
410 location: t.location.clone(),
411 children: class_children(
412 &t.own_methods,
413 Some(&t.own_properties),
414 &t.own_constants,
415 false,
416 ),
417 });
418 }
419 for e in defs.slice.enums.iter() {
420 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
421 for (_, case) in e.cases.iter() {
422 children.push(DocumentSymbol {
423 name: case.name.clone(),
424 kind: DeclarationKind::EnumCase,
425 location: case.location.clone(),
426 children: Vec::new(),
427 });
428 }
429 out.push(DocumentSymbol {
430 name: e.fqcn.clone(),
431 kind: DeclarationKind::Enum,
432 location: e.location.clone(),
433 children,
434 });
435 }
436 for f in defs.slice.functions.iter() {
437 out.push(DocumentSymbol {
438 name: f.fqn.clone(),
439 kind: DeclarationKind::Function,
440 location: f.location.clone(),
441 children: Vec::new(),
442 });
443 }
444 for (name, _) in defs.slice.constants.iter() {
445 out.push(DocumentSymbol {
446 name: name.clone(),
447 kind: DeclarationKind::Constant,
448 location: None,
449 children: Vec::new(),
450 });
451 }
452 out
453 }
454}
455
456#[derive(Debug, Clone)]
459pub struct SubtypeClassSite {
460 pub fqcn: Arc<str>,
462 pub kind: crate::db::ClassLikeKind,
463 pub is_abstract: bool,
464 pub file: Arc<str>,
465 pub range: crate::Range,
467}
468
469pub(super) fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
472 crate::Range {
473 start: crate::Position {
474 line,
475 column: col_start,
476 },
477 end: crate::Position {
478 line,
479 column: col_end,
480 },
481 }
482}
483
484pub(super) fn identifier_char_col(
488 line: &str,
489 needle: &str,
490 min_col: usize,
491 case_insensitive: bool,
492) -> Option<u32> {
493 if needle.is_empty() {
494 return None;
495 }
496 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
497 let chars: Vec<char> = line.chars().collect();
498 let needle_chars: Vec<char> = needle.chars().collect();
499 let n = needle_chars.len();
500 if chars.len() < n {
501 return None;
502 }
503 for start in min_col..=chars.len().saturating_sub(n) {
504 let matches = chars[start..start + n]
505 .iter()
506 .zip(needle_chars.iter())
507 .all(|(a, b)| {
508 if case_insensitive {
509 a.eq_ignore_ascii_case(b)
510 } else {
511 a == b
512 }
513 });
514 if !matches {
515 continue;
516 }
517 let before_ok = start == 0 || !is_ident(chars[start - 1]);
518 let after = start + n;
519 let after_ok = after >= chars.len() || !is_ident(chars[after]);
520 if before_ok && after_ok {
521 return Some(start as u32);
522 }
523 }
524 None
525}
526
527#[cfg(test)]
538fn mentions_identifier(hay: &str, needle: &str) -> bool {
539 let hay = hay.as_bytes();
540 let needle = needle.as_bytes();
541 let n = needle.len();
542 if n == 0 || hay.len() < n {
543 return false;
544 }
545 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
546 let first = needle[0].to_ascii_lowercase();
547 for i in 0..=(hay.len() - n) {
548 if hay[i].to_ascii_lowercase() != first || !hay[i..i + n].eq_ignore_ascii_case(needle) {
549 continue;
550 }
551 if (i == 0 || !is_ident(hay[i - 1])) && (i + n == hay.len() || !is_ident(hay[i + n])) {
552 return true;
553 }
554 }
555 false
556}
557
558fn short(fqn: &str) -> &str {
559 fqn.rsplit('\\').next().unwrap_or(fqn)
560}
561
562pub(super) struct ReferenceGate {
570 pub(super) idents: Vec<String>,
571 pub(super) raw: Vec<String>,
572}
573
574impl ReferenceGate {
575 pub(super) fn for_symbol(db: &MirDbStorage, symbol: &crate::Name) -> Self {
601 if let crate::Name::Method { class, name } = symbol {
602 if name.as_ref() == "__construct" && !class.is_empty() {
603 return Self {
604 idents: reference_gate_needles(symbol),
605 raw: vec!["->__construct".to_string(), "::__construct".to_string()],
606 };
607 }
608 if name.as_ref() != "__construct" && name.as_ref() != "__invoke" && !class.is_empty() {
609 let here = crate::db::Fqcn::from_str(db, class.as_ref());
610 let is_static = crate::db::find_method_in_chain(db, here, name)
611 .map(|(_, m)| m.is_static)
612 .unwrap_or(false);
613 if is_static || crate::db::class_exists(db, class.as_ref()) {
614 return Self {
615 idents: vec![name.to_string()],
616 raw: Vec::new(),
617 };
618 }
619 }
620 }
621 Self {
622 idents: reference_gate_needles(symbol),
623 raw: Vec::new(),
624 }
625 }
626}
627
628fn reference_gate_needles(symbol: &crate::Name) -> Vec<String> {
634 let mut needles = match symbol {
635 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
636 vec![short(f).to_string()]
637 }
638 crate::Name::Method { class, name } if name.as_ref() == "__construct" => {
650 if class.is_empty() {
651 vec![name.to_string()]
654 } else {
655 vec![short(class).to_string()]
656 }
657 }
658 crate::Name::Method { class, name }
659 | crate::Name::Property { class, name }
660 | crate::Name::ClassConstant { class, name } => {
661 let mut v = vec![name.to_string()];
662 if !class.is_empty() {
663 v.push(short(class).to_string());
664 }
665 v
666 }
667 };
668 needles.retain(|n| !n.is_empty());
671 needles
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 #[test]
679 fn mentions_identifier_is_case_insensitive_and_word_bounded() {
680 assert!(mentions_identifier("$this->save();", "save"));
681 assert!(mentions_identifier("new COLOR()", "Color"));
682 assert!(mentions_identifier("use App\\Color as Paint;", "color"));
683 assert!(!mentions_identifier("$this->saveAll();", "save"));
684 assert!(!mentions_identifier("return $unsaved;", "save"));
685 assert!(!mentions_identifier("no occurrence", "save"));
686 assert!(!mentions_identifier("anything", ""));
687 assert!(!mentions_identifier("function xÉclairFoo() {}", "Éclair"));
690 assert!(mentions_identifier("implements Éclair {}", "Éclair"));
691 }
692
693 #[test]
694 fn mention_scanner_membership_equals_per_needle_scans() {
695 use crate::db::MentionScanner;
700 use std::sync::Arc;
701 let universe = ["Color", "save", "ColorPicker", "Éclair", "C1", "_Wrap"];
702 let names: Vec<(mir_types::Name, bool)> = universe
703 .iter()
704 .map(|s| (mir_types::Name::new(s).ascii_lowercase(), false))
705 .collect();
706 let scanner = Arc::new(MentionScanner::build(1, names).unwrap());
707 let hays = [
708 "$this->save();",
709 "new COLOR()",
710 "use App\\Color as Paint;",
711 "$this->saveAll();",
712 "return $unsaved;",
713 "new ColorPicker(); Color::save();",
714 "function xÉclairFoo() {}",
715 "implements Éclair {}",
716 "colorsave savecolor color_save",
717 "class C1 extends _Wrap {}",
718 "",
719 ];
720 for hay in hays {
721 let scanned = scanner.scan(hay);
722 for needle in universe {
723 let expected = mentions_identifier(hay, needle);
724 let name = mir_types::Name::new(needle).ascii_lowercase();
725 assert_eq!(
726 scanned.binary_search(&name).is_ok(),
727 expected,
728 "needle {needle:?} on {hay:?}"
729 );
730 }
731 }
732 }
733
734 #[test]
735 fn gate_needles_cover_member_and_owner_class() {
736 let n = reference_gate_needles(&crate::Name::method("App\\Job", "run"));
739 assert!(n.contains(&"run".to_string()) && n.contains(&"Job".to_string()));
740 let n = reference_gate_needles(&crate::Name::class("App\\Ui\\Color"));
741 assert_eq!(n, vec!["Color".to_string()]);
742 let n = reference_gate_needles(&crate::Name::method("", "run"));
744 assert_eq!(n, vec!["run".to_string()]);
745 }
746
747 #[test]
748 fn gate_needles_for_constructor_scope_to_owner_class_only() {
749 let n = reference_gate_needles(&crate::Name::method("App\\Job", "__construct"));
754 assert_eq!(n, vec!["Job".to_string()]);
755 let n = reference_gate_needles(&crate::Name::method("", "__construct"));
757 assert_eq!(n, vec!["__construct".to_string()]);
758 }
759
760 fn session_with(files: &[(&str, &str)]) -> crate::AnalysisSession {
761 let mut session = crate::AnalysisSession::new(crate::PhpVersion::LATEST);
762 for (path, text) in files {
763 session.set_file_text(Arc::from(*path), Arc::from(*text));
764 }
765 session
766 }
767
768 #[test]
769 fn gate_static_method_is_member_name_only() {
770 let session = session_with(&[
775 (
776 "owner.php",
777 "<?php\nclass Owner { public static function m(): void {} }\n",
778 ),
779 ("sub.php", "<?php\nclass Sub extends Owner {}\n"),
780 ]);
781 let gate = session
782 .snapshot()
783 .reference_gate(&crate::Name::method("Owner", "m"));
784 assert_eq!(gate.idents, vec!["m".to_string()]);
785 assert!(gate.raw.is_empty());
786 }
787
788 #[test]
789 fn gate_instance_method_is_member_name_only() {
790 let session = session_with(&[(
791 "owner.php",
792 "<?php\nclass Owner { public function m(): void {} }\n",
793 )]);
794 let gate = session
795 .snapshot()
796 .reference_gate(&crate::Name::method("Owner", "m"));
797 assert_eq!(gate.idents, vec!["m".to_string()]);
798 assert!(gate.raw.is_empty());
799 }
800
801 #[test]
802 fn gate_invoke_keeps_owner_needle() {
803 let session = session_with(&[(
804 "owner.php",
805 "<?php\nclass Owner { public function __invoke(): void {} }\n",
806 )]);
807 let gate = session
808 .snapshot()
809 .reference_gate(&crate::Name::method("Owner", "__invoke"));
810 assert_eq!(
811 gate.idents,
812 reference_gate_needles(&crate::Name::method("Owner", "__invoke"))
813 );
814 assert!(gate.raw.is_empty());
815 }
816
817 #[test]
818 fn gate_constructor_adds_raw_call_tokens() {
819 let session = session_with(&[(
824 "owner.php",
825 "<?php\nclass Owner { public function __construct() {} }\n",
826 )]);
827 let gate = session
828 .snapshot()
829 .reference_gate(&crate::Name::method("Owner", "__construct"));
830 assert_eq!(
831 gate.idents,
832 reference_gate_needles(&crate::Name::method("Owner", "__construct"))
833 );
834 assert_eq!(
835 gate.raw,
836 vec!["->__construct".to_string(), "::__construct".to_string()]
837 );
838 }
839
840 #[test]
841 fn gate_unresolvable_owner_falls_back_to_general_needles() {
842 let session = session_with(&[(
843 "owner.php",
844 "<?php\nclass Owner { public static function m(): void {} }\n",
845 )]);
846 let gate = session
847 .snapshot()
848 .reference_gate(&crate::Name::method("Nonexistent\\Missing", "m"));
849 assert_eq!(
850 gate.idents,
851 reference_gate_needles(&crate::Name::method("Nonexistent\\Missing", "m"))
852 );
853 assert!(gate.raw.is_empty());
854 }
855}