Skip to content

Commit

Permalink
feat: foreach completion (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
phil-nelson authored and felixfbecker committed Dec 18, 2017
1 parent f46fccd commit 9eea26d
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 3 deletions.
37 changes: 37 additions & 0 deletions fixtures/completion/foreach.php
@@ -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) {
$
}
8 changes: 8 additions & 0 deletions src/CompletionProvider.php
Expand Up @@ -486,6 +486,14 @@ private function findVariableDefinitionsInNode(Node $node, string $namePrefix =

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
Expand Down
40 changes: 39 additions & 1 deletion src/DefinitionResolver.php
Expand Up @@ -568,6 +568,20 @@ public function resolveVariableToNode($var)
}
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) {
Expand Down Expand Up @@ -619,6 +633,9 @@ public function resolveExpressionNodeToType($expr)
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);
}
Expand Down Expand Up @@ -900,7 +917,7 @@ public function resolveExpressionNodeToType($expr)
$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;
Expand Down Expand Up @@ -1080,6 +1097,27 @@ public function getTypeFromNode($node)
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 (
Expand Down
1 change: 1 addition & 0 deletions src/Server/TextDocument.php
Expand Up @@ -337,6 +337,7 @@ public function hover(TextDocumentIdentifier $textDocument, Position $position):
if ($def === null) {
return new Hover([], $range);
}
$contents = [];
if ($def->declarationLine) {
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
}
Expand Down
140 changes: 140 additions & 0 deletions tests/Server/TextDocument/CompletionTest.php
Expand Up @@ -554,6 +554,146 @@ public function testBarePhp()
], 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');
Expand Down
Expand Up @@ -36,7 +36,7 @@
},
"containerName": "A"
},
"type__tostring": "string[]",
"type__tostring": "bool[]",
"type": {},
"declarationLine": "protected $foo;",
"documentation": null,
Expand Down
2 changes: 1 addition & 1 deletion tests/Validation/cases/magicConsts.php.expected.json
Expand Up @@ -40,7 +40,7 @@
},
"containerName": "A"
},
"type__tostring": "\\__CLASS__[]",
"type__tostring": "bool[]",
"type": {},
"declarationLine": "private static $deprecationsTriggered;",
"documentation": null,
Expand Down

0 comments on commit 9eea26d

Please sign in to comment.