mir_analyzer/
dead_code.rs1use mir_codebase::storage::Visibility;
13use mir_codebase::Codebase;
14use mir_issues::{Issue, IssueKind, Location, Severity};
15
16use crate::stubs::StubVfs;
17
18const MAGIC_METHODS: &[&str] = &[
20 "__construct",
21 "__destruct",
22 "__call",
23 "__callstatic",
24 "__get",
25 "__set",
26 "__isset",
27 "__unset",
28 "__sleep",
29 "__wakeup",
30 "__serialize",
31 "__unserialize",
32 "__tostring",
33 "__invoke",
34 "__set_state",
35 "__clone",
36 "__debuginfo",
37];
38
39pub struct DeadCodeAnalyzer<'a> {
40 codebase: &'a Codebase,
41}
42
43impl<'a> DeadCodeAnalyzer<'a> {
44 pub fn new(codebase: &'a Codebase) -> Self {
45 Self { codebase }
46 }
47
48 pub fn analyze(&self) -> Vec<Issue> {
49 let mut issues = Vec::new();
50
51 for entry in self.codebase.classes.iter() {
53 let cls = entry.value();
54 let fqcn = cls.fqcn.as_ref();
55
56 for (method_name, method) in &cls.own_methods {
57 if method.visibility != Visibility::Private {
58 continue;
59 }
60 let name = method_name.as_ref();
61 if MAGIC_METHODS.contains(&name) {
62 continue;
63 }
64 if !self.codebase.is_method_referenced(fqcn, name) {
65 let (file, line) = location_from_storage(&method.location);
66 issues.push(Issue::new(
67 IssueKind::UnusedMethod {
68 class: fqcn.to_string(),
69 method: name.to_string(),
70 },
71 Location {
72 file,
73 line,
74 line_end: line,
75 col_start: 0,
76 col_end: 0,
77 },
78 ));
79 }
80 }
81
82 for (prop_name, prop) in &cls.own_properties {
83 if prop.visibility != Visibility::Private {
84 continue;
85 }
86 let name = prop_name.as_ref();
87 if !self.codebase.is_property_referenced(fqcn, name) {
88 let (file, line) = location_from_storage(&prop.location);
89 issues.push(Issue::new(
90 IssueKind::UnusedProperty {
91 class: fqcn.to_string(),
92 property: name.to_string(),
93 },
94 Location {
95 file,
96 line,
97 line_end: line,
98 col_start: 0,
99 col_end: 0,
100 },
101 ));
102 }
103 }
104 }
105
106 let stub_vfs = StubVfs::new();
108 for entry in self.codebase.functions.iter() {
109 let func = entry.value();
110 let fqn = func.fqn.as_ref();
111 if let Some(loc) = &func.location {
114 if stub_vfs.is_stub_file(loc.file.as_ref()) {
115 continue;
116 }
117 }
118 if !self.codebase.is_function_referenced(fqn) {
119 let (file, line) = location_from_storage(&func.location);
120 issues.push(Issue::new(
121 IssueKind::UnusedFunction {
122 name: func.short_name.to_string(),
123 },
124 Location {
125 file,
126 line,
127 line_end: line,
128 col_start: 0,
129 col_end: 0,
130 },
131 ));
132 }
133 }
134
135 for issue in &mut issues {
137 issue.severity = Severity::Info;
138 }
139
140 issues
141 }
142}
143
144fn location_from_storage(
145 loc: &Option<mir_codebase::storage::Location>,
146) -> (std::sync::Arc<str>, u32) {
147 match loc {
148 Some(l) => (l.file.clone(), 1), None => (std::sync::Arc::from("<unknown>"), 1),
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::project::ProjectAnalyzer;
157
158 #[test]
159 fn builtin_functions_not_flagged_as_unused() {
160 let analyzer = ProjectAnalyzer::new();
165 analyzer.load_stubs();
166 let issues = DeadCodeAnalyzer::new(analyzer.codebase()).analyze();
167 let builtin_false_positives: Vec<_> = issues
168 .iter()
169 .filter(|i| {
170 matches!(&i.kind, IssueKind::UnusedFunction { name } if
171 matches!(name.as_str(), "strlen" | "array_map" | "json_encode" | "preg_match")
172 )
173 })
174 .collect();
175 assert!(
176 builtin_false_positives.is_empty(),
177 "Expected no UnusedFunction for PHP builtins, got: {:?}",
178 builtin_false_positives
179 .iter()
180 .map(|i| i.kind.message())
181 .collect::<Vec<_>>()
182 );
183 }
184}