Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Features:
- String <=> Heredoc code action #2825 @mamazu
- Support new expression without parenthesis #2811
- Support vscode evaluatable expressions #2905 @zobo
- Support `match` and `throw` expression keywords completion #2819 @przepompownia
- Runtime support for PHP 8.4 #2829
- Initial support for property hooks @dantleech #2833

Expand Down
19 changes: 18 additions & 1 deletion lib/Completion/Bridge/TolerantParser/CompletionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
use Microsoft\PhpParser\Node\Expression\AnonymousFunctionCreationExpression;
use Microsoft\PhpParser\Node\Expression\ArgumentExpression;
use Microsoft\PhpParser\Node\Expression\BinaryExpression;
use Microsoft\PhpParser\Node\Expression\CallExpression;
use Microsoft\PhpParser\Node\Expression\MemberAccessExpression;
use Microsoft\PhpParser\Node\Expression\ScopedPropertyAccessExpression;
use Microsoft\PhpParser\Node\Expression\Variable;
use Microsoft\PhpParser\Node\InterfaceBaseClause;
use Microsoft\PhpParser\Node\MatchArm;
Expand All @@ -36,6 +39,7 @@
use Microsoft\PhpParser\Node\Statement\WhileStatement;
use Microsoft\PhpParser\Node\Statement\InterfaceDeclaration;
use Microsoft\PhpParser\Node\Statement\TraitDeclaration;
use Microsoft\PhpParser\Node\StringLiteral;
use Microsoft\PhpParser\Node\TraitUseClause;
use Microsoft\PhpParser\TokenKind;
use Phpactor\TextDocument\ByteOffset;
Expand All @@ -62,7 +66,20 @@ public static function expression(?Node $node): bool
return false;
}

if ($parent instanceof ArgumentExpression) {
if (
$node instanceof Variable
|| $node instanceof ExpressionStatement
|| $node instanceof MemberAccessExpression
|| $node instanceof ScopedPropertyAccessExpression
|| $node instanceof StringLiteral
) {
return false;
}

if (
$node instanceof CallExpression
|| $parent instanceof ArgumentExpression
) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Generator;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\StatementNode;
use Phpactor\Completion\Bridge\TolerantParser\CompletionContext;
use Phpactor\Completion\Bridge\TolerantParser\TolerantCompletor;
use Phpactor\Completion\Core\Suggestion;
Expand All @@ -15,6 +16,10 @@

class KeywordCompletor implements TolerantCompletor
{
private const EXPRESSIONS = [
'match' => " (\$1) {\$0\n}",
'throw' => ' $1',
];
private const MAGIC_METHODS = [
'__construct' => "(\$1)\n{\$0\n}",
'__call' => "(string \\\$\${1:name}, array \\\$\${2:arguments}): \${3:mixed}\n{\$0\n}",
Expand All @@ -38,7 +43,7 @@ class KeywordCompletor implements TolerantCompletor
public function complete(Node $node, TextDocument $source, ByteOffset $offset): Generator
{
if (CompletionContext::promotedPropertyVisibility($node)) {
yield from $this->keywords(['private ', 'public ', 'protected ', ]);
yield from $this->keywords(['private ', 'public ', 'protected ']);
return true;
}
if (CompletionContext::classClause($node, $offset)) {
Expand All @@ -64,7 +69,16 @@ public function complete(Node $node, TextDocument $source, ByteOffset $offset):
return true;
}

if (!$node instanceof MethodDeclaration && CompletionContext::classMembersBody($node->parent)) {
if (CompletionContext::expression($node)) {
yield from $this->expressions();
return true;
}

if (
!$node instanceof MethodDeclaration
&& CompletionContext::classMembersBody($node->parent)
&& !$node->parent instanceof StatementNode
) {
yield from $this->keywords([
'function ',
'const ',
Expand All @@ -80,6 +94,20 @@ public function complete(Node $node, TextDocument $source, ByteOffset $offset):
return true;
}

/**
* @return Generator<Suggestion>
*/
private function expressions(): Generator
{
foreach (self::EXPRESSIONS as $name => $snippet) {
yield Suggestion::createWithOptions($name . ' ', [
'type' => Suggestion::TYPE_KEYWORD,
'priority' => -255,
'snippet' => $name . $snippet,
]);
}
}

/**
* @return Generator<Suggestion>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,29 @@ public function testComplete(string $source, array $expected): void
/**
* @return Generator<string,array{string,array<string,mixed>[]}>
*/
public function provideComplete(): Generator
public static function provideComplete(): Generator
{
yield 'member keywords' => [
'<?php class Foobar { p<>',
$this->expect(['private ', 'protected ', 'public ']),
self::expect(['private ', 'protected ', 'public ']),
];

yield 'member keyword postfix' => [
'<?php class Foobar { private <>',
$this->expect(['const ', 'function ']),
self::expect(['const ', 'function ']),
];
yield 'member keyword postfix 2' => [
'<?php class Foobar { private func<>',
$this->expect(['const ', 'function ']),
self::expect(['const ', 'function ']),
];

yield '__construct' => [
'<?php class Foobar { public function __c<>',
[...$this->expectMagicMethods()],
[...self::expectMagicMethods()],
];
yield '__construct 2' => [
'<?php class Foo extends Bar implements One { public function __c<> }',
[...$this->expectMagicMethods()],
[...self::expectMagicMethods()],
];

yield 'no magic methods here' => [
Expand All @@ -55,49 +55,77 @@ public function provideComplete(): Generator

yield 'class implements 1' => [
'<?php class Foobar <>',
$this->expect(['extends ', 'implements ']),
self::expect(['extends ', 'implements ']),
];
yield 'class implements 2' => [
'<?php class Foobar impl<>',
$this->expect(['extends ', 'implements ']),
self::expect(['extends ', 'implements ']),
];

yield 'class keyword' => [
'<?php cl<>',
$this->expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
self::expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
];
yield 'class keyword 2' => [
'<?php class F {} cl<>',
$this->expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
self::expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
];
yield 'class keyword 3' => [
'<?php class F {function fo() {}} cl<>',
$this->expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
self::expect(['class ', 'enum ', 'function ', 'interface ', 'trait ']),
];
yield 'match keyword' => [
'<?php class F { public function foo() { $x = mat<> }}',
[...self::expectExpressions()],
];
yield 'match unexpected' => [
'<?php class F { public function foo() { $this->mat<> }}',
[],
];
yield 'match unexpected 2' => [
'<?php class F { public function foo() { $this->foo(<>) }}',
[...self::expectExpressions()],
];
yield 'match unexpected 3' => [
'<?php class F { public function foo() { $this->foo(self::<>) }}',
[],
];
yield 'match unexpected 4' => [
'<?php if (1)<> {}',
[],
];
yield 'match unexpected in string' => [
'<?php strlen(\'<>',
[],
];
yield 'match unexpected 5' => [
'<?php $<>',
[],
];

yield 'if condition classes' => [
'<?php class Stuff { public function testing() { if ($this i<>} }',
$this->expect(['instanceof ']),
self::expect(['instanceof ']),
];
yield 'if condition' => [
'<?php if ($test i<>',
$this->expect(['instanceof ']),
self::expect(['instanceof ']),
];
yield 'while with empty expression' => [
'<?php while (<>',
$this->expect([]),
self::expect([]),
];
yield 'while condition' => [
'<?php while ($test i<>',
$this->expect(['instanceof ']),
self::expect(['instanceof ']),
];
yield 'while condition (without variable)' => [
'<?php while (<>',
[],
];
yield 'while condition (with expression)' => [
'<?php while ($node->getParent() i<>',
$this->expect(['instanceof ']),
self::expect(['instanceof ']),
];
}

Expand All @@ -110,7 +138,7 @@ protected function createTolerantCompletor(TextDocument $source): TolerantComple
* @return array<array<string,mixed>>
* @param array<string> $array
*/
private function expect(array $array): array
private static function expect(array $array): array
{
return array_map(fn (string $keyword) => [
'name' => $keyword,
Expand All @@ -120,7 +148,22 @@ private function expect(array $array): array
/**
* @return Generator<array{name:string,snippet:string}>
*/
private function expectMagicMethods(): Generator
private static function expectExpressions(): Generator
{
$expressions = [
'match' => " (\$1) {\$0\n}",
'throw' => ' $1',
];

foreach ($expressions as $name => $snippet) {
yield ['name' => $name . ' ', 'snippet' => $name . $snippet];
}
}

/**
* @return Generator<array{name:string,snippet:string}>
*/
private static function expectMagicMethods(): Generator
{
$methods = [
'__construct' => "(\$1)\n{\$0\n}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Phpactor\Completion\Tests\Unit\Bridge\TolerantParser;

use Phpactor\TextDocument\TextDocumentBuilder;
use Phpactor\WorseReflection\Bridge\TolerantParser\AstProvider\TolerantAstProvider;
use PHPUnit\Framework\Attributes\DataProvider;
use Generator;
Expand All @@ -16,7 +17,7 @@ class CompletionContextTest extends TestCase
public function testExpression(string $source, bool $expected): void
{
[$source, $offset] = ExtractOffset::fromSource($source);
$node = (new TolerantAstProvider())->parseString($source)->getDescendantNodeAtPosition((int)$offset);
$node = (new TolerantAstProvider())->parseString($source)->getDescendantNodeAtPosition($offset);
self::assertEquals($expected, CompletionContext::expression($node));
}

Expand Down Expand Up @@ -69,15 +70,35 @@ public static function provideExpression(): Generator
"<?php class Foo { public function bar() {\necho 'hello world'; \$bar = 12;} A<> }",
false,
];
yield 'not between if condition and body' => [
'<?php if(1)<> {}',
false,
];
yield 'not in variable' => [
'<?php $<>',
false,
];
yield 'not in string literal' => [
'<?php strlen(\'<>',
false,
];
yield 'not in scoped property access expr' => [
'<?php class Foo { public function foo() { $this->foo(self::<>) }',
false,
];

yield 'in class method body 1' => [
'<?php class Foo { public function foo() { A<> }',
true
true,
];
yield 'in class method body 2' => [
'<?php class Foo { public function bar() { if (true) { return false; } A<> } }',
true,
];
yield 'in function call' => [
'<?php class Foo { public function foo() { $this->foo(<>) }',
true,
];
yield 'in foreach' => [
'<?php class Foo { public function bar() { if (true) { return false; } foreach(<> } }',
true,
Expand All @@ -88,7 +109,7 @@ public static function provideExpression(): Generator
public function testClassMemberBody(string $source, bool $expected): void
{
[$source, $offset] = ExtractOffset::fromSource($source);
$node = (new TolerantAstProvider())->parseString($source)->getDescendantNodeAtPosition((int)$offset);
$node = (new TolerantAstProvider())->get(TextDocumentBuilder::fromString($source))->getDescendantNodeAtPosition((int)$offset);
self::assertEquals($expected, CompletionContext::classMembersBody($node));
}

Expand All @@ -99,20 +120,24 @@ public static function provideClassMemberBody(): Generator
{
yield 'property' => [
'<?php class Foo { pri<> }',
true
true,
];
yield 'visibility 1' => [
'<?php class Foo { <> }',
true
true,
];
yield 'visibility 2' => [
'<?php class Foo { private <> }',
true
true,
];
yield 'visibility 3' => [
'<?php class Foo { private Foob<> }',
true,
];
yield 'method body' => [
'<?php class Foo { private function foo() { <> } }',
true,
];

// todo...
yield 'visibility 4' => [
Expand Down