diff --git a/.travis.yml b/.travis.yml index 1301d4a..cb4bcbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,5 +56,5 @@ stages: if: branch = master AND type = push AND fork = false branches: - except: - - /^v\d+\.\d+\.\d+$/ + only: + - master diff --git a/Performance.php b/Performance.php index 5022f84..6fc1c28 100644 --- a/Performance.php +++ b/Performance.php @@ -14,7 +14,7 @@ use RecursiveIteratorIterator; $totalSize = 0; -$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "CodeIgniter", "cakephp"]; +$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"]; foreach($frameworks as $framework) { $iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework"); diff --git a/README.md b/README.md index d3a4d46..5d95d61 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ Example: - [Eclipse Che](https://eclipse.org/che/) - [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php) - NeoVim: [LanguageServer-php-neovim](https://github.com/roxma/LanguageServer-php-neovim) with [LanguageClient neovim](https://github.com/autozimu/LanguageClient-neovim) + - Atom: [ide-php](https://github.com/atom/ide-php) ## Contributing diff --git a/fixtures/completion/foreach.php b/fixtures/completion/foreach.php index df8b6df..84bafd1 100644 --- a/fixtures/completion/foreach.php +++ b/fixtures/completion/foreach.php @@ -35,3 +35,12 @@ foreach ($array3 as $key => $value) { foreach ($bar->test() as $value) { $ } + +foreach ($unknownArray as $member->access => $unknown) { + $unkno + +foreach ($loop as $loop) { +} + +foreach ($loop->getArray() as $loop) { +} diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 905f9d6..f5e5f7b 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -545,6 +545,15 @@ class DefinitionResolver } else { throw new \InvalidArgumentException('$var must be Variable, Param or ClosureUse, not ' . get_class($var)); } + if (empty($name)) { + return null; + } + + $shouldDescend = function ($nodeToDescand) { + // Make sure not to decend into functions or classes (they represent a scope boundary) + return !($nodeToDescand instanceof PhpParser\FunctionLike || $nodeToDescand instanceof PhpParser\ClassLike); + }; + // Traverse the AST up do { // If a function is met, check the parameters and use statements @@ -569,37 +578,55 @@ 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 + // Check each previous sibling node and their descendents for a variable assignment to that variable + // Each previous sibling could contain a declaration of the variable while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) { - if ($n instanceof Node\Statement\ExpressionStatement) { - $n = $n->expression; - } - if ( - // TODO - clean this up - ($n instanceof Node\Expression\AssignmentExpression && $n->operator->kind === PhpParser\TokenKind::EqualsToken) - && $n->leftOperand instanceof Node\Expression\Variable && $n->leftOperand->getName() === $name - ) { + + // Check the sibling itself + if (self::isVariableDeclaration($n, $name)) { return $n; } + + // Check descendant of this sibling (e.g. the children of a previous if block) + foreach ($n->getDescendantNodes($shouldDescend) as $descendant) { + if (self::isVariableDeclaration($descendant, $name)) { + return $descendant; + } + } } } while (isset($n) && $n = $n->parent); // Return null if nothing was found return null; } + /** + * Checks whether the given Node declares the given variable name + * + * @param Node $n The Node to check + * @param string $name The name of the wanted variable + * @return bool + */ + private static function isVariableDeclaration(Node $n, string $name) + { + if ( + // TODO - clean this up + ($n instanceof Node\Expression\AssignmentExpression && $n->operator->kind === PhpParser\TokenKind::EqualsToken) + && $n->leftOperand instanceof Node\Expression\Variable && $n->leftOperand->getName() === $name + ) { + return true; + } + + if ( + ($n instanceof Node\ForeachValue || $n instanceof Node\ForeachKey) + && $n->expression instanceof Node\Expression\Variable + && $n->expression->getName() === $name + ) { + return true; + } + + return false; + } + /** * Given an expression node, resolves that expression recursively to a type. * If the type could not be resolved, returns Types\Mixed_. @@ -1122,6 +1149,7 @@ class DefinitionResolver if ($collectionType instanceof Types\Array_) { return $collectionType->getValueType(); } + return new Types\Mixed_(); } // PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS diff --git a/tests/Server/TextDocument/CompletionTest.php b/tests/Server/TextDocument/CompletionTest.php index ded5941..80d3026 100644 --- a/tests/Server/TextDocument/CompletionTest.php +++ b/tests/Server/TextDocument/CompletionTest.php @@ -691,6 +691,21 @@ class CompletionTest extends TestCase ), ] ], + 'foreach unknown type' => [ + new Position(39, 10), + [ + new CompletionItem( + '$unknown', + CompletionItemKind::VARIABLE, + 'mixed', + null, + null, + null, + null, + new TextEdit(new Range(new Position(39, 10), new Position(39, 10)), 'wn') + ), + ] + ], ]; }