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 docblock_nav;
19pub(crate) mod expr;
20pub mod file_analyzer;
21mod file_extensions;
22pub(crate) mod flow_state;
23pub(crate) mod generic;
24pub mod include_graph;
25pub mod indexing;
26#[doc(hidden)]
27pub mod metrics;
28pub(crate) mod narrowing;
29#[doc(hidden)]
30pub mod parse_cache;
31#[doc(hidden)]
32pub mod parser;
33#[doc(hidden)]
34pub mod perf_fixture;
35pub mod php_version;
36pub mod prelude;
37pub(crate) mod reference_key;
38pub mod session;
39pub mod source_provider;
40pub(crate) mod stmt;
41#[doc(hidden)]
42pub mod stub_cache;
43#[doc(hidden)]
44pub mod stubs;
45pub(crate) mod subtype;
46pub mod suppression;
47pub(crate) mod taint;
48mod tmp_suffix;
49pub(crate) mod type_env;
50pub(crate) mod util;
51
52pub use batch::{
53 analyze_source, analyze_source_with_options, dead_code_issue_kinds, discover_files,
54 discover_files_with_extensions, AnalysisResult, BatchOptions,
55};
56pub use file_analyzer::{FileAnalysis, FileAnalyzer};
57pub use file_extensions::PhpFileExtensions;
58pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
59pub use parser::type_from_hint::type_from_hint;
60pub use parser::{DocblockParser, ParsedDocblock};
61pub use php_version::{ParsePhpVersionError, PhpVersion};
62pub use session::{AnalysisSession, AnalysisSnapshot, ReferenceIncludes, SubtypeClassSite};
63pub use source_provider::{FsSourceProvider, SourceProvider};
64
65pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
69 let w = written.trim_start_matches('\\');
70 let c = canonical.trim_start_matches('\\');
71 if w == c || !w.eq_ignore_ascii_case(c) {
72 return None;
73 }
74 let w_last = w.rsplit('\\').next().unwrap_or(w);
75 let c_last = c.rsplit('\\').next().unwrap_or(c);
76 if w_last != c_last {
77 let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
78 let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
79 if w_prefix == c_prefix {
80 return Some((w_last.to_string(), c_last.to_string()));
81 }
82 }
83 Some((w.to_string(), c.to_string()))
84}
85pub use stubs::{
86 is_builtin_constant, is_builtin_function, stub_files, stub_path_for_class,
87 ChainedClassResolver, StubClassResolver, StubVfs,
88};
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
112pub struct Position {
113 pub line: u32,
114 pub column: u32,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub struct Range {
120 pub start: Position,
121 pub end: Position,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub enum Name {
132 Class(std::sync::Arc<str>),
134 Function(std::sync::Arc<str>),
136 Method {
138 class: std::sync::Arc<str>,
139 name: std::sync::Arc<str>,
140 },
141 Property {
143 class: std::sync::Arc<str>,
144 name: std::sync::Arc<str>,
145 },
146 ClassConstant {
148 class: std::sync::Arc<str>,
149 name: std::sync::Arc<str>,
150 },
151 GlobalConstant(std::sync::Arc<str>),
153}
154
155impl Name {
156 pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
159 Name::Method {
160 class: class.into(),
161 name: std::sync::Arc::from(name.to_ascii_lowercase()),
162 }
163 }
164
165 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
167 Name::Class(fqcn.into())
168 }
169
170 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
172 Name::Function(fqn.into())
173 }
174
175 pub fn property(
177 class: impl Into<std::sync::Arc<str>>,
178 name: impl Into<std::sync::Arc<str>>,
179 ) -> Self {
180 Name::Property {
181 class: class.into(),
182 name: name.into(),
183 }
184 }
185
186 pub fn class_constant(
188 class: impl Into<std::sync::Arc<str>>,
189 name: impl Into<std::sync::Arc<str>>,
190 ) -> Self {
191 Name::ClassConstant {
192 class: class.into(),
193 name: name.into(),
194 }
195 }
196
197 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
199 Name::GlobalConstant(fqn.into())
200 }
201
202 pub fn codebase_key(&self) -> String {
212 match self {
213 Name::Class(fqcn) => format!("cls:{fqcn}"),
214 Name::Function(fqn) => format!("fn:{fqn}"),
215 Name::Method { class, name } => format!("meth:{class}::{name}"),
216 Name::Property { class, name } => format!("prop:{class}::{name}"),
217 Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
218 Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
219 }
220 }
221}
222
223pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
231 let stripped = symbol_key
232 .strip_prefix("meth:")
233 .or_else(|| symbol_key.strip_prefix("prop:"))
234 .or_else(|| symbol_key.strip_prefix("cnst:"))
235 .or_else(|| symbol_key.strip_prefix("cls:"))
236 .or_else(|| symbol_key.strip_prefix("fn:"))
237 .or_else(|| symbol_key.strip_prefix("gcnst:"))
238 .unwrap_or(symbol_key);
239 match stripped.split_once("::") {
240 Some((class, _)) => class,
241 None => stripped,
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum SymbolLookupError {
248 NotFound,
250 NoSourceLocation,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum LoadOutcome {
258 AlreadyLoaded,
260 Loaded,
263 NotResolvable,
267}
268
269impl LoadOutcome {
270 pub fn is_loaded(self) -> bool {
273 !matches!(self, LoadOutcome::NotResolvable)
274 }
275}
276
277pub trait ClassResolver: Send + Sync {
285 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
288}
289
290impl ClassResolver for composer::Psr4Map {
291 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
292 composer::Psr4Map::resolve(self, fqcn)
293 }
294}
295
296impl std::fmt::Display for SymbolLookupError {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 SymbolLookupError::NotFound => write!(f, "symbol not found"),
300 SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
301 }
302 }
303}
304
305impl std::error::Error for SymbolLookupError {}
306
307#[derive(Debug)]
310pub struct DependencyGraph {
311 files: Vec<Arc<str>>,
312 file_ids: FxHashMap<Arc<str>, u32>,
313 dependencies: Vec<Vec<u32>>,
315 dependents: Vec<Vec<u32>>,
317 legacy_dependencies: OnceLock<FxHashMap<String, Vec<String>>>,
318 legacy_dependents: OnceLock<FxHashMap<String, Vec<String>>>,
319}
320
321impl Clone for DependencyGraph {
322 fn clone(&self) -> Self {
323 Self {
324 files: self.files.clone(),
325 file_ids: self.file_ids.clone(),
326 dependencies: self.dependencies.clone(),
327 dependents: self.dependents.clone(),
328 legacy_dependencies: OnceLock::new(),
329 legacy_dependents: OnceLock::new(),
330 }
331 }
332}
333
334impl DependencyGraph {
335 pub(crate) fn from_compact_parts(
336 files: Vec<Arc<str>>,
337 file_ids: FxHashMap<Arc<str>, u32>,
338 dependencies: Vec<Vec<u32>>,
339 dependents: Vec<Vec<u32>>,
340 ) -> Self {
341 Self {
342 files,
343 file_ids,
344 dependencies,
345 dependents,
346 legacy_dependencies: OnceLock::new(),
347 legacy_dependents: OnceLock::new(),
348 }
349 }
350
351 pub fn file_count(&self) -> usize {
353 self.files.len()
354 }
355
356 pub fn dependency_edge_count(&self) -> usize {
358 self.dependencies.iter().map(Vec::len).sum()
359 }
360
361 pub fn dependent_edge_count(&self) -> usize {
363 self.dependents.iter().map(Vec::len).sum()
364 }
365
366 fn file_path(&self, id: u32) -> &Arc<str> {
367 &self.files[id as usize]
368 }
369
370 fn stringify_adjacency(&self, adjacency: &[Vec<u32>]) -> FxHashMap<String, Vec<String>> {
371 adjacency
372 .iter()
373 .enumerate()
374 .filter(|(_, deps)| !deps.is_empty())
375 .map(|(file_id, deps)| {
376 (
377 self.files[file_id].to_string(),
378 deps.iter()
379 .map(|&dep_id| self.file_path(dep_id).to_string())
380 .collect(),
381 )
382 })
383 .collect()
384 }
385
386 fn direct_arcs<'a>(
387 &'a self,
388 file: &str,
389 adjacency: &'a [Vec<u32>],
390 ) -> impl Iterator<Item = &'a Arc<str>> {
391 self.file_ids
392 .get(file)
393 .and_then(|&id| adjacency.get(id as usize))
394 .into_iter()
395 .flatten()
396 .map(|&id| self.file_path(id))
397 }
398
399 pub fn dependency_paths_of(&self, file: &str) -> Vec<Arc<str>> {
401 self.direct_arcs(file, &self.dependencies)
402 .cloned()
403 .collect()
404 }
405
406 pub fn dependent_paths_of(&self, file: &str) -> Vec<Arc<str>> {
408 self.direct_arcs(file, &self.dependents).cloned().collect()
409 }
410
411 pub fn dependencies_of(&self, file: &str) -> &[String] {
413 self.legacy_dependencies
414 .get_or_init(|| self.stringify_adjacency(&self.dependencies))
415 .get(file)
416 .map(|v| v.as_slice())
417 .unwrap_or(&[])
418 }
419
420 pub fn dependents_of(&self, file: &str) -> &[String] {
422 self.legacy_dependents
423 .get_or_init(|| self.stringify_adjacency(&self.dependents))
424 .get(file)
425 .map(|v| v.as_slice())
426 .unwrap_or(&[])
427 }
428
429 pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
431 let mut visited = rustc_hash::FxHashSet::default();
432 let Some(&seed) = self.file_ids.get(file) else {
433 return Vec::new();
434 };
435 let mut queue = vec![seed];
436 visited.insert(seed);
437 let mut result = Vec::new();
438
439 while let Some(current) = queue.pop() {
440 for &dep in &self.dependencies[current as usize] {
441 if visited.insert(dep) {
442 queue.push(dep);
443 result.push(self.file_path(dep).to_string());
444 }
445 }
446 }
447 result
448 }
449
450 pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
452 let mut visited = rustc_hash::FxHashSet::default();
453 let Some(&seed) = self.file_ids.get(file) else {
454 return Vec::new();
455 };
456 let mut queue = vec![seed];
457 visited.insert(seed);
458 let mut result = Vec::new();
459
460 while let Some(current) = queue.pop() {
461 for &dep in &self.dependents[current as usize] {
462 if visited.insert(dep) {
463 queue.push(dep);
464 result.push(self.file_path(dep).to_string());
465 }
466 }
467 }
468 result
469 }
470}
471
472pub mod symbol;
473pub use mir_codebase::definitions::{
474 DeclaredParam, FunctionDef, TagDescriptions, TemplateParam, Visibility,
475};
476pub use mir_issues::{Issue, IssueKind, Severity};
477pub use mir_types::Type;
478
479#[cfg(test)]
480mod dependency_graph_tests {
481 use super::*;
482
483 fn graph(edges: &[(&str, &[&str])]) -> DependencyGraph {
484 let mut files: Vec<Arc<str>> = edges
485 .iter()
486 .flat_map(|(from, deps)| {
487 std::iter::once(*from)
488 .chain(deps.iter().copied())
489 .map(Arc::<str>::from)
490 })
491 .collect();
492 files.sort();
493 files.dedup();
494
495 let file_ids: FxHashMap<Arc<str>, u32> = files
496 .iter()
497 .enumerate()
498 .map(|(id, file)| (file.clone(), id as u32))
499 .collect();
500 let mut dependencies = vec![Vec::new(); files.len()];
501 let mut dependents = vec![Vec::new(); files.len()];
502
503 for (from, deps) in edges {
504 let from_id = file_ids[*from];
505 for dep in *deps {
506 let dep_id = file_ids[*dep];
507 dependencies[from_id as usize].push(dep_id);
508 dependents[dep_id as usize].push(from_id);
509 }
510 }
511 for adjacency in [&mut dependencies, &mut dependents] {
512 for deps in adjacency {
513 deps.sort();
514 deps.dedup();
515 }
516 }
517
518 DependencyGraph::from_compact_parts(files, file_ids, dependencies, dependents)
519 }
520
521 #[test]
522 fn transitive_traversal_deduplicates_diamond_graphs() {
523 let graph = graph(&[
524 ("a.php", &["b.php", "c.php"]),
525 ("b.php", &["d.php"]),
526 ("c.php", &["d.php"]),
527 ]);
528
529 let deps = graph.transitive_dependencies("a.php");
530 assert_eq!(
531 deps.iter().filter(|path| path.as_str() == "d.php").count(),
532 1
533 );
534 assert_eq!(deps.len(), 3);
535
536 let dependents = graph.transitive_dependents("d.php");
537 assert_eq!(
538 dependents
539 .iter()
540 .filter(|path| path.as_str() == "a.php")
541 .count(),
542 1
543 );
544 assert_eq!(dependents.len(), 3);
545 }
546
547 #[test]
548 fn direct_arc_accessors_do_not_require_legacy_string_maps() {
549 let graph = graph(&[("consumer.php", &["service.php"])]);
550
551 let deps = graph.dependency_paths_of("consumer.php");
552 assert_eq!(deps, vec![Arc::<str>::from("service.php")]);
553 assert!(graph.legacy_dependencies.get().is_none());
554
555 let dependents = graph.dependent_paths_of("service.php");
556 assert_eq!(dependents, vec![Arc::<str>::from("consumer.php")]);
557 assert!(graph.legacy_dependents.get().is_none());
558
559 assert_eq!(
560 graph.dependencies_of("consumer.php"),
561 &["service.php".to_string()]
562 );
563 assert!(graph.legacy_dependencies.get().is_some());
564 }
565}
566
567pub fn location_from_span(
577 span: php_ast::Span,
578 file: std::sync::Arc<str>,
579 source: &str,
580 source_map: &php_rs_parser::source_map::SourceMap,
581) -> mir_types::Location {
582 let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
583 let (line_end, col_end) = if span.start < span.end {
584 diagnostics::offset_to_line_col(source, span.end, source_map)
585 } else {
586 (line, col_start)
587 };
588 mir_types::Location {
589 file,
590 line,
591 line_end,
592 col_start,
593 col_end: diagnostics::clamp_col_end(line, line_end, col_start, col_end),
594 }
595}
596pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
597
598pub mod composer;
599pub use composer::{ComposerError, Psr4Map};
600pub use include_graph::{include_closure, VendorTargets};
601pub use type_env::ScopeId;
602
603#[doc(hidden)]
604pub mod test_utils;