Skip to content

Commit eba3e98

Browse files
committed
Add properties and method signatures
1 parent 5cd2cc2 commit eba3e98

3 files changed

Lines changed: 1304 additions & 220 deletions

File tree

example.php

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,13 @@
11
<?php
22

3-
/**
4-
* PHPantomLSP Demo File
5-
*
6-
* This file demonstrates the hover functionality of PHPantomLSP.
7-
* Try hovering over the word "PHPantom" below to see the special message!
8-
*/
9-
103
namespace Demo;
114

125
class PHPantomDemo
136
{
14-
/**
15-
* Welcome to PHPantom - the phantom PHP Language Server!
16-
*
17-
* Hover over "PHPantom" anywhere in this file to see the LSP in action.
18-
*/
19-
public function demonstrateHover(): void
20-
{
21-
// PHPantom provides basic LSP functionality
22-
$message = "PHPantom is a minimal LSP server written in Rust";
23-
24-
echo $message . PHP_EOL;
25-
}
7+
public int $count = 0;
268

27-
/**
28-
* Regular PHP code - hovering over these words won't trigger special behavior
29-
*/
30-
public function regularCode(): string
9+
public function regularCode(string $text): string
3110
{
32-
$variable = "This is regular code";
33-
$function = "Some function";
34-
35-
// Only "PHPantom" triggers the hover message
36-
return "PHPantom rocks!";
3711
}
3812
}
3913

40-
// Create an instance and run the demo
41-
$demo = new PHPantomDemo();
42-
$demo->demonstrateHover();
43-
44-
// More PHPantom references for testing
45-
// Try hovering over PHPantom in comments too!
46-
$phantom = "PHPantom";
47-
48-
?>

src/lib.rs

Lines changed: 228 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,56 @@ use bumpalo::Bump;
88
use mago_syntax::ast::*;
99
use mago_syntax::parser::parse_file_content;
1010

11+
/// Stores extracted parameter information from a parsed PHP method.
12+
#[derive(Debug, Clone)]
13+
pub struct ParameterInfo {
14+
/// The parameter name including the `$` prefix (e.g. "$text").
15+
pub name: String,
16+
/// Whether this parameter is required (no default value and not variadic).
17+
pub is_required: bool,
18+
/// Optional type hint string (e.g. "string", "int", "?Foo").
19+
pub type_hint: Option<String>,
20+
/// Whether this parameter is variadic (has `...`).
21+
pub is_variadic: bool,
22+
/// Whether this parameter is passed by reference (has `&`).
23+
pub is_reference: bool,
24+
}
25+
26+
/// Stores extracted method information from a parsed PHP class.
27+
#[derive(Debug, Clone)]
28+
pub struct MethodInfo {
29+
/// The method name (e.g. "updateText").
30+
pub name: String,
31+
/// The parameters of the method.
32+
pub parameters: Vec<ParameterInfo>,
33+
/// Optional return type hint string (e.g. "void", "string", "?int").
34+
pub return_type: Option<String>,
35+
/// Whether the method is static.
36+
pub is_static: bool,
37+
}
38+
39+
/// Stores extracted property information from a parsed PHP class.
40+
#[derive(Debug, Clone)]
41+
pub struct PropertyInfo {
42+
/// The property name WITHOUT the `$` prefix (e.g. "name", "age").
43+
/// This matches PHP access syntax: `$this->name` not `$this->$name`.
44+
pub name: String,
45+
/// Optional type hint string (e.g. "string", "int").
46+
pub type_hint: Option<String>,
47+
/// Whether the property is static.
48+
pub is_static: bool,
49+
}
50+
1151
/// Stores extracted class information from a parsed PHP file.
1252
/// All data is owned so we don't depend on the parser's arena lifetime.
1353
#[derive(Debug, Clone)]
1454
pub struct ClassInfo {
1555
/// The name of the class (e.g. "User").
1656
pub name: String,
17-
/// The names of methods defined directly in this class.
18-
pub methods: Vec<String>,
57+
/// The methods defined directly in this class.
58+
pub methods: Vec<MethodInfo>,
59+
/// The properties defined directly in this class.
60+
pub properties: Vec<PropertyInfo>,
1961
/// Byte offset where the class body starts (left brace).
2062
pub start_offset: u32,
2163
/// Byte offset where the class body ends (right brace).
@@ -88,6 +130,99 @@ impl Backend {
88130
}
89131
}
90132

133+
/// Extract a string representation of a type hint from the AST.
134+
fn extract_hint_string(hint: &Hint) -> String {
135+
match hint {
136+
Hint::Identifier(ident) => ident.value().to_string(),
137+
Hint::Nullable(nullable) => {
138+
format!("?{}", Self::extract_hint_string(nullable.hint))
139+
}
140+
Hint::Union(union) => {
141+
let left = Self::extract_hint_string(union.left);
142+
let right = Self::extract_hint_string(union.right);
143+
format!("{}|{}", left, right)
144+
}
145+
Hint::Intersection(intersection) => {
146+
let left = Self::extract_hint_string(intersection.left);
147+
let right = Self::extract_hint_string(intersection.right);
148+
format!("{}&{}", left, right)
149+
}
150+
Hint::Void(ident)
151+
| Hint::Never(ident)
152+
| Hint::Float(ident)
153+
| Hint::Bool(ident)
154+
| Hint::Integer(ident)
155+
| Hint::String(ident)
156+
| Hint::Object(ident)
157+
| Hint::Mixed(ident)
158+
| Hint::Iterable(ident) => ident.value.to_string(),
159+
Hint::Null(keyword)
160+
| Hint::True(keyword)
161+
| Hint::False(keyword)
162+
| Hint::Array(keyword)
163+
| Hint::Callable(keyword)
164+
| Hint::Static(keyword)
165+
| Hint::Self_(keyword)
166+
| Hint::Parent(keyword) => keyword.value.to_string(),
167+
Hint::Parenthesized(paren) => {
168+
format!("({})", Self::extract_hint_string(paren.hint))
169+
}
170+
}
171+
}
172+
173+
/// Extract parameter information from a method's parameter list.
174+
fn extract_parameters(parameter_list: &FunctionLikeParameterList) -> Vec<ParameterInfo> {
175+
parameter_list
176+
.parameters
177+
.iter()
178+
.map(|param| {
179+
let name = param.variable.name.to_string();
180+
let is_variadic = param.ellipsis.is_some();
181+
let is_reference = param.ampersand.is_some();
182+
let has_default = param.default_value.is_some();
183+
let is_required = !has_default && !is_variadic;
184+
185+
let type_hint = param.hint.as_ref().map(|h| Self::extract_hint_string(h));
186+
187+
ParameterInfo {
188+
name,
189+
is_required,
190+
type_hint,
191+
is_variadic,
192+
is_reference,
193+
}
194+
})
195+
.collect()
196+
}
197+
198+
/// Extract property information from a class member Property node.
199+
fn extract_property_info(property: &Property) -> Vec<PropertyInfo> {
200+
let is_static = property.modifiers().iter().any(|m| m.is_static());
201+
202+
let type_hint = property.hint().map(|h| Self::extract_hint_string(h));
203+
204+
property
205+
.variables()
206+
.iter()
207+
.map(|var| {
208+
let raw_name = var.name.to_string();
209+
// Strip the leading `$` for property names since PHP access
210+
// syntax is `$this->name` not `$this->$name`.
211+
let name = if let Some(stripped) = raw_name.strip_prefix('$') {
212+
stripped.to_string()
213+
} else {
214+
raw_name
215+
};
216+
217+
PropertyInfo {
218+
name,
219+
type_hint: type_hint.clone(),
220+
is_static,
221+
}
222+
})
223+
.collect()
224+
}
225+
91226
/// Parse PHP source text and extract class information.
92227
/// Returns a Vec of ClassInfo for all classes found in the file.
93228
pub fn parse_php(&self, content: &str) -> Vec<ClassInfo> {
@@ -113,9 +248,31 @@ impl Backend {
113248
let class_name = class.name.value.to_string();
114249

115250
let mut methods = Vec::new();
251+
let mut properties = Vec::new();
252+
116253
for member in class.members.iter() {
117-
if let ClassLikeMember::Method(method) = member {
118-
methods.push(method.name.value.to_string());
254+
match member {
255+
ClassLikeMember::Method(method) => {
256+
let name = method.name.value.to_string();
257+
let parameters = Self::extract_parameters(&method.parameter_list);
258+
let return_type = method
259+
.return_type_hint
260+
.as_ref()
261+
.map(|rth| Self::extract_hint_string(&rth.hint));
262+
let is_static = method.modifiers.iter().any(|m| m.is_static());
263+
264+
methods.push(MethodInfo {
265+
name,
266+
parameters,
267+
return_type,
268+
is_static,
269+
});
270+
}
271+
ClassLikeMember::Property(property) => {
272+
let mut prop_infos = Self::extract_property_info(property);
273+
properties.append(&mut prop_infos);
274+
}
275+
_ => {}
119276
}
120277
}
121278

@@ -125,6 +282,7 @@ impl Backend {
125282
classes.push(ClassInfo {
126283
name: class_name,
127284
methods,
285+
properties,
128286
start_offset,
129287
end_offset,
130288
});
@@ -175,6 +333,43 @@ impl Backend {
175333
.find(|c| offset >= c.start_offset && offset <= c.end_offset)
176334
}
177335

336+
/// Build the label showing the full method signature.
337+
///
338+
/// Example: `regularCode(string $text, $frogs = false): string`
339+
fn build_method_label(method: &MethodInfo) -> String {
340+
let params: Vec<String> = method
341+
.parameters
342+
.iter()
343+
.map(|p| {
344+
let mut parts = Vec::new();
345+
if let Some(ref th) = p.type_hint {
346+
parts.push(th.clone());
347+
}
348+
if p.is_reference {
349+
parts.push(format!("&{}", p.name));
350+
} else if p.is_variadic {
351+
parts.push(format!("...{}", p.name));
352+
} else {
353+
parts.push(p.name.clone());
354+
}
355+
let param_str = parts.join(" ");
356+
if !p.is_required && !p.is_variadic {
357+
format!("{} = ...", param_str)
358+
} else {
359+
param_str
360+
}
361+
})
362+
.collect();
363+
364+
let ret = method
365+
.return_type
366+
.as_ref()
367+
.map(|r| format!(": {}", r))
368+
.unwrap_or_default();
369+
370+
format!("{}({}){}", method.name, params.join(", "), ret)
371+
}
372+
178373
/// Public helper for tests: get the ast_map for a given URI.
179374
pub fn get_classes_for_uri(&self, uri: &str) -> Option<Vec<ClassInfo>> {
180375
if let Ok(map) = self.ast_map.lock() {
@@ -329,17 +524,38 @@ impl LanguageServer for Backend {
329524
&& let Some(offset) = Self::position_to_offset(&content, position)
330525
&& let Some(class_info) = Self::find_class_at_offset(&classes, offset)
331526
{
332-
let items: Vec<CompletionItem> = class_info
333-
.methods
334-
.iter()
335-
.map(|method_name| CompletionItem {
336-
label: method_name.clone(),
527+
let mut items: Vec<CompletionItem> = Vec::new();
528+
529+
// Add method completions
530+
for method in &class_info.methods {
531+
let label = Self::build_method_label(method);
532+
533+
items.push(CompletionItem {
534+
label,
337535
kind: Some(CompletionItemKind::METHOD),
338536
detail: Some(format!("Class: {}", class_info.name)),
339-
insert_text: Some(method_name.clone()),
537+
insert_text: Some(method.name.clone()),
538+
filter_text: Some(method.name.clone()),
340539
..CompletionItem::default()
341-
})
342-
.collect();
540+
});
541+
}
542+
543+
// Add property completions
544+
for property in &class_info.properties {
545+
let detail = if let Some(ref th) = property.type_hint {
546+
format!("Class: {} — {}", class_info.name, th)
547+
} else {
548+
format!("Class: {}", class_info.name)
549+
};
550+
551+
items.push(CompletionItem {
552+
label: property.name.clone(),
553+
kind: Some(CompletionItemKind::PROPERTY),
554+
detail: Some(detail),
555+
insert_text: Some(property.name.clone()),
556+
..CompletionItem::default()
557+
});
558+
}
343559

344560
if !items.is_empty() {
345561
return Ok(Some(CompletionResponse::Array(items)));

0 commit comments

Comments
 (0)