1use rustc_hash::FxHashMap;
2use std::sync::{Arc, OnceLock};
3
4pub(crate) mod analyzer_db;
5pub(crate) mod attributes;
6pub mod batch;
7pub(crate) mod body_analysis;
8#[doc(hidden)]
9pub mod cache;
10pub(crate) mod call;
11pub(crate) mod class;
12pub(crate) mod collector;
13pub(crate) mod contradiction;
14#[doc(hidden)]
15pub mod db;
16pub(crate) mod dead_code;
17pub(crate) mod diagnostics;
18pub(crate) mod expr;
19pub mod file_analyzer;
20pub(crate) mod flow_state;
21pub(crate) mod generic;
22pub mod indexing;
23#[doc(hidden)]
24pub mod metrics;
25pub(crate) mod narrowing;
26#[doc(hidden)]
27pub mod parse_cache;
28#[doc(hidden)]
29pub mod parser;
30pub mod php_version;
31pub mod prelude;
32pub(crate) mod reference_key;
33pub mod session;
34pub mod source_provider;
35pub(crate) mod stmt;
36#[doc(hidden)]
37pub mod stub_cache;
38#[doc(hidden)]
39pub mod stubs;
40pub(crate) mod subtype;
41pub mod suppression;
42pub(crate) mod taint;
43pub(crate) mod type_env;
44pub(crate) mod util;
45
46pub use batch::{
47 analyze_source, dead_code_issue_kinds, discover_files, AnalysisResult, BatchOptions,
48};
49pub use file_analyzer::{FileAnalysis, FileAnalyzer};
50pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
51pub use parser::type_from_hint::type_from_hint;
52pub use parser::{DocblockParser, ParsedDocblock};
53pub use php_version::{ParsePhpVersionError, PhpVersion};
54pub use session::{AnalysisSession, ReferenceIncludes, SubtypeClassSite};
55pub use source_provider::{FsSourceProvider, SourceProvider};
56
57pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
61 let w = written.trim_start_matches('\\');
62 let c = canonical.trim_start_matches('\\');
63 if w == c || !w.eq_ignore_ascii_case(c) {
64 return None;
65 }
66 let w_last = w.rsplit('\\').next().unwrap_or(w);
67 let c_last = c.rsplit('\\').next().unwrap_or(c);
68 if w_last != c_last {
69 let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
70 let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
71 if w_prefix == c_prefix {
72 return Some((w_last.to_string(), c_last.to_string()));
73 }
74 }
75 Some((w.to_string(), c.to_string()))
76}
77pub use stubs::{
78 is_builtin_constant, is_builtin_function, stub_files, stub_path_for_class,
79 ChainedClassResolver, StubClassResolver, StubVfs,
80};
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
103pub struct Position {
104 pub line: u32,
105 pub column: u32,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct Range {
111 pub start: Position,
112 pub end: Position,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122pub enum Name {
123 Class(std::sync::Arc<str>),
125 Function(std::sync::Arc<str>),
127 Method {
129 class: std::sync::Arc<str>,
130 name: std::sync::Arc<str>,
131 },
132 Property {
134 class: std::sync::Arc<str>,
135 name: std::sync::Arc<str>,
136 },
137 ClassConstant {
139 class: std::sync::Arc<str>,
140 name: std::sync::Arc<str>,
141 },
142 GlobalConstant(std::sync::Arc<str>),
144}
145
146impl Name {
147 pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
150 Name::Method {
151 class: class.into(),
152 name: std::sync::Arc::from(name.to_ascii_lowercase()),
153 }
154 }
155
156 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
158 Name::Class(fqcn.into())
159 }
160
161 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
163 Name::Function(fqn.into())
164 }
165
166 pub fn property(
168 class: impl Into<std::sync::Arc<str>>,
169 name: impl Into<std::sync::Arc<str>>,
170 ) -> Self {
171 Name::Property {
172 class: class.into(),
173 name: name.into(),
174 }
175 }
176
177 pub fn class_constant(
179 class: impl Into<std::sync::Arc<str>>,
180 name: impl Into<std::sync::Arc<str>>,
181 ) -> Self {
182 Name::ClassConstant {
183 class: class.into(),
184 name: name.into(),
185 }
186 }
187
188 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
190 Name::GlobalConstant(fqn.into())
191 }
192
193 pub fn codebase_key(&self) -> String {
203 match self {
204 Name::Class(fqcn) => format!("cls:{fqcn}"),
205 Name::Function(fqn) => format!("fn:{fqn}"),
206 Name::Method { class, name } => format!("meth:{class}::{name}"),
207 Name::Property { class, name } => format!("prop:{class}::{name}"),
208 Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
209 Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
210 }
211 }
212}
213
214pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
222 let stripped = symbol_key
223 .strip_prefix("meth:")
224 .or_else(|| symbol_key.strip_prefix("prop:"))
225 .or_else(|| symbol_key.strip_prefix("cnst:"))
226 .or_else(|| symbol_key.strip_prefix("cls:"))
227 .or_else(|| symbol_key.strip_prefix("fn:"))
228 .or_else(|| symbol_key.strip_prefix("gcnst:"))
229 .unwrap_or(symbol_key);
230 match stripped.split_once("::") {
231 Some((class, _)) => class,
232 None => stripped,
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum SymbolLookupError {
239 NotFound,
241 NoSourceLocation,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum LoadOutcome {
249 AlreadyLoaded,
251 Loaded,
254 NotResolvable,
258}
259
260impl LoadOutcome {
261 pub fn is_loaded(self) -> bool {
264 !matches!(self, LoadOutcome::NotResolvable)
265 }
266}
267
268pub trait ClassResolver: Send + Sync {
276 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
279}
280
281impl ClassResolver for composer::Psr4Map {
282 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
283 composer::Psr4Map::resolve(self, fqcn)
284 }
285}
286
287impl std::fmt::Display for SymbolLookupError {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 SymbolLookupError::NotFound => write!(f, "symbol not found"),
291 SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
292 }
293 }
294}
295
296impl std::error::Error for SymbolLookupError {}
297
298#[derive(Debug, Clone)]
301pub struct HoverInfo {
302 pub ty: Type,
304 pub docstring: Option<String>,
306 pub definition: Option<mir_types::Location>,
308}
309
310#[derive(Debug)]
313pub struct DependencyGraph {
314 files: Vec<Arc<str>>,
315 file_ids: FxHashMap<Arc<str>, u32>,
316 dependencies: Vec<Vec<u32>>,
318 dependents: Vec<Vec<u32>>,
320 legacy_dependencies: OnceLock<FxHashMap<String, Vec<String>>>,
321 legacy_dependents: OnceLock<FxHashMap<String, Vec<String>>>,
322}
323
324impl Clone for DependencyGraph {
325 fn clone(&self) -> Self {
326 Self {
327 files: self.files.clone(),
328 file_ids: self.file_ids.clone(),
329 dependencies: self.dependencies.clone(),
330 dependents: self.dependents.clone(),
331 legacy_dependencies: OnceLock::new(),
332 legacy_dependents: OnceLock::new(),
333 }
334 }
335}
336
337impl DependencyGraph {
338 pub(crate) fn from_compact_parts(
339 files: Vec<Arc<str>>,
340 file_ids: FxHashMap<Arc<str>, u32>,
341 dependencies: Vec<Vec<u32>>,
342 dependents: Vec<Vec<u32>>,
343 ) -> Self {
344 Self {
345 files,
346 file_ids,
347 dependencies,
348 dependents,
349 legacy_dependencies: OnceLock::new(),
350 legacy_dependents: OnceLock::new(),
351 }
352 }
353
354 pub fn file_count(&self) -> usize {
356 self.files.len()
357 }
358
359 pub fn dependency_edge_count(&self) -> usize {
361 self.dependencies.iter().map(Vec::len).sum()
362 }
363
364 pub fn dependent_edge_count(&self) -> usize {
366 self.dependents.iter().map(Vec::len).sum()
367 }
368
369 fn file_path(&self, id: u32) -> &Arc<str> {
370 &self.files[id as usize]
371 }
372
373 fn stringify_adjacency(&self, adjacency: &[Vec<u32>]) -> FxHashMap<String, Vec<String>> {
374 adjacency
375 .iter()
376 .enumerate()
377 .filter(|(_, deps)| !deps.is_empty())
378 .map(|(file_id, deps)| {
379 (
380 self.files[file_id].to_string(),
381 deps.iter()
382 .map(|&dep_id| self.file_path(dep_id).to_string())
383 .collect(),
384 )
385 })
386 .collect()
387 }
388
389 fn direct_arcs<'a>(
390 &'a self,
391 file: &str,
392 adjacency: &'a [Vec<u32>],
393 ) -> impl Iterator<Item = &'a Arc<str>> {
394 self.file_ids
395 .get(file)
396 .and_then(|&id| adjacency.get(id as usize))
397 .into_iter()
398 .flatten()
399 .map(|&id| self.file_path(id))
400 }
401
402 pub fn dependency_paths_of(&self, file: &str) -> Vec<Arc<str>> {
404 self.direct_arcs(file, &self.dependencies)
405 .cloned()
406 .collect()
407 }
408
409 pub fn dependent_paths_of(&self, file: &str) -> Vec<Arc<str>> {
411 self.direct_arcs(file, &self.dependents).cloned().collect()
412 }
413
414 pub fn dependencies_of(&self, file: &str) -> &[String] {
416 self.legacy_dependencies
417 .get_or_init(|| self.stringify_adjacency(&self.dependencies))
418 .get(file)
419 .map(|v| v.as_slice())
420 .unwrap_or(&[])
421 }
422
423 pub fn dependents_of(&self, file: &str) -> &[String] {
425 self.legacy_dependents
426 .get_or_init(|| self.stringify_adjacency(&self.dependents))
427 .get(file)
428 .map(|v| v.as_slice())
429 .unwrap_or(&[])
430 }
431
432 pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
434 let mut visited = rustc_hash::FxHashSet::default();
435 let Some(&seed) = self.file_ids.get(file) else {
436 return Vec::new();
437 };
438 let mut queue = vec![seed];
439 visited.insert(seed);
440 let mut result = Vec::new();
441
442 while let Some(current) = queue.pop() {
443 for &dep in &self.dependencies[current as usize] {
444 if visited.insert(dep) {
445 queue.push(dep);
446 result.push(self.file_path(dep).to_string());
447 }
448 }
449 }
450 result
451 }
452
453 pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
455 let mut visited = rustc_hash::FxHashSet::default();
456 let Some(&seed) = self.file_ids.get(file) else {
457 return Vec::new();
458 };
459 let mut queue = vec![seed];
460 visited.insert(seed);
461 let mut result = Vec::new();
462
463 while let Some(current) = queue.pop() {
464 for &dep in &self.dependents[current as usize] {
465 if visited.insert(dep) {
466 queue.push(dep);
467 result.push(self.file_path(dep).to_string());
468 }
469 }
470 }
471 result
472 }
473}
474
475pub mod symbol;
476pub use mir_codebase::definitions::{DeclaredParam, FunctionDef, TemplateParam, Visibility};
477pub use mir_issues::{Issue, IssueKind, Severity};
478pub use mir_types::Type;
479
480#[cfg(test)]
481mod dependency_graph_tests {
482 use super::*;
483
484 fn graph(edges: &[(&str, &[&str])]) -> DependencyGraph {
485 let mut files: Vec<Arc<str>> = edges
486 .iter()
487 .flat_map(|(from, deps)| {
488 std::iter::once(*from)
489 .chain(deps.iter().copied())
490 .map(Arc::<str>::from)
491 })
492 .collect();
493 files.sort();
494 files.dedup();
495
496 let file_ids: FxHashMap<Arc<str>, u32> = files
497 .iter()
498 .enumerate()
499 .map(|(id, file)| (file.clone(), id as u32))
500 .collect();
501 let mut dependencies = vec![Vec::new(); files.len()];
502 let mut dependents = vec![Vec::new(); files.len()];
503
504 for (from, deps) in edges {
505 let from_id = file_ids[*from];
506 for dep in *deps {
507 let dep_id = file_ids[*dep];
508 dependencies[from_id as usize].push(dep_id);
509 dependents[dep_id as usize].push(from_id);
510 }
511 }
512 for adjacency in [&mut dependencies, &mut dependents] {
513 for deps in adjacency {
514 deps.sort();
515 deps.dedup();
516 }
517 }
518
519 DependencyGraph::from_compact_parts(files, file_ids, dependencies, dependents)
520 }
521
522 #[test]
523 fn transitive_traversal_deduplicates_diamond_graphs() {
524 let graph = graph(&[
525 ("a.php", &["b.php", "c.php"]),
526 ("b.php", &["d.php"]),
527 ("c.php", &["d.php"]),
528 ]);
529
530 let deps = graph.transitive_dependencies("a.php");
531 assert_eq!(
532 deps.iter().filter(|path| path.as_str() == "d.php").count(),
533 1
534 );
535 assert_eq!(deps.len(), 3);
536
537 let dependents = graph.transitive_dependents("d.php");
538 assert_eq!(
539 dependents
540 .iter()
541 .filter(|path| path.as_str() == "a.php")
542 .count(),
543 1
544 );
545 assert_eq!(dependents.len(), 3);
546 }
547
548 #[test]
549 fn direct_arc_accessors_do_not_require_legacy_string_maps() {
550 let graph = graph(&[("consumer.php", &["service.php"])]);
551
552 let deps = graph.dependency_paths_of("consumer.php");
553 assert_eq!(deps, vec![Arc::<str>::from("service.php")]);
554 assert!(graph.legacy_dependencies.get().is_none());
555
556 let dependents = graph.dependent_paths_of("service.php");
557 assert_eq!(dependents, vec![Arc::<str>::from("consumer.php")]);
558 assert!(graph.legacy_dependents.get().is_none());
559
560 assert_eq!(
561 graph.dependencies_of("consumer.php"),
562 &["service.php".to_string()]
563 );
564 assert!(graph.legacy_dependencies.get().is_some());
565 }
566}
567
568pub fn location_from_span(
578 span: php_ast::Span,
579 file: std::sync::Arc<str>,
580 source: &str,
581 source_map: &php_rs_parser::source_map::SourceMap,
582) -> mir_types::Location {
583 let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
584 let (line_end, col_end) = if span.start < span.end {
585 diagnostics::offset_to_line_col(source, span.end, source_map)
586 } else {
587 (line, col_start)
588 };
589 mir_types::Location {
590 file,
591 line,
592 line_end,
593 col_start,
594 col_end: diagnostics::clamp_col_end(line, line_end, col_start, col_end),
595 }
596}
597pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
598
599pub mod composer;
600pub use composer::{ComposerError, Psr4Map};
601pub use type_env::ScopeId;
602
603#[doc(hidden)]
604pub mod test_utils;