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