php-lsp 0.3.0

A PHP Language Server Protocol implementation
Documentation
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! Comprehensive hover coverage.

mod common;

use common::{TestServer, render_hover};
use expect_test::expect;

#[tokio::test]
async fn hover_function() {
    let mut s = TestServer::new().await;
    let v = s.check_hover(r#"<?php function gr$0eet(): void {}"#).await;
    expect![[r#"
        ```php
        function greet(): void
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_function_with_signature() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(r#"<?php function gr$0eet(string $name, int $count = 1): string {}"#)
        .await;
    expect![[r#"
        ```php
        function greet(string $name, int $count = 1): string
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_method() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Greeter {
    public function he$0llo(): string { return 'hi'; }
}"#,
        )
        .await;
    expect![[r#"
        ```php
        function hello(): string
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_static_method() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Registry {
    public static function ge$0t(string $k): mixed {}
}"#,
        )
        .await;
    expect![[r#"
        ```php
        function get(string $k): mixed
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_class_identifier() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Gre$0eter {}
"#,
        )
        .await;
    expect![[r#"
        ```php
        class Greeter
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_enum_identifier() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
enum Stat$0us { case Active; case Inactive; }
"#,
        )
        .await;
    expect![[r#"
        ```php
        enum Status
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_interface_identifier() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
interface Writ$0able { public function write(): void; }
"#,
        )
        .await;
    expect![[r#"
        ```php
        interface Writable
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_docblock_annotated_function() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
/**
 * Greets the user.
 * @param string $name the name
 * @return string
 */
function gr$0eet(string $name): string { return $name; }
"#,
        )
        .await;
    expect![[r#"
        ```php
        function greet(string $name): string
        ```

        ---

        Greets the user.

        **@return** `string`
        **@param** `string` `$name` — the name"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_method_call_resolves_receiver_class() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Mailer { public function process(string $to): bool {} }
class Queue  { public function process(int $id): void {} }
$mailer = new Mailer();
$mailer->pro$0cess('');
"#,
        )
        .await;
    expect![[r#"
        ```php
        Mailer::process(string $to): bool
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_variable_is_scoped_to_method() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Widget {}
class Invoice {}
class Service {
    public function a(): void { $result = new Widget(); }
    public function b(): void { $res$0ult = new Invoice(); }
}
"#,
        )
        .await;
    expect!["`$result` `Invoice`"].assert_eq(&v);
}

#[tokio::test]
async fn hover_missing_symbol_returns_nothing() {
    let mut s = TestServer::new().await;
    let v = s.check_hover(r#"<?php fo$0o();"#).await;
    expect!["<no hover>"].assert_eq(&v);
}

#[tokio::test]
async fn hover_class_in_extends_clause_cross_file() {
    let tmp = tempfile::tempdir().unwrap();
    std::fs::write(tmp.path().join("Base.php"), "<?php\nclass Base {}\n").unwrap();
    let child_src = "<?php\nclass Child extends Base {}\n";
    std::fs::write(tmp.path().join("Child.php"), child_src).unwrap();
    let mut s = TestServer::with_root(tmp.path()).await;
    s.wait_for_index_ready().await;
    // Only open Child.php — Base.php is indexed but never opened.
    s.open("Child.php", child_src).await;
    let (_, line, col) = s.locate("Child.php", "Base", 0);
    let resp = s.hover("Child.php", line, col).await;
    expect![[r#"
        ```php
        class Base
        ```"#]]
    .assert_eq(&render_hover(&resp));
}

#[tokio::test]
async fn hover_class_as_param_type_cross_file() {
    let tmp = tempfile::tempdir().unwrap();
    std::fs::write(
        tmp.path().join("Post.php"),
        "<?php\nclass Post { public string $title = ''; }\n",
    )
    .unwrap();
    let ctrl_src = "<?php\nfunction show(Post $post): void {}\n";
    std::fs::write(tmp.path().join("Controller.php"), ctrl_src).unwrap();
    let mut s = TestServer::with_root(tmp.path()).await;
    s.wait_for_index_ready().await;
    // Only open Controller.php — Post.php is indexed but never opened.
    s.open("Controller.php", ctrl_src).await;
    let (_, line, col) = s.locate("Controller.php", "Post", 0);
    let resp = s.hover("Controller.php", line, col).await;
    expect![[r#"
        ```php
        class Post
        ```"#]]
    .assert_eq(&render_hover(&resp));
}

#[tokio::test]
async fn hover_across_files_via_use() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"//- /src/Greeter.php
<?php
namespace App;
class Greeter {
    public function hello(): string { return 'hi'; }
}

//- /src/main.php
<?php
use App\Greeter;
$g = new Greeter();
$g->hel$0lo();
"#,
        )
        .await;
    expect![[r#"
        ```php
        Greeter::hello(): string
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_property_access() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class User {
    public string $name = '';
}
$u = new User();
echo $u->na$0me;
"#,
        )
        .await;
    expect![[r#"
        ```php
        (property) User::$name: string
        ```"#]]
    .assert_eq(&v);
}

/// Hovering on an enum *case* (not the enum name) should return the qualified
/// case label. If the server only indexes enum names but not individual cases
/// this will produce `<no hover>` — that is the bug to fix.
#[tokio::test]
async fn hover_enum_case_declaration() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
enum Status { case Acti$0ve; case Inactive; }
"#,
        )
        .await;
    expect![[r#"
        ```php
        case Status::Active
        ```"#]]
    .assert_eq(&v);
}

/// Hovering on a class constant must show the constant with its inferred or
/// declared type. An unimplemented constant-hover returns `<no hover>`.
#[tokio::test]
async fn hover_class_constant() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
class Config {
    const VERSI$0ON = 42;
}
"#,
        )
        .await;
    expect![[r#"
        ```php
        const int VERSION = 42
        ```"#]]
    .assert_eq(&v);
}

/// A function with a nullable param type `?T` must render the `?` in hover so
/// callers can see the type is optional. Cursor is on the function name.
#[tokio::test]
async fn hover_nullable_param_type() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
function sho$0w(?string $label): void {}
"#,
        )
        .await;
    expect![[r#"
        ```php
        function show(?string $label): void
        ```"#]]
    .assert_eq(&v);
}

/// Hovering on a trait identifier must render as `trait Name`, not `class`.
#[tokio::test]
async fn hover_trait_identifier() {
    let mut s = TestServer::new().await;
    let v = s
        .check_hover(
            r#"<?php
trait Logg$0able { public function log(): void {} }
"#,
        )
        .await;
    expect![[r#"
        ```php
        trait Loggable
        ```"#]]
    .assert_eq(&v);
}

#[tokio::test]
async fn hover_trait_inherited_method() {
    let mut s = TestServer::new().await;
    let out = s
        .check_hover(
            r#"<?php
trait Greeting {
    public function sayHello(string $name): string {
        return "Hello, {$name}";
    }
}
class Greeter {
    use Greeting;
    public function run(): string {
        return $this->$0sayHello('world');
    }
}
"#,
        )
        .await;
    assert!(
        out.contains("sayHello"),
        "hover on trait-inherited method must return its signature, got: {out}"
    );
}

#[tokio::test]
async fn hover_multi_trait_alpha() {
    let mut s = TestServer::new().await;
    let out = s
        .check_hover(
            r#"<?php
trait A { public function alpha(): int { return 1; } }
trait B { public function beta(): int { return 2; } }
class Both {
    use A;
    use B;
    public function run(): int { return $this->$0alpha() + $this->beta(); }
}
"#,
        )
        .await;
    assert!(
        out.contains("alpha"),
        "hover on alpha() via multi-trait must mention it, got: {out}"
    );
}

#[tokio::test]
async fn hover_multi_trait_beta() {
    let mut s = TestServer::new().await;
    let out = s
        .check_hover(
            r#"<?php
trait A { public function alpha(): int { return 1; } }
trait B { public function beta(): int { return 2; } }
class Both {
    use A;
    use B;
    public function run(): int { return $this->alpha() + $this->$0beta(); }
}
"#,
        )
        .await;
    assert!(
        out.contains("beta"),
        "hover on beta() via multi-trait must mention it, got: {out}"
    );
}

#[tokio::test]
async fn hover_on_empty_file_returns_null_not_error() {
    let mut s = TestServer::new().await;
    s.open("empty.php", "").await;
    let resp = s.hover("empty.php", 0, 0).await;
    assert!(
        resp["error"].is_null(),
        "hover errored on empty file: {resp:?}"
    );
    assert!(
        resp["result"].is_null(),
        "hover on empty file should be null, got: {:?}",
        resp["result"]
    );
}

#[tokio::test]
async fn hover_past_eof_does_not_crash() {
    let mut s = TestServer::new().await;
    s.open("short.php", "<?php\nfunction f(): void {}\n").await;
    let resp = s.hover("short.php", 500, 500).await;
    assert!(resp["error"].is_null(), "hover past EOF errored: {resp:?}");
}