Skip to content

Commit 591bd85

Browse files
committed
Add implicit interfaces on enums
1 parent cd1ffac commit 591bd85

5 files changed

Lines changed: 165 additions & 1 deletion

File tree

src/completion/builder.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,28 @@ impl Backend {
8989
) -> Vec<CompletionItem> {
9090
let mut items: Vec<CompletionItem> = Vec::new();
9191

92+
// ─── Synthetic BackedEnum properties ────────────────────────────────
93+
// If this is a BackedEnum, synthesize `name` and `value` for instance access.
94+
let mut synthetic_properties: Vec<PropertyInfo> = Vec::new();
95+
if target_class
96+
.implemented_interfaces
97+
.contains(&"\\BackedEnum".to_string())
98+
&& matches!(access_kind, AccessKind::Arrow | AccessKind::Other)
99+
{
100+
synthetic_properties.push(PropertyInfo {
101+
name: "name".to_string(),
102+
type_hint: Some("string".to_string()),
103+
is_static: false,
104+
visibility: Visibility::Public,
105+
});
106+
synthetic_properties.push(PropertyInfo {
107+
name: "value".to_string(),
108+
type_hint: Some("string|int".to_string()),
109+
is_static: false,
110+
visibility: Visibility::Public,
111+
});
112+
}
113+
92114
// Methods — filtered by static / instance, excluding magic methods
93115
for method in &target_class.methods {
94116
if Self::is_magic_method(&method.name) {
@@ -125,7 +147,11 @@ impl Backend {
125147
}
126148

127149
// Properties — filtered by static / instance
128-
for property in &target_class.properties {
150+
// First, add synthetic BackedEnum properties (if any)
151+
for property in synthetic_properties
152+
.iter()
153+
.chain(target_class.properties.iter())
154+
{
129155
// parent:: excludes private members
130156
if access_kind == AccessKind::ParentDoubleColon
131157
&& property.visibility == Visibility::Private

src/parser.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ impl Backend {
566566
parent_class,
567567
used_traits,
568568
mixins,
569+
implemented_interfaces: Vec::new(),
569570
});
570571
}
571572
Statement::Interface(iface) => {
@@ -624,6 +625,7 @@ impl Backend {
624625
parent_class,
625626
used_traits,
626627
mixins,
628+
implemented_interfaces: Vec::new(),
627629
});
628630
}
629631
Statement::Trait(trait_def) => {
@@ -678,6 +680,7 @@ impl Backend {
678680
parent_class: None,
679681
used_traits,
680682
mixins,
683+
implemented_interfaces: Vec::new(),
681684
});
682685
}
683686
Statement::Enum(enum_def) => {
@@ -722,6 +725,13 @@ impl Backend {
722725
let start_offset = enum_def.left_brace.start.offset;
723726
let end_offset = enum_def.right_brace.end.offset;
724727

728+
// Determine if the enum is backed (has a type) or is a unit enum.
729+
let implemented_interfaces = if enum_def.backing_type_hint.is_some() {
730+
vec!["\\BackedEnum".to_string()]
731+
} else {
732+
vec!["\\UnitEnum".to_string()]
733+
};
734+
725735
classes.push(ClassInfo {
726736
name: enum_name,
727737
methods,
@@ -732,6 +742,7 @@ impl Backend {
732742
parent_class,
733743
used_traits,
734744
mixins,
745+
implemented_interfaces,
735746
});
736747
}
737748
Statement::Namespace(namespace) => {

src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,6 @@ pub struct ClassInfo {
210210
/// classes via magic methods (`__call`, `__get`, `__set`, etc.).
211211
/// Resolved to fully-qualified names during post-processing.
212212
pub mixins: Vec<String>,
213+
/// Interfaces implemented by this class or enum (including implicit BackedEnum/UnitEnum for enums).
214+
pub implemented_interfaces: Vec<String>,
213215
}

tests/completion_enums.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,73 @@ use common::{create_psr4_workspace, create_test_backend};
44
use tower_lsp::LanguageServer;
55
use tower_lsp::lsp_types::*;
66

7+
/// Test: BackedEnum completion includes `name` and `value` properties.
8+
#[tokio::test]
9+
async fn test_backed_enum_completion_includes_name_and_value() {
10+
let backend = create_test_backend();
11+
12+
let uri = Url::parse("file:///backed_enum.php").unwrap();
13+
let text = concat!(
14+
"<?php\n",
15+
"enum Status: string {\n",
16+
" case Active = 'active';\n",
17+
" public function label(): string {\n",
18+
" $this->\n",
19+
" }\n",
20+
"}\n",
21+
);
22+
23+
backend
24+
.did_open(DidOpenTextDocumentParams {
25+
text_document: TextDocumentItem {
26+
uri: uri.clone(),
27+
language_id: "php".to_string(),
28+
version: 1,
29+
text: text.to_string(),
30+
},
31+
})
32+
.await;
33+
34+
let result = backend
35+
.completion(CompletionParams {
36+
text_document_position: TextDocumentPositionParams {
37+
text_document: TextDocumentIdentifier { uri },
38+
position: Position {
39+
line: 4,
40+
character: 15,
41+
},
42+
},
43+
work_done_progress_params: WorkDoneProgressParams::default(),
44+
partial_result_params: PartialResultParams::default(),
45+
context: None,
46+
})
47+
.await
48+
.unwrap();
49+
50+
assert!(result.is_some(), "Completion should return results");
51+
match result.unwrap() {
52+
CompletionResponse::Array(items) => {
53+
let property_names: Vec<&str> = items
54+
.iter()
55+
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
56+
.map(|i| i.filter_text.as_deref().unwrap())
57+
.collect();
58+
59+
assert!(
60+
property_names.contains(&"name"),
61+
"BackedEnum completion should include 'name' property, got: {:?}",
62+
property_names
63+
);
64+
assert!(
65+
property_names.contains(&"value"),
66+
"BackedEnum completion should include 'value' property, got: {:?}",
67+
property_names
68+
);
69+
}
70+
_ => panic!("Expected CompletionResponse::Array"),
71+
}
72+
}
73+
774
// ─── Basic enum case completion via :: ──────────────────────────────────────
875

976
/// Test: Completing on `EnumName::` should show enum cases as constants.

tests/parser.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,64 @@ async fn test_parse_php_extracts_properties() {
107107
);
108108
}
109109

110+
#[tokio::test]
111+
async fn test_enum_implemented_interfaces() {
112+
let backend = create_test_backend();
113+
114+
// Backed enum (should implement \BackedEnum)
115+
let php_backed = r#"
116+
<?php
117+
enum Status: int {
118+
case Active = 1;
119+
case Inactive = 2;
120+
}
121+
"#;
122+
123+
let classes = backend.parse_php(php_backed);
124+
assert_eq!(classes.len(), 1);
125+
let enum_info = &classes[0];
126+
assert_eq!(enum_info.name, "Status");
127+
assert!(
128+
enum_info
129+
.implemented_interfaces
130+
.contains(&"\\BackedEnum".to_string()),
131+
"Backed enum should implement \\BackedEnum"
132+
);
133+
assert!(
134+
!enum_info
135+
.implemented_interfaces
136+
.contains(&"\\UnitEnum".to_string()),
137+
"Backed enum should not implement \\UnitEnum"
138+
);
139+
140+
// Unit enum (should implement \UnitEnum)
141+
let php_unit = r#"
142+
<?php
143+
enum Color {
144+
case Red;
145+
case Green;
146+
case Blue;
147+
}
148+
"#;
149+
150+
let classes = backend.parse_php(php_unit);
151+
assert_eq!(classes.len(), 1);
152+
let enum_info = &classes[0];
153+
assert_eq!(enum_info.name, "Color");
154+
assert!(
155+
enum_info
156+
.implemented_interfaces
157+
.contains(&"\\UnitEnum".to_string()),
158+
"Unit enum should implement \\UnitEnum"
159+
);
160+
assert!(
161+
!enum_info
162+
.implemented_interfaces
163+
.contains(&"\\BackedEnum".to_string()),
164+
"Unit enum should not implement \\BackedEnum"
165+
);
166+
}
167+
110168
#[tokio::test]
111169
async fn test_parse_php_extracts_static_properties() {
112170
let backend = create_test_backend();

0 commit comments

Comments
 (0)