feat: foreach completion (#551)
parent
f46fccd0d3
commit
9eea26df71
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Foo;
|
||||||
|
|
||||||
|
class Bar {
|
||||||
|
public $foo;
|
||||||
|
|
||||||
|
/** @return Bar[] */
|
||||||
|
public function test() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
$bar = new Bar();
|
||||||
|
$bars = $bar->test();
|
||||||
|
$array1 = [new Bar(), new \stdClass()];
|
||||||
|
$array2 = ['foo' => $bar, $bar];
|
||||||
|
$array3 = ['foo' => $bar, 'baz' => $bar];
|
||||||
|
|
||||||
|
foreach ($bars as $value) {
|
||||||
|
$v
|
||||||
|
$value->
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($array1 as $key => $value) {
|
||||||
|
$
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($array2 as $key => $value) {
|
||||||
|
$
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($array3 as $key => $value) {
|
||||||
|
$
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bar->test() as $value) {
|
||||||
|
$
|
||||||
|
}
|
|
@ -486,6 +486,14 @@ class CompletionProvider
|
||||||
|
|
||||||
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
|
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
|
||||||
$vars[] = $node->leftOperand;
|
$vars[] = $node->leftOperand;
|
||||||
|
} elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
|
||||||
|
foreach ($node->getDescendantNodes() as $descendantNode) {
|
||||||
|
if ($descendantNode instanceof Node\Expression\Variable
|
||||||
|
&& ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
|
||||||
|
) {
|
||||||
|
$vars[] = $descendantNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get all descendent variables, then filter to ones that start with $namePrefix.
|
// Get all descendent variables, then filter to ones that start with $namePrefix.
|
||||||
// Avoiding closure usage in tight loop
|
// Avoiding closure usage in tight loop
|
||||||
|
|
|
@ -568,6 +568,20 @@ class DefinitionResolver
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we get to a ForeachStatement, check the keys and values
|
||||||
|
if ($n instanceof Node\Statement\ForeachStatement) {
|
||||||
|
if ($n->foreachKey && $n->foreachKey->expression->getName() === $name) {
|
||||||
|
return $n->foreachKey;
|
||||||
|
}
|
||||||
|
if ($n->foreachValue
|
||||||
|
&& $n->foreachValue->expression instanceof Node\Expression\Variable
|
||||||
|
&& $n->foreachValue->expression->getName() === $name
|
||||||
|
) {
|
||||||
|
return $n->foreachValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check each previous sibling node for a variable assignment to that variable
|
// Check each previous sibling node for a variable assignment to that variable
|
||||||
while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) {
|
while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) {
|
||||||
if ($n instanceof Node\Statement\ExpressionStatement) {
|
if ($n instanceof Node\Statement\ExpressionStatement) {
|
||||||
|
@ -619,6 +633,9 @@ class DefinitionResolver
|
||||||
if ($defNode instanceof Node\Expression\AssignmentExpression || $defNode instanceof Node\UseVariableName) {
|
if ($defNode instanceof Node\Expression\AssignmentExpression || $defNode instanceof Node\UseVariableName) {
|
||||||
return $this->resolveExpressionNodeToType($defNode);
|
return $this->resolveExpressionNodeToType($defNode);
|
||||||
}
|
}
|
||||||
|
if ($defNode instanceof Node\ForeachKey || $defNode instanceof Node\ForeachValue) {
|
||||||
|
return $this->getTypeFromNode($defNode);
|
||||||
|
}
|
||||||
if ($defNode instanceof Node\Parameter) {
|
if ($defNode instanceof Node\Parameter) {
|
||||||
return $this->getTypeFromNode($defNode);
|
return $this->getTypeFromNode($defNode);
|
||||||
}
|
}
|
||||||
|
@ -900,7 +917,7 @@ class DefinitionResolver
|
||||||
$keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer;
|
$keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$valueTypes = array_unique($keyTypes);
|
$valueTypes = array_unique($valueTypes);
|
||||||
$keyTypes = array_unique($keyTypes);
|
$keyTypes = array_unique($keyTypes);
|
||||||
if (empty($valueTypes)) {
|
if (empty($valueTypes)) {
|
||||||
$valueType = null;
|
$valueType = null;
|
||||||
|
@ -1080,6 +1097,27 @@ class DefinitionResolver
|
||||||
return new Types\Mixed_;
|
return new Types\Mixed_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FOREACH KEY/VARIABLE
|
||||||
|
if ($node instanceof Node\ForeachKey || $node->parent instanceof Node\ForeachKey) {
|
||||||
|
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
|
||||||
|
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName);
|
||||||
|
if ($collectionType instanceof Types\Array_) {
|
||||||
|
return $collectionType->getKeyType();
|
||||||
|
}
|
||||||
|
return new Types\Mixed_();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOREACH VALUE/VARIABLE
|
||||||
|
if ($node instanceof Node\ForeachValue
|
||||||
|
|| ($node instanceof Node\Expression\Variable && $node->parent instanceof Node\ForeachValue)
|
||||||
|
) {
|
||||||
|
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
|
||||||
|
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName);
|
||||||
|
if ($collectionType instanceof Types\Array_) {
|
||||||
|
return $collectionType->getValueType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS
|
// PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS
|
||||||
// Get the documented type the assignment resolves to.
|
// Get the documented type the assignment resolves to.
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -337,6 +337,7 @@ class TextDocument
|
||||||
if ($def === null) {
|
if ($def === null) {
|
||||||
return new Hover([], $range);
|
return new Hover([], $range);
|
||||||
}
|
}
|
||||||
|
$contents = [];
|
||||||
if ($def->declarationLine) {
|
if ($def->declarationLine) {
|
||||||
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
|
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
|
||||||
}
|
}
|
||||||
|
|
|
@ -554,6 +554,146 @@ class CompletionTest extends TestCase
|
||||||
], true), $items);
|
], true), $items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider foreachProvider
|
||||||
|
*/
|
||||||
|
public function testForeach(Position $position, array $expectedItems)
|
||||||
|
{
|
||||||
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/foreach.php');
|
||||||
|
$this->loader->open($completionUri, file_get_contents($completionUri));
|
||||||
|
$items = $this->textDocument->completion(
|
||||||
|
new TextDocumentIdentifier($completionUri),
|
||||||
|
$position
|
||||||
|
)->wait();
|
||||||
|
$this->assertCompletionsListSubset(new CompletionList($expectedItems, true), $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function foreachProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'foreach value' => [
|
||||||
|
new Position(18, 6),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'$value',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'\\Foo\\Bar',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(18, 6), new Position(18, 6)), 'alue')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'foreach value resolved' => [
|
||||||
|
new Position(19, 12),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'foo',
|
||||||
|
CompletionItemKind::PROPERTY,
|
||||||
|
'mixed'
|
||||||
|
),
|
||||||
|
new CompletionItem(
|
||||||
|
'test',
|
||||||
|
CompletionItemKind::METHOD,
|
||||||
|
'\\Foo\\Bar[]'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'array creation with multiple objects' => [
|
||||||
|
new Position(23, 5),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'$value',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'\\Foo\\Bar|\\stdClass',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(23, 5), new Position(23, 5)), 'value')
|
||||||
|
),
|
||||||
|
new CompletionItem(
|
||||||
|
'$key',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'int',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(23, 5), new Position(23, 5)), 'key')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'array creation with string/int keys and object values' => [
|
||||||
|
new Position(27, 5),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'$value',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'\\Foo\\Bar',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(27, 5), new Position(27, 5)), 'value')
|
||||||
|
),
|
||||||
|
new CompletionItem(
|
||||||
|
'$key',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'string|int',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(27, 5), new Position(27, 5)), 'key')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'array creation with only string keys' => [
|
||||||
|
new Position(31, 5),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'$value',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'\\Foo\\Bar',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(31, 5), new Position(31, 5)), 'value')
|
||||||
|
),
|
||||||
|
new CompletionItem(
|
||||||
|
'$key',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'string',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(31, 5), new Position(31, 5)), 'key')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'foreach function call' => [
|
||||||
|
new Position(35, 5),
|
||||||
|
[
|
||||||
|
new CompletionItem(
|
||||||
|
'$value',
|
||||||
|
CompletionItemKind::VARIABLE,
|
||||||
|
'\\Foo\\Bar',
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new TextEdit(new Range(new Position(35, 5), new Position(35, 5)), 'value')
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testMethodReturnType()
|
public function testMethodReturnType()
|
||||||
{
|
{
|
||||||
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/method_return_type.php');
|
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/method_return_type.php');
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
"containerName": "A"
|
"containerName": "A"
|
||||||
},
|
},
|
||||||
"type__tostring": "string[]",
|
"type__tostring": "bool[]",
|
||||||
"type": {},
|
"type": {},
|
||||||
"declarationLine": "protected $foo;",
|
"declarationLine": "protected $foo;",
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
},
|
},
|
||||||
"containerName": "A"
|
"containerName": "A"
|
||||||
},
|
},
|
||||||
"type__tostring": "\\__CLASS__[]",
|
"type__tostring": "bool[]",
|
||||||
"type": {},
|
"type": {},
|
||||||
"declarationLine": "private static $deprecationsTriggered;",
|
"declarationLine": "private static $deprecationsTriggered;",
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|
Loading…
Reference in New Issue