1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use std::sync::Arc;
use mir_codebase::storage::{ConstantDef, EnumCaseDef};
use mir_issues::{Issue, IssueKind};
use mir_types::Type;
use php_ast::owned::EnumMemberKind;
use super::DefinitionCollector;
use crate::parser::name_to_string_owned;
impl DefinitionCollector<'_> {
pub(super) fn collect_enum(
&mut self,
decl: &php_ast::owned::EnumDecl,
stmt_span: php_ast::Span,
) {
let enum_name = decl.name.as_deref().unwrap_or_default().to_string();
let fqcn = self.resolve_name(&enum_name);
let enum_doc = decl
.doc_comment
.as_ref()
.map(|c| crate::parser::DocblockParser::parse(&c.text))
.unwrap_or_default();
let scalar_type = decl
.scalar_type
.as_ref()
.map(|n| crate::parser::docblock::parse_type_string(&name_to_string_owned(n)));
let mut interfaces: Vec<Arc<str>> = decl
.implements
.iter()
.map(|n| self.resolve_name(&name_to_string_owned(n)).into())
.collect();
// PHP automatically makes every enum implement UnitEnum (and backed enums
// implement IntBackedEnum / StringBackedEnum, which extend BackedEnum → UnitEnum).
// Inject the appropriate interface so method resolution finds cases() / from() /
// tryFrom() in the stubs without the user having to write `implements BackedEnum`.
let implicit_iface: Arc<str> = match decl.scalar_type.as_ref() {
None => Arc::from("UnitEnum"),
Some(n) if name_to_string_owned(n).eq_ignore_ascii_case("string") => {
Arc::from("StringBackedEnum")
}
_ => Arc::from("IntBackedEnum"),
};
if !interfaces.contains(&implicit_iface) {
interfaces.push(implicit_iface);
}
let mut cases = indexmap::IndexMap::new();
let mut own_methods = indexmap::IndexMap::new();
let mut own_constants = indexmap::IndexMap::new();
for member in decl.body.members.iter() {
match &member.kind {
EnumMemberKind::Case(c) => {
let case_name = c.name.as_deref().unwrap_or_default();
let case_doc = c
.doc_comment
.as_ref()
.map(|d| crate::parser::DocblockParser::parse(&d.text))
.unwrap_or_default();
let case_deprecated =
case_doc.deprecated.as_deref().map(Arc::from).or_else(|| {
if c.attributes.iter().any(|a| {
a.name
.parts
.last()
.map(|p| p.as_ref().eq_ignore_ascii_case("Deprecated"))
.unwrap_or(false)
}) {
Some(Arc::from(""))
} else {
None
}
});
// Extract the literal type from the case value expression (e.g. `= 'foo'`, `= 42`).
// Falls back to mixed when the expression is not a simple literal (class constant,
// const expression) — those can't be statically checked at collection time.
let value_ty = c
.value
.as_ref()
.and_then(|expr| super::infer_const_value(&expr.kind));
// Validate the case value's type against the backing scalar type.
if let (Some(backing), Some(case_val_ty)) = (&scalar_type, &value_ty) {
if !case_val_ty.is_subtype_structural(backing) {
let lc = self.source_map.offset_to_line_col(member.span.start);
let line = lc.line + 1;
self.issues.add(Issue::new(
IssueKind::BackedEnumCaseTypeMismatch {
enum_name: fqcn.clone(),
case_name: case_name.to_string(),
expected: backing.to_string(),
actual: case_val_ty.to_string(),
},
mir_issues::Location {
file: self.file.clone(),
line,
line_end: line,
col_start: 0,
col_end: 0,
},
));
}
}
cases.insert(
Arc::from(case_name),
EnumCaseDef {
name: Arc::from(case_name),
// Store the inferred literal type; fall back to mixed for non-literal values.
value: Some(value_ty.unwrap_or_else(Type::mixed)),
location: Some(self.location(member.span.start, member.span.end)),
deprecated: case_deprecated,
},
);
}
EnumMemberKind::Method(m) => {
if let Some(method) =
self.build_method_storage(m, &fqcn, Some(&member.span), None, &[])
{
own_methods.insert(
Arc::from(crate::util::php_ident_lowercase(&method.name).as_str()),
Arc::new(method),
);
}
}
EnumMemberKind::ClassConst(c) => {
let const_name = c.name.as_deref().unwrap_or_default();
own_constants.insert(
Arc::from(const_name),
ConstantDef {
name: Arc::from(const_name),
ty: Type::mixed(),
visibility: None,
is_final: c.is_final,
location: Some(self.location(member.span.start, member.span.end)),
deprecated: None,
},
);
}
EnumMemberKind::TraitUse(_) => {}
}
}
let type_aliases = self.build_type_aliases(&enum_doc);
let mut dummy_properties = indexmap::IndexMap::new();
self.add_docblock_members(
&enum_doc,
&type_aliases,
&fqcn,
&mut own_methods,
&mut dummy_properties,
Some(self.location(stmt_span.start, stmt_span.end)),
);
self.slice
.enums
.push(std::sync::Arc::new(mir_codebase::EnumDef {
fqcn: fqcn.into(),
short_name: Arc::from(enum_name.as_str()),
scalar_type,
interfaces,
cases,
own_methods,
own_constants,
location: Some(self.location(stmt_span.start, stmt_span.end)),
}));
}
}