1
0
Fork 0

feat: foreach completion (#551)

pull/529/merge
Phil Nelson 2017-12-18 12:55:12 +11:00 committed by Felix Becker
parent f46fccd0d3
commit 9eea26df71
7 changed files with 227 additions and 3 deletions

View File

@ -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) {
$
}

View File

@ -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

View File

@ -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 (

View File

@ -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);
} }

View File

@ -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');

View File

@ -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,

View File

@ -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,