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)) {
|
||||
$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 {
|
||||
// Get all descendent variables, then filter to ones that start with $namePrefix.
|
||||
// Avoiding closure usage in tight loop
|
||||
|
|
|
@ -568,6 +568,20 @@ class DefinitionResolver
|
|||
}
|
||||
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
|
||||
while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) {
|
||||
if ($n instanceof Node\Statement\ExpressionStatement) {
|
||||
|
@ -619,6 +633,9 @@ class DefinitionResolver
|
|||
if ($defNode instanceof Node\Expression\AssignmentExpression || $defNode instanceof Node\UseVariableName) {
|
||||
return $this->resolveExpressionNodeToType($defNode);
|
||||
}
|
||||
if ($defNode instanceof Node\ForeachKey || $defNode instanceof Node\ForeachValue) {
|
||||
return $this->getTypeFromNode($defNode);
|
||||
}
|
||||
if ($defNode instanceof Node\Parameter) {
|
||||
return $this->getTypeFromNode($defNode);
|
||||
}
|
||||
|
@ -900,7 +917,7 @@ class DefinitionResolver
|
|||
$keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer;
|
||||
}
|
||||
}
|
||||
$valueTypes = array_unique($keyTypes);
|
||||
$valueTypes = array_unique($valueTypes);
|
||||
$keyTypes = array_unique($keyTypes);
|
||||
if (empty($valueTypes)) {
|
||||
$valueType = null;
|
||||
|
@ -1080,6 +1097,27 @@ class DefinitionResolver
|
|||
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
|
||||
// Get the documented type the assignment resolves to.
|
||||
if (
|
||||
|
|
|
@ -337,6 +337,7 @@ class TextDocument
|
|||
if ($def === null) {
|
||||
return new Hover([], $range);
|
||||
}
|
||||
$contents = [];
|
||||
if ($def->declarationLine) {
|
||||
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
|
||||
}
|
||||
|
|
|
@ -554,6 +554,146 @@ class CompletionTest extends TestCase
|
|||
], 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()
|
||||
{
|
||||
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/method_return_type.php');
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
},
|
||||
"containerName": "A"
|
||||
},
|
||||
"type__tostring": "string[]",
|
||||
"type__tostring": "bool[]",
|
||||
"type": {},
|
||||
"declarationLine": "protected $foo;",
|
||||
"documentation": null,
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
},
|
||||
"containerName": "A"
|
||||
},
|
||||
"type__tostring": "\\__CLASS__[]",
|
||||
"type__tostring": "bool[]",
|
||||
"type": {},
|
||||
"declarationLine": "private static $deprecationsTriggered;",
|
||||
"documentation": null,
|
||||
|
|
Loading…
Reference in New Issue