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 mod recursion;
38pub(crate) mod reference_key;
39pub mod session;
40pub mod source_provider;
41pub(crate) mod stmt;
42#[doc(hidden)]
43pub mod stub_cache;
44#[doc(hidden)]
45pub mod stubs;
46pub(crate) mod subtype;
47pub mod suppression;
48pub(crate) mod taint;
49mod tmp_suffix;
50pub(crate) mod type_env;
51pub(crate) mod util;
52
53pub use batch::{
54 analyze_source, analyze_source_with_options, dead_code_issue_kinds, discover_files,
55 discover_files_with_extensions, unused_variable_issue_kinds, AnalysisResult, BatchOptions,
56};
57pub use file_analyzer::{FileAnalysis, FileAnalyzer};
58pub use file_extensions::PhpFileExtensions;
59pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
60pub use parser::type_from_hint::type_from_hint;
61pub use parser::{DocblockParser, ParsedDocblock};
62pub use php_version::{ParsePhpVersionError, PhpVersion};
63pub use session::{AnalysisSession, AnalysisSnapshot, ReferenceIncludes, SubtypeClassSite};
64pub use source_provider::{FsSourceProvider, SourceProvider};
65
66pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
70 let w = written.trim_start_matches('\\');
71 let c = canonical.trim_start_matches('\\');
72 if w == c || !w.eq_ignore_ascii_case(c) {
73 return None;
74 }
75 let w_last = w.rsplit('\\').next().unwrap_or(w);
76 let c_last = c.rsplit('\\').next().unwrap_or(c);
77 if w_last != c_last {
78 let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
79 let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
80 if w_prefix == c_prefix {
81 return Some((w_last.to_string(), c_last.to_string()));
82 }
83 }
84 Some((w.to_string(), c.to_string()))
85}
86pub use stubs::{
87 is_builtin_constant, is_builtin_function, stub_files, stub_path_for_class,
88 ChainedClassResolver, StubClassResolver, StubVfs,
89};
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
113pub struct Position {
114 pub line: u32,
115 pub column: u32,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub struct Range {
121 pub start: Position,
122 pub end: Position,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
132pub enum Name {
133 Class(std::sync::Arc<str>),
135 Function(std::sync::Arc<str>),
137 Method {
139 class: std::sync::Arc<str>,
140 name: std::sync::Arc<str>,
141 },
142 Property {
144 class: std::sync::Arc<str>,
145 name: std::sync::Arc<str>,
146 },
147 ClassConstant {
149 class: std::sync::Arc<str>,
150 name: std::sync::Arc<str>,
151 },
152 GlobalConstant(std::sync::Arc<str>),
154}
155
156impl Name {
157 pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
160 Name::Method {
161 class: class.into(),
162 name: std::sync::Arc::from(name.to_ascii_lowercase()),
163 }
164 }
165
166 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
168 Name::Class(fqcn.into())
169 }
170
171 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
173 Name::Function(fqn.into())
174 }
175
176 pub fn property(
178 class: impl Into<std::sync::Arc<str>>,
179 name: impl Into<std::sync::Arc<str>>,
180 ) -> Self {
181 Name::Property {
182 class: class.into(),
183 name: name.into(),
184 }
185 }
186
187 pub fn class_constant(
189 class: impl Into<std::sync::Arc<str>>,
190 name: impl Into<std::sync::Arc<str>>,
191 ) -> Self {
192 Name::ClassConstant {
193 class: class.into(),
194 name: name.into(),
195 }
196 }
197
198 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
200 Name::GlobalConstant(fqn.into())
201 }
202
203 pub fn codebase_key(&self) -> String {
213 match self {
214 Name::Class(fqcn) => format!("cls:{fqcn}"),
215 Name::Function(fqn) => format!("fn:{fqn}"),
216 Name::Method { class, name } => format!("meth:{class}::{name}"),
217 Name::Property { class, name } => format!("prop:{class}::{name}"),
218 Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
219 Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
220 }
221 }
222}
223
224pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
232 let stripped = symbol_key
233 .strip_prefix("meth:")
234 .or_else(|| symbol_key.strip_prefix("prop:"))
235 .or_else(|| symbol_key.strip_prefix("cnst:"))
236 .or_else(|| symbol_key.strip_prefix("cls:"))
237 .or_else(|| symbol_key.strip_prefix("fn:"))
238 .or_else(|| symbol_key.strip_prefix("gcnst:"))
239 .unwrap_or(symbol_key);
240 match stripped.split_once("::") {
241 Some((class, _)) => class,
242 None => stripped,
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub enum SymbolLookupError {
249 NotFound,
251 NoSourceLocation,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum LoadOutcome {
259 AlreadyLoaded,
261 Loaded,
264 NotResolvable,
268}
269
270impl LoadOutcome {
271 pub fn is_loaded(self) -> bool {
274 !matches!(self, LoadOutcome::NotResolvable)
275 }
276}
277
278pub trait ClassResolver: Send + Sync {
286 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
289}
290
291impl ClassResolver for composer::Psr4Map {
292 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
293 composer::Psr4Map::resolve(self, fqcn)
294 }
295}
296
297impl std::fmt::Display for SymbolLookupError {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 SymbolLookupError::NotFound => write!(f, "symbol not found"),
301 SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
302 }
303 }
304}
305
306impl std::error::Error for SymbolLookupError {}
307
308#[derive(Debug)]
311pub struct DependencyGraph {
312 files: Vec<Arc<str>>,
313 file_ids: FxHashMap<Arc<str>, u32>,
314 dependencies: Vec<Vec<u32>>,
316 dependents: Vec<Vec<u32>>,
318 legacy_dependencies: OnceLock<FxHashMap<String, Vec<String>>>,
319 legacy_dependents: OnceLock<FxHashMap<String, Vec<String>>>,
320}
321
322impl Clone for DependencyGraph {
323 fn clone(&self) -> Self {
324 Self {
325 files: self.files.clone(),
326 file_ids: self.file_ids.clone(),
327 dependencies: self.dependencies.clone(),
328 dependents: self.dependents.clone(),
329 legacy_dependencies: OnceLock::new(),
330 legacy_dependents: OnceLock::new(),
331 }
332 }
333}
334
335impl DependencyGraph {
336 pub(crate) fn from_compact_parts(
337 files: Vec<Arc<str>>,
338 file_ids: FxHashMap<Arc<str>, u32>,
339 dependencies: Vec<Vec<u32>>,
340 dependents: Vec<Vec<u32>>,
341 ) -> Self {
342 Self {
343 files,
344 file_ids,
345 dependencies,
346 dependents,
347 legacy_dependencies: OnceLock::new(),
348 legacy_dependents: OnceLock::new(),
349 }
350 }
351
352 pub fn file_count(&self) -> usize {
354 self.files.len()
355 }
356
357 pub fn dependency_edge_count(&self) -> usize {
359 self.dependencies.iter().map(Vec::len).sum()
360 }
361
362 pub fn dependent_edge_count(&self) -> usize {
364 self.dependents.iter().map(Vec::len).sum()
365 }
366
367 fn file_path(&self, id: u32) -> &Arc<str> {
368 &self.files[id as usize]
369 }
370
371 fn stringify_adjacency(&self, adjacency: &[Vec<u32>]) -> FxHashMap<String, Vec<String>> {
372 adjacency
373 .iter()
374 .enumerate()
375 .filter(|(_, deps)| !deps.is_empty())
376 .map(|(file_id, deps)| {
377 (
378 self.files[file_id].to_string(),
379 deps.iter()
380 .map(|&dep_id| self.file_path(dep_id).to_string())
381 .collect(),
382 )
383 })
384 .collect()
385 }
386
387 fn direct_arcs<'a>(
388 &'a self,
389 file: &str,
390 adjacency: &'a [Vec<u32>],
391 ) -> impl Iterator<Item = &'a Arc<str>> {
392 self.file_ids
393 .get(file)
394 .and_then(|&id| adjacency.get(id as usize))
395 .into_iter()
396 .flatten()
397 .map(|&id| self.file_path(id))
398 }
399
400 pub fn dependency_paths_of(&self, file: &str) -> Vec<Arc<str>> {
402 self.direct_arcs(file, &self.dependencies)
403 .cloned()
404 .collect()
405 }
406
407 pub fn dependent_paths_of(&self, file: &str) -> Vec<Arc<str>> {
409 self.direct_arcs(file, &self.dependents).cloned().collect()
410 }
411
412 pub fn dependencies_of(&self, file: &str) -> &[String] {
414 self.legacy_dependencies
415 .get_or_init(|| self.stringify_adjacency(&self.dependencies))
416 .get(file)
417 .map(|v| v.as_slice())
418 .unwrap_or(&[])
419 }
420
421 pub fn dependents_of(&self, file: &str) -> &[String] {
423 self.legacy_dependents
424 .get_or_init(|| self.stringify_adjacency(&self.dependents))
425 .get(file)
426 .map(|v| v.as_slice())
427 .unwrap_or(&[])
428 }
429
430 pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
432 let mut visited = rustc_hash::FxHashSet::default();
433 let Some(&seed) = self.file_ids.get(file) else {
434 return Vec::new();
435 };
436 let mut queue = vec![seed];
437 visited.insert(seed);
438 let mut result = Vec::new();
439
440 while let Some(current) = queue.pop() {
441 for &dep in &self.dependencies[current as usize] {
442 if visited.insert(dep) {
443 queue.push(dep);
444 result.push(self.file_path(dep).to_string());
445 }
446 }
447 }
448 result
449 }
450
451 pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
453 let mut visited = rustc_hash::FxHashSet::default();
454 let Some(&seed) = self.file_ids.get(file) else {
455 return Vec::new();
456 };
457 let mut queue = vec![seed];
458 visited.insert(seed);
459 let mut result = Vec::new();
460
461 while let Some(current) = queue.pop() {
462 for &dep in &self.dependents[current as usize] {
463 if visited.insert(dep) {
464 queue.push(dep);
465 result.push(self.file_path(dep).to_string());
466 }
467 }
468 }
469 result
470 }
471}
472
473pub mod symbol;
474pub use mir_codebase::definitions::{
475 DeclaredParam, FunctionDef, TagDescriptions, TemplateParam, Visibility,
476};
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 include_graph::{include_closure, VendorTargets};
602pub use type_env::ScopeId;
603
604#[doc(hidden)]
605pub mod test_utils;