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