Commit 157cf5f8 authored by Gert's avatar Gert

Support arrow functions introduced in PHP 7.4

parent f24727ee
Pipeline #75819464 passed with stages
in 9 minutes and 9 seconds
## 5.0.0 (Unreleased)
* Support PHP 7.4 arrow functions
* Support PHP 7.4 typed properties
* An emulative lexer is now used, allowing you to parse more recent code (with the new HEREDOC, arrow functions, ...) when running on older PHP versions as well
* Using a PHP version that is newer or as recent as the one used by your code is still recommended, though.
* [Classlike, function and constant completions now show their FQSEN in the `detail` property instead of the `label`](https://gitlab.com/Serenata/Serenata/issues/269)
* Fix namespace autocompletion suggestions not having their backslashes properly escaped
* Fix classlike autocompletion suggestions not having their backslashes properly escaped when starting with a leading slash
......
......@@ -14,7 +14,8 @@ final class ClosureNodeTypeDeducer extends AbstractNodeTypeDeducer
*/
public function deduce(TypeDeductionContext $context): array
{
if (!$context->getNode() instanceof Node\Expr\Closure) {
if (!$context->getNode() instanceof Node\Expr\Closure &&
!$context->getNode() instanceof Node\Expr\ArrowFunction) {
throw new TypeDeductionException("Can't handle node of type " . get_class($context->getNode()));
}
......
......@@ -251,6 +251,7 @@ final class NodeTypeDeducer extends AbstractNodeTypeDeducer
Node\Expr\ConstFetch::class => $this->constFetchNodeTypeDeducer,
Node\Expr\ArrayDimFetch::class => $this->arrayDimFetchNodeTypeDeducer,
Node\Expr\Closure::class => $this->closureNodeTypeDeducer,
Node\Expr\ArrowFunction::class => $this->closureNodeTypeDeducer,
Node\Expr\New_::class => $this->newNodeTypeDeducer,
Node\Expr\Clone_::class => $this->cloneNodeTypeDeducer,
Node\Expr\Array_::class => $this->arrayNodeTypeDeducer,
......
......@@ -54,7 +54,9 @@ final class FunctionLikeBodyOffsetAttachingVisitor extends NodeVisitorAbstract
$byteOffset = $node->getAttribute('startFilePos');
for ($i = $node->getAttribute('startTokenPos'); $i < $node->getAttribute('endTokenPos'); ++$i) {
if ($this->tokens[$i] === '{') {
if ($node instanceof Node\Expr\ArrowFunction && $this->tokens[$i][0] === T_DOUBLE_ARROW) {
return $byteOffset + 1;
} elseif ($this->tokens[$i] === '{') {
return $byteOffset;
}
......@@ -74,6 +76,10 @@ final class FunctionLikeBodyOffsetAttachingVisitor extends NodeVisitorAbstract
{
$byteOffset = $node->getAttribute('endFilePos');
if ($node instanceof Node\Expr\ArrowFunction) {
return $byteOffset;
}
for ($i = $node->getAttribute('endTokenPos'); $i > 0; --$i) {
if ($this->tokens[$i] === '}') {
return $byteOffset;
......
......@@ -58,6 +58,7 @@ final class ScopeLimitingVisitor extends NodeVisitorAbstract
$node instanceof Node\Stmt\Function_ ||
$node instanceof Node\Stmt\ClassMethod ||
$node instanceof Node\Expr\Closure ||
$node instanceof Node\Expr\ArrowFunction ||
$node instanceof Node\Stmt\If_ ||
$node instanceof Node\Stmt\TryCatch ||
$node instanceof Node\Stmt\While_ ||
......
......@@ -206,15 +206,18 @@ final class TypeQueryingVisitor extends NodeVisitorAbstract
} elseif ($node instanceof Node\FunctionLike) {
$variablesOutsideCurrentScope = ['$this'];
// If a variable is in a use() statement of a closure, we can't reset the state as we still need to
// examine the parent scope of the closure where the variable is defined.
if ($node instanceof Node\Expr\Closure) {
foreach ($node->uses as $closureUse) {
$variablesOutsideCurrentScope[] = '$' . $closureUse->var->name;
if (!$node instanceof Node\Expr\ArrowFunction) {
// If a variable is in a use() statement of a closure, we can't reset the state as we still need to
// examine the parent scope of the closure where the variable is defined. Arrow functions simply
// capture all variables of their parent scope.
if ($node instanceof Node\Expr\Closure) {
foreach ($node->uses as $closureUse) {
$variablesOutsideCurrentScope[] = '$' . $closureUse->var->name;
}
}
}
$this->expressionTypeInfoMap->removeAllExcept($variablesOutsideCurrentScope);
$this->expressionTypeInfoMap->removeAllExcept($variablesOutsideCurrentScope);
}
foreach ($node->getParams() as $param) {
$this->expressionTypeInfoMap->setBestMatch('$' . $param->var->name, $node);
......
......@@ -137,7 +137,12 @@ class SignatureHelpRetriever
}
$argumentNode = NodeHelpers::findNodeOfAnyTypeInNodePath($node, Node\Arg::class);
$closureNode = NodeHelpers::findNodeOfAnyTypeInNodePath($node, Node\Expr\Closure::class);
$closureNode = NodeHelpers::findNodeOfAnyTypeInNodePath(
$node,
Node\Expr\Closure::class,
Node\Expr\ArrowFunction::class
);
if ($argumentNode) {
if ($argumentNode->getAttribute('parent') !== $invocationNode) {
......
......@@ -3,7 +3,7 @@ parameters:
services:
lexer:
class: PhpParser\Lexer
class: PhpParser\Lexer\Emulative
arguments:
- usedAttributes:
- comments
......
......@@ -146,6 +146,24 @@ class ExpressionTypeDeducerTest extends AbstractIntegrationTest
static::assertSame([], $output);
}
/**
* @return void
*/
public function testCorrectlyMovesBeyondClosureScopeForArrowFunctions(): void
{
$output = $this->deduceTypesFromExpression('ClosureVariableArrowFunction.phpt', '$e');
static::assertSame(['\A\E'], $output);
$output = $this->deduceTypesFromExpression('ClosureVariableArrowFunction.phpt', '$d');
static::assertSame(['\A\D'], $output);
$output = $this->deduceTypesFromExpression('ClosureVariableArrowFunction.phpt', '$a');
static::assertSame(['\A\A'], $output);
}
/**
* @return void
*/
......@@ -784,6 +802,19 @@ class ExpressionTypeDeducerTest extends AbstractIntegrationTest
static::assertSame(['\Closure'], $result);
}
/**
* @return void
*/
public function testCorrectlyAnalyzesArrowFunctionClosures(): void
{
$result = $this->deduceTypesFromExpression(
'ArrowFunctionClosure.phpt',
'$var'
);
static::assertSame(['\Closure'], $result);
}
/**
* @return void
*/
......
<?php
namespace A;
function test(D $d)
{
$e = new E();
$closure = fn (A $a) => foo(/* <MARKER> */);
}
......@@ -118,6 +118,33 @@ class SignatureHelpRetrieverTest extends AbstractIntegrationTest
static::assertSignatureHelpSignaturesEquals($fileName, 119, 119, $expectedSignaturesResult);
}
/**
* @return void
*/
public function testFunctionCallDoesNotWorkWhenInsideArrowFunctionClosureArgumentBody(): void
{
$expectedSignaturesResult = [
new SignatureInformation('test(Closure $a)', null, [
new ParameterInformation('Closure $a', null),
]),
];
$fileName = 'FunctionCallArrowFunctionClosureArgumentBody.phpt';
static::assertSignatureHelpSignaturesEquals($fileName, 80, 88, $expectedSignaturesResult);
for ($i = 89; $i <= 90; ++$i) {
$hadException = false;
static::assertNull(
$this->getSignatureHelp($fileName, $i),
'Signature help should not trigger inside the body of arrow function arguments'
);
}
static::assertSignatureHelpSignaturesEquals($fileName, 91, 91, $expectedSignaturesResult);
}
/**
* @return void
*/
......@@ -391,7 +418,7 @@ class SignatureHelpRetrieverTest extends AbstractIntegrationTest
while ($i <= $end) {
$result = $this->getSignatureHelp($fileName, $i);
static::assertNotNull($result);
static::assertNotNull($result, 'Expected signature help at offset ' . $i);
static::assertSame(0, $result->getActiveSignature());
static::assertEquals($signatures, $result->getSignatures());
......
<?php
namespace A;
/**
* @param \Closure $a
*/
function test($a)
{
test(fn($a) => 5);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment