Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions tests/EndToEnd/MessageErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
use Firehed\PhpLsp\Tests\EndToEnd\Client\ServerProcess;
use Firehed\PhpLsp\Tests\EndToEnd\Client\StartsServerTrait;
use Firehed\PhpLsp\Tests\EndToEnd\Session\ClientCapabilities;
use Firehed\PhpLsp\Tests\EndToEnd\Session\Feature;
use Firehed\PhpLsp\Tests\EndToEnd\Session\Session;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use stdClass;

/**
* Messages the server cannot act on are answered with an error, and the
* session carries on.
* Messages the server cannot act on are answered, and the session carries on.
*/
#[CoversNothing]
final class MessageErrorTest extends TestCase
Expand All @@ -37,6 +39,40 @@ protected function setUp(): void
$this->session->start(new ClientCapabilities());
}

/**
* Today the answer is an empty result. JSON-RPC 2.0 搂5.1 also defines
* InvalidParams (-32602) for this.
*/
#[DataProvider('malformedPositionParams')]
public function testMalformedPositionParamsAreAnsweredWithNoResult(string $params): void
{
$decoded = json_decode($params, flags: JSON_THROW_ON_ERROR);
assert($decoded instanceof stdClass);

$response = $this->client->request(Feature::Definition->value, $decoded);
$this->session->end();

self::assertNull($response->error, 'malformed parameters are not answered with an error');
self::assertNull($response->result, 'malformed parameters are answered with no result');
self::assertSame(0, $this->server->waitForExit(), 'the session carries on');
}

/**
* Parameters are checked before the document is read, so the URI need not
* name a real file.
*
* @return iterable<string, array{string}>
*/
public static function malformedPositionParams(): iterable
{
yield 'textDocument is not an object' => ['{"textDocument":"x","position":{"line":0,"character":0}}'];
yield 'uri is not a string' => ['{"textDocument":{"uri":123},"position":{"line":0,"character":0}}'];
yield 'position is not an object' => ['{"textDocument":{"uri":"file:///x.php"},"position":"x"}'];
yield 'line is not an integer' => [
'{"textDocument":{"uri":"file:///x.php"},"position":{"line":"x","character":0}}',
];
}

public function testInvalidRequestIsAnsweredAtItsId(): void
{
// JSON-RPC 2.0 搂4: `method` is a string.
Expand Down
19 changes: 19 additions & 0 deletions tests/EndToEnd/scripts/definition-class-from-new.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'SignatureHelp.php';

return new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition(
$file,
new Marker\SymbolMarker('class_instantiation'),
expect: new Expectation\LandsOn('src/Domain/User.php', line: 15),
),
],
);
28 changes: 28 additions & 0 deletions tests/EndToEnd/scripts/definition-constant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'src/Resolution/NamespacedConstant.php';

$definition = fn (
string $marker,
Expectation\DefinitionExpectationInterface $expect,
): Session\Script => new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition($file, new Marker\SymbolMarker($marker), expect: $expect),
],
);

return [
// An unqualified constant in a namespace is looked up in that namespace first,
'namespaced' => $definition('namespaced_const_fetch', new Expectation\LandsOn($file, line: 13)),
// and falls back to the global one when the namespace does not define it.
'global fallback' => $definition(
'global_const_fetch',
new Expectation\LandsOn('AutoloadFiles/helpers.php', line: 19),
),
];
28 changes: 28 additions & 0 deletions tests/EndToEnd/scripts/definition-dynamic-access.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'EdgeCases/DynamicAccess.php';

$definition = fn (
string $marker,
Expectation\DefinitionExpectationInterface $expect,
): Session\Script => new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition($file, new Marker\SymbolMarker($marker), expect: $expect),
],
);

return [
'class name' => $definition('dynamic_class_name', new Expectation\NoAnswer()),
'static property on a variable class' => $definition('dynamic_class_static_prop', new Expectation\NoAnswer()),
'constant on a variable class' => $definition('dynamic_class_const', new Expectation\NoAnswer()),
// In `$this->$method()` and `self::$method()` the cursor is on the
// variable, so its assignment is the definition.
'instance method name' => $definition('dynamic_instance_method', new Expectation\LandsOn($file, line: 16)),
'static method name' => $definition('dynamic_static_method', new Expectation\LandsOn($file, line: 10)),
];
19 changes: 19 additions & 0 deletions tests/EndToEnd/scripts/definition-enum-method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'src/Enum/Status.php';

return new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition(
$file,
new Marker\SymbolMarker('enum_method'),
expect: new Expectation\LandsOn($file, line: 17),
),
],
);
37 changes: 37 additions & 0 deletions tests/EndToEnd/scripts/definition-inheritance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$child = 'src/Inheritance/ChildClass.php';
$parent = 'src/Inheritance/ParentClass.php';

$definition = fn (
string $file,
string $marker,
Expectation\DefinitionExpectationInterface $expect,
): Session\Script => new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition($file, new Marker\SymbolMarker($marker), expect: $expect),
],
);

return [
'inherited method' => $definition($child, 'inherited_method', new Expectation\LandsOn($parent, line: 29)),
'overridden method' => $definition($child, 'overridden_method', new Expectation\LandsOn($child, line: 22)),
'parent method' => $definition($child, 'parent_method', new Expectation\LandsOn($parent, line: 29)),
'self method' => $definition($child, 'self_method', new Expectation\LandsOn($parent, line: 39)),
'static keyword method' => $definition($child, 'static_method', new Expectation\LandsOn($parent, line: 39)),
'private method' => $definition($parent, 'private_method_internal', new Expectation\LandsOn($parent, line: 59)),
'protected method' => $definition($parent, 'protected_method_internal', new Expectation\LandsOn($parent, line: 54)),
'class constant' => $definition($parent, 'class_constant', new Expectation\LandsOn($parent, line: 9)),
// A method from a used trait wins over the parent's method of the same name.
'trait precedence' => $definition(
'src/Definition/TraitPrecedenceChild.php',
'trait_precedence',
new Expectation\LandsOn('src/Definition/TraitPrecedenceTrait.php', line: 9),
),
];
19 changes: 19 additions & 0 deletions tests/EndToEnd/scripts/definition-method-in-global-scope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$file = 'TopLevel/global_scope_hover.php';

return new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition(
$file,
new Marker\SymbolMarker('global_method_call'),
expect: new Expectation\LandsOn('src/Domain/User.php', line: 40),
),
],
);
30 changes: 30 additions & 0 deletions tests/EndToEnd/scripts/definition-no-target.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$noAnswer = fn (string $file, Marker\MarkerInterface $at): Session\Script => new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($file),
new Step\Definition($file, $at, expect: new Expectation\NoAnswer()),
],
);

$parentless = 'EdgeCases/ParentWithoutExtends.php';
$outside = 'EdgeCases/SelfOutsideClass.php';
$unknown = 'EdgeCases/UnknownTypeMethod.php';
$builtin = 'EdgeCases/BuiltinDefinitions.php';

return [
'parent method without extends' => $noAnswer($parentless, new Marker\SymbolMarker('parent_method')),
'new parent without extends' => $noAnswer($parentless, new Marker\CursorMarker('def_new_parent')),
'self method outside a class' => $noAnswer($outside, new Marker\SymbolMarker('self_method')),
'new self outside a class' => $noAnswer($outside, new Marker\CursorMarker('def_new_self')),
'method on an untyped parameter' => $noAnswer($unknown, new Marker\SymbolMarker('untyped_param')),
'unknown method' => $noAnswer($unknown, new Marker\SymbolMarker('unknown_method')),
// Built-ins come from reflection and have no file to go to.
'built-in class' => $noAnswer($builtin, new Marker\SymbolMarker('builtin_class')),
'built-in method' => $noAnswer($builtin, new Marker\SymbolMarker('builtin_method')),
];
35 changes: 35 additions & 0 deletions tests/EndToEnd/scripts/definition-user.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Firehed\PhpLsp\Tests\EndToEnd;

$user = 'src/Domain/User.php';
$timestamps = 'src/Traits/HasTimestamps.php';

$definition = fn (
string $marker,
Expectation\DefinitionExpectationInterface $expect,
): Session\Script => new Session\Script(
project: 'tests/Fixtures',
steps: [
new Step\Open($user),
new Step\Definition($user, new Marker\SymbolMarker($marker), expect: $expect),
],
);

return [
'static method' => $definition('create', new Expectation\LandsOn($user, line: 124)),
'instance method' => $definition('setName', new Expectation\LandsOn($user, line: 48)),
'method via assignment' => $definition('method_via_assignment', new Expectation\LandsOn($user, line: 48)),
'nullsafe method' => $definition('setName_nullsafe', new Expectation\LandsOn($user, line: 48)),
'nullsafe method via assignment' => $definition(
'nullsafe_via_assignment',
new Expectation\LandsOn($user, line: 48),
),
'property' => $definition('manager', new Expectation\LandsOn($user, line: 29)),
'promoted property' => $definition('promoted_property', new Expectation\LandsOn($user, line: 24)),
'trait method' => $definition('markCreated', new Expectation\LandsOn($timestamps, line: 39)),
'trait property' => $definition('trait_property_consumer', new Expectation\LandsOn($timestamps, line: 17)),
'unknown class' => $definition('unknown_class', new Expectation\NoAnswer()),
];
6 changes: 6 additions & 0 deletions tests/Fixtures/EdgeCases/BuiltinDefinitions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

// Built-in classes come from reflection and have no file to go to.

new DateTime(); //hover:builtin_class
DateTime::createFromFormat('Ymd', '20240101'); //hover:builtin_method
Loading
Loading