1use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use mir_codebase::storage::{MethodStorage, Visibility};
14use mir_codebase::Codebase;
15use mir_issues::{Issue, IssueKind, Location};
16
17pub struct ClassAnalyzer<'a> {
22 codebase: &'a Codebase,
23 analyzed_files: HashSet<Arc<str>>,
25 sources: HashMap<Arc<str>, &'a str>,
27}
28
29impl<'a> ClassAnalyzer<'a> {
30 pub fn new(codebase: &'a Codebase) -> Self {
31 Self {
32 codebase,
33 analyzed_files: HashSet::new(),
34 sources: HashMap::new(),
35 }
36 }
37
38 pub fn with_files(
39 codebase: &'a Codebase,
40 files: HashSet<Arc<str>>,
41 file_data: &'a [(Arc<str>, String)],
42 ) -> Self {
43 let sources: HashMap<Arc<str>, &'a str> = file_data
44 .iter()
45 .map(|(f, s)| (f.clone(), s.as_str()))
46 .collect();
47 Self {
48 codebase,
49 analyzed_files: files,
50 sources,
51 }
52 }
53
54 pub fn analyze_all(&self) -> Vec<Issue> {
56 let mut issues = Vec::new();
57
58 let class_keys: Vec<Arc<str>> = self
59 .codebase
60 .classes
61 .iter()
62 .map(|e| e.key().clone())
63 .collect();
64
65 for fqcn in &class_keys {
66 let cls = match self.codebase.classes.get(fqcn.as_ref()) {
67 Some(c) => c,
68 None => continue,
69 };
70
71 if !self.analyzed_files.is_empty() {
73 let in_analyzed = cls
74 .location
75 .as_ref()
76 .map(|loc| self.analyzed_files.contains(&loc.file))
77 .unwrap_or(false);
78 if !in_analyzed {
79 continue;
80 }
81 }
82
83 if let Some(parent_fqcn) = &cls.parent {
85 if let Some(parent) = self.codebase.classes.get(parent_fqcn.as_ref()) {
86 if parent.is_final {
87 let loc = issue_location(cls.location.as_ref(), fqcn);
88 let mut issue = Issue::new(
89 IssueKind::FinalClassExtended {
90 parent: parent_fqcn.to_string(),
91 child: fqcn.to_string(),
92 },
93 loc,
94 );
95 if let Some(snippet) = extract_snippet(cls.location.as_ref(), &self.sources)
96 {
97 issue = issue.with_snippet(snippet);
98 }
99 issues.push(issue);
100 }
101 }
102 }
103
104 if cls.is_abstract {
106 self.check_overrides(&cls, &mut issues);
108 continue;
109 }
110
111 self.check_abstract_methods_implemented(&cls, &mut issues);
113
114 self.check_interface_methods_implemented(&cls, &mut issues);
116
117 self.check_overrides(&cls, &mut issues);
119 }
120
121 issues
122 }
123
124 fn check_abstract_methods_implemented(
129 &self,
130 cls: &mir_codebase::storage::ClassStorage,
131 issues: &mut Vec<Issue>,
132 ) {
133 let fqcn = &cls.fqcn;
134
135 for ancestor_fqcn in &cls.all_parents {
137 let ancestor = match self.codebase.classes.get(ancestor_fqcn.as_ref()) {
138 Some(a) => a,
139 None => continue,
140 };
141
142 for (method_name, method) in &ancestor.own_methods {
143 if !method.is_abstract {
144 continue;
145 }
146
147 if cls
149 .get_method(method_name.as_ref())
150 .map(|m| !m.is_abstract)
151 .unwrap_or(false)
152 {
153 continue; }
155
156 let loc = issue_location(cls.location.as_ref(), fqcn);
157 let mut issue = Issue::new(
158 IssueKind::UnimplementedAbstractMethod {
159 class: fqcn.to_string(),
160 method: method_name.to_string(),
161 },
162 loc,
163 );
164 if let Some(snippet) = extract_snippet(cls.location.as_ref(), &self.sources) {
165 issue = issue.with_snippet(snippet);
166 }
167 issues.push(issue);
168 }
169 }
170 }
171
172 fn check_interface_methods_implemented(
177 &self,
178 cls: &mir_codebase::storage::ClassStorage,
179 issues: &mut Vec<Issue>,
180 ) {
181 let fqcn = &cls.fqcn;
182
183 let all_ifaces: Vec<Arc<str>> = cls
185 .all_parents
186 .iter()
187 .filter(|p| self.codebase.interfaces.contains_key(p.as_ref()))
188 .cloned()
189 .collect();
190
191 for iface_fqcn in &all_ifaces {
192 let iface = match self.codebase.interfaces.get(iface_fqcn.as_ref()) {
193 Some(i) => i,
194 None => continue,
195 };
196
197 for (method_name, _method) in &iface.own_methods {
198 let implemented = cls
200 .get_method(method_name.as_ref())
201 .map(|m| !m.is_abstract)
202 .unwrap_or(false);
203
204 if !implemented {
205 let loc = issue_location(cls.location.as_ref(), fqcn);
206 let mut issue = Issue::new(
207 IssueKind::UnimplementedInterfaceMethod {
208 class: fqcn.to_string(),
209 interface: iface_fqcn.to_string(),
210 method: method_name.to_string(),
211 },
212 loc,
213 );
214 if let Some(snippet) = extract_snippet(cls.location.as_ref(), &self.sources) {
215 issue = issue.with_snippet(snippet);
216 }
217 issues.push(issue);
218 }
219 }
220 }
221 }
222
223 fn check_overrides(&self, cls: &mir_codebase::storage::ClassStorage, issues: &mut Vec<Issue>) {
228 let fqcn = &cls.fqcn;
229
230 for (method_name, own_method) in &cls.own_methods {
231 if method_name.as_ref() == "__construct" {
233 continue;
234 }
235
236 let parent_method = self.find_parent_method(cls, method_name.as_ref());
238
239 let parent = match parent_method {
240 Some(m) => m,
241 None => continue, };
243
244 let loc = issue_location(own_method.location.as_ref(), fqcn);
245
246 if parent.is_final {
248 let mut issue = Issue::new(
249 IssueKind::FinalMethodOverridden {
250 class: fqcn.to_string(),
251 method: method_name.to_string(),
252 parent: parent.fqcn.to_string(),
253 },
254 loc.clone(),
255 );
256 if let Some(snippet) = extract_snippet(own_method.location.as_ref(), &self.sources)
257 {
258 issue = issue.with_snippet(snippet);
259 }
260 issues.push(issue);
261 }
262
263 if visibility_reduced(own_method.visibility, parent.visibility) {
265 let mut issue = Issue::new(
266 IssueKind::OverriddenMethodAccess {
267 class: fqcn.to_string(),
268 method: method_name.to_string(),
269 },
270 loc.clone(),
271 );
272 if let Some(snippet) = extract_snippet(own_method.location.as_ref(), &self.sources)
273 {
274 issue = issue.with_snippet(snippet);
275 }
276 issues.push(issue);
277 }
278
279 if let (Some(child_ret), Some(parent_ret)) =
286 (&own_method.return_type, &parent.return_type)
287 {
288 let parent_from_docblock = parent_ret.from_docblock;
289 let involves_named_objects = self.type_has_named_objects(child_ret)
290 || self.type_has_named_objects(parent_ret);
291 let involves_self_static = self.type_has_self_or_static(child_ret)
292 || self.type_has_self_or_static(parent_ret);
293
294 if !parent_from_docblock
295 && !involves_named_objects
296 && !involves_self_static
297 && !child_ret.is_subtype_of_simple(parent_ret)
298 && !parent_ret.is_mixed()
299 && !child_ret.is_mixed()
300 && !self.return_type_has_template(parent_ret)
301 {
302 issues.push(
303 Issue::new(
304 IssueKind::MethodSignatureMismatch {
305 class: fqcn.to_string(),
306 method: method_name.to_string(),
307 detail: format!(
308 "return type '{}' is not a subtype of parent '{}'",
309 child_ret, parent_ret
310 ),
311 },
312 loc.clone(),
313 )
314 .with_snippet(method_name.to_string()),
315 );
316 }
317 }
318
319 let parent_required = parent
321 .params
322 .iter()
323 .filter(|p| !p.is_optional && !p.is_variadic)
324 .count();
325 let child_required = own_method
326 .params
327 .iter()
328 .filter(|p| !p.is_optional && !p.is_variadic)
329 .count();
330
331 if child_required > parent_required {
332 issues.push(
333 Issue::new(
334 IssueKind::MethodSignatureMismatch {
335 class: fqcn.to_string(),
336 method: method_name.to_string(),
337 detail: format!(
338 "overriding method requires {} argument(s) but parent requires {}",
339 child_required, parent_required
340 ),
341 },
342 loc.clone(),
343 )
344 .with_snippet(method_name.to_string()),
345 );
346 }
347
348 let shared_len = parent.params.len().min(own_method.params.len());
359 for i in 0..shared_len {
360 let parent_param = &parent.params[i];
361 let child_param = &own_method.params[i];
362
363 let (parent_ty, child_ty) = match (&parent_param.ty, &child_param.ty) {
364 (Some(p), Some(c)) => (p, c),
365 _ => continue,
366 };
367
368 if parent_ty.is_mixed()
369 || child_ty.is_mixed()
370 || self.type_has_named_objects(parent_ty)
371 || self.type_has_named_objects(child_ty)
372 || self.type_has_self_or_static(parent_ty)
373 || self.type_has_self_or_static(child_ty)
374 || self.return_type_has_template(parent_ty)
375 || self.return_type_has_template(child_ty)
376 {
377 continue;
378 }
379
380 if !parent_ty.is_subtype_of_simple(child_ty) {
383 issues.push(
384 Issue::new(
385 IssueKind::MethodSignatureMismatch {
386 class: fqcn.to_string(),
387 method: method_name.to_string(),
388 detail: format!(
389 "parameter ${} type '{}' is narrower than parent type '{}'",
390 child_param.name, child_ty, parent_ty
391 ),
392 },
393 loc.clone(),
394 )
395 .with_snippet(method_name.to_string()),
396 );
397 break; }
399 }
400 }
401 }
402
403 fn return_type_has_template(&self, ty: &mir_types::Union) -> bool {
411 use mir_types::Atomic;
412 ty.types.iter().any(|atomic| match atomic {
413 Atomic::TTemplateParam { .. } => true,
414 Atomic::TClassString(Some(inner)) => !self.codebase.type_exists(inner.as_ref()),
415 Atomic::TNamedObject { fqcn, type_params } => {
416 (!fqcn.contains('\\') && !self.codebase.type_exists(fqcn.as_ref()))
418 || type_params.iter().any(|tp| self.return_type_has_template(tp))
420 }
421 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
422 self.return_type_has_template(key) || self.return_type_has_template(value)
423 }
424 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
425 self.return_type_has_template(value)
426 }
427 _ => false,
428 })
429 }
430
431 fn type_has_named_objects(&self, ty: &mir_types::Union) -> bool {
436 use mir_types::Atomic;
437 ty.types.iter().any(|a| match a {
438 Atomic::TNamedObject { .. } => true,
439 Atomic::TArray { key, value } | Atomic::TNonEmptyArray { key, value } => {
440 self.type_has_named_objects(key) || self.type_has_named_objects(value)
441 }
442 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
443 self.type_has_named_objects(value)
444 }
445 _ => false,
446 })
447 }
448
449 fn type_has_self_or_static(&self, ty: &mir_types::Union) -> bool {
452 use mir_types::Atomic;
453 ty.types
454 .iter()
455 .any(|a| matches!(a, Atomic::TSelf { .. } | Atomic::TStaticObject { .. }))
456 }
457
458 fn find_parent_method(
460 &self,
461 cls: &mir_codebase::storage::ClassStorage,
462 method_name: &str,
463 ) -> Option<MethodStorage> {
464 for ancestor_fqcn in &cls.all_parents {
466 if let Some(ancestor_cls) = self.codebase.classes.get(ancestor_fqcn.as_ref()) {
467 if let Some(m) = ancestor_cls.own_methods.get(method_name) {
468 return Some(m.clone());
469 }
470 } else if let Some(iface) = self.codebase.interfaces.get(ancestor_fqcn.as_ref()) {
471 if let Some(m) = iface.own_methods.get(method_name) {
472 return Some(m.clone());
473 }
474 }
475 }
476 None
477 }
478}
479
480fn visibility_reduced(child_vis: Visibility, parent_vis: Visibility) -> bool {
486 matches!(
489 (parent_vis, child_vis),
490 (Visibility::Public, Visibility::Protected)
491 | (Visibility::Public, Visibility::Private)
492 | (Visibility::Protected, Visibility::Private)
493 )
494}
495
496fn issue_location(
499 storage_loc: Option<&mir_codebase::storage::Location>,
500 fqcn: &Arc<str>,
501) -> Location {
502 match storage_loc {
503 Some(loc) => Location {
504 file: loc.file.clone(),
505 line: loc.line,
506 col_start: loc.col,
507 col_end: loc.col,
508 },
509 None => Location {
510 file: fqcn.clone(),
511 line: 1,
512 col_start: 0,
513 col_end: 0,
514 },
515 }
516}
517
518fn extract_snippet(
520 storage_loc: Option<&mir_codebase::storage::Location>,
521 sources: &HashMap<Arc<str>, &str>,
522) -> Option<String> {
523 let loc = storage_loc?;
524 let src = *sources.get(&loc.file)?;
525 let start = loc.start as usize;
526 let end = loc.end as usize;
527 if start >= src.len() {
528 return None;
529 }
530 let end = end.min(src.len());
531 let span_text = &src[start..end];
532 let first_line = span_text.lines().next().unwrap_or(span_text);
534 Some(first_line.trim().to_string())
535}