Skip to main content

mir_analyzer/
dead_code.rs

1/// Dead-code detector (M18).
2///
3/// After Pass 2 has recorded all method/property/function references into the
4/// codebase, this analyzer walks every class and reports:
5///
6/// - `UnusedMethod`   — private method that is never called
7/// - `UnusedProperty` — private property that is never read
8/// - `UnusedFunction` — non-public free function that is never called
9///
10/// Magic methods (`__construct`, `__destruct`, `__toString`, etc.) and
11/// constructors are excluded because they are called implicitly.
12use mir_codebase::storage::Visibility;
13use mir_codebase::Codebase;
14use mir_issues::{Issue, IssueKind, Location, Severity};
15
16// Magic PHP methods that are invoked implicitly — never flag these as unused.
17const MAGIC_METHODS: &[&str] = &[
18    "__construct",
19    "__destruct",
20    "__call",
21    "__callStatic",
22    "__get",
23    "__set",
24    "__isset",
25    "__unset",
26    "__sleep",
27    "__wakeup",
28    "__serialize",
29    "__unserialize",
30    "__toString",
31    "__invoke",
32    "__set_state",
33    "__clone",
34    "__debugInfo",
35];
36
37pub struct DeadCodeAnalyzer<'a> {
38    codebase: &'a Codebase,
39}
40
41impl<'a> DeadCodeAnalyzer<'a> {
42    pub fn new(codebase: &'a Codebase) -> Self {
43        Self { codebase }
44    }
45
46    pub fn analyze(&self) -> Vec<Issue> {
47        let mut issues = Vec::new();
48
49        // --- Private methods / properties on classes ---
50        for entry in self.codebase.classes.iter() {
51            let cls = entry.value();
52            let fqcn = cls.fqcn.as_ref();
53
54            for (method_name, method) in &cls.own_methods {
55                if method.visibility != Visibility::Private {
56                    continue;
57                }
58                let name = method_name.as_ref();
59                if MAGIC_METHODS.contains(&name) {
60                    continue;
61                }
62                if !self.codebase.is_method_referenced(fqcn, name) {
63                    let (file, line) = location_from_storage(&method.location);
64                    issues.push(Issue::new(
65                        IssueKind::UnusedMethod {
66                            class: fqcn.to_string(),
67                            method: name.to_string(),
68                        },
69                        Location {
70                            file,
71                            line,
72                            col_start: 0,
73                            col_end: 0,
74                        },
75                    ));
76                }
77            }
78
79            for (prop_name, prop) in &cls.own_properties {
80                if prop.visibility != Visibility::Private {
81                    continue;
82                }
83                let name = prop_name.as_ref();
84                if !self.codebase.is_property_referenced(fqcn, name) {
85                    let (file, line) = location_from_storage(&prop.location);
86                    issues.push(Issue::new(
87                        IssueKind::UnusedProperty {
88                            class: fqcn.to_string(),
89                            property: name.to_string(),
90                        },
91                        Location {
92                            file,
93                            line,
94                            col_start: 0,
95                            col_end: 0,
96                        },
97                    ));
98                }
99            }
100        }
101
102        // Downgrade all dead-code issues to Info
103        for issue in &mut issues {
104            issue.severity = Severity::Info;
105        }
106
107        issues
108    }
109}
110
111fn location_from_storage(
112    loc: &Option<mir_codebase::storage::Location>,
113) -> (std::sync::Arc<str>, u32) {
114    match loc {
115        Some(l) => (l.file.clone(), 1), // byte offset → line mapping not available here
116        None => (std::sync::Arc::from("<unknown>"), 1),
117    }
118}