Skip to content

Commit

Permalink
Array functions stubs
Browse files Browse the repository at this point in the history
  • Loading branch information
BackEndTea committed Jun 15, 2021
1 parent 2abb92e commit a9dd9af
Show file tree
Hide file tree
Showing 14 changed files with 312 additions and 13 deletions.
2 changes: 2 additions & 0 deletions conf/bleedingEdge.neon
Expand Up @@ -21,3 +21,5 @@ parameters:
apiRules: true
deepInspectTypes: true
neverInGenericReturnType: true
stubFiles:
- ../stubs/arrayFunctions.stub
4 changes: 2 additions & 2 deletions src/Analyser/MutatingScope.php
Expand Up @@ -3234,8 +3234,8 @@ public function enterForeachKey(Expr $iteratee, string $keyName): self
*/
public function enterCatch(array $classes, ?string $variableName): self
{
$type = TypeCombinator::union(...array_map(static function (string $class): ObjectType {
return new ObjectType($class);
$type = TypeCombinator::union(...array_map(static function (\PhpParser\Node\Name $class): ObjectType {
return new ObjectType((string) $class);
}, $classes));

return $this->enterCatchType($type, $variableName);
Expand Down
36 changes: 34 additions & 2 deletions src/PhpDoc/StubPhpDocProvider.php
Expand Up @@ -52,6 +52,9 @@ class StubPhpDocProvider
/** @var array<string, array<string, array<string>>> */
private array $knownMethodsParameterNames = [];

/** @var array<string, array<string>> */
private array $knownFunctionParameterNames = [];

/**
* @param \PHPStan\Parser\Parser $parser
* @param string[] $stubFiles
Expand Down Expand Up @@ -164,7 +167,13 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p
return null;
}

public function findFunctionPhpDoc(string $functionName): ?ResolvedPhpDocBlock
/**
* @param string $functionName
* @param array<int, string> $positionalParameterNames
* @return ResolvedPhpDocBlock|null
* @throws \PHPStan\ShouldNotHappenException
*/
public function findFunctionPhpDoc(string $functionName, array $positionalParameterNames): ?ResolvedPhpDocBlock
{
if (!$this->isKnownFunction($functionName)) {
return null;
Expand All @@ -176,14 +185,29 @@ public function findFunctionPhpDoc(string $functionName): ?ResolvedPhpDocBlock

if (array_key_exists($functionName, $this->knownFunctionsDocComments)) {
[$file, $docComment] = $this->knownFunctionsDocComments[$functionName];
$this->functionMap[$functionName] = $this->fileTypeMapper->getResolvedPhpDoc(
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$file,
null,
null,
$functionName,
$docComment
);

if (!isset($this->knownFunctionParameterNames[$functionName])) {
throw new \PHPStan\ShouldNotHappenException();
}

$functionParameterNames = $this->knownFunctionParameterNames[$functionName];
$parameterNameMapping = [];
foreach ($positionalParameterNames as $i => $parameterName) {
if (!array_key_exists($i, $functionParameterNames)) {
continue;
}
$parameterNameMapping[$functionParameterNames[$i]] = $parameterName;
}

$this->functionMap[$functionName] = $resolvedPhpDoc->changeParameterNamesByMapping($parameterNameMapping);

return $this->functionMap[$functionName];
}

Expand Down Expand Up @@ -252,6 +276,14 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void
$this->functionMap[$functionName] = null;
return;
}
$this->knownFunctionParameterNames[$functionName] = array_map(static function (Node\Param $param): string {
if (!$param->var instanceof Variable || !is_string($param->var->name)) {
throw new \PHPStan\ShouldNotHappenException();
}

return $param->var->name;
}, $node->getParams());

$this->knownFunctionsDocComments[$functionName] = [$stubFile, $docComment->getText()];
return;
}
Expand Down
4 changes: 3 additions & 1 deletion src/Reflection/BetterReflection/BetterReflectionProvider.php
Expand Up @@ -281,7 +281,9 @@ private function getCustomFunction(string $functionName): \PHPStan\Reflection\Ph
$isInternal = false;
$isFinal = false;
$isPure = null;
$resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName());
$resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string {
return $parameter->getName();
}, $reflectionFunction->getParameters()));
if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) {
$fileName = $reflectionFunction->getFileName();
$docComment = $reflectionFunction->getDocComment();
Expand Down
4 changes: 3 additions & 1 deletion src/Reflection/Runtime/RuntimeReflectionProvider.php
Expand Up @@ -265,7 +265,9 @@ private function getCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope
$isInternal = false;
$isFinal = false;
$isPure = null;
$resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName());
$resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string {
return $parameter->getName();
}, $reflectionFunction->getParameters()));
if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) {
$fileName = $reflectionFunction->getFileName();
$docComment = $reflectionFunction->getDocComment();
Expand Down
49 changes: 43 additions & 6 deletions src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php
Expand Up @@ -4,6 +4,8 @@

use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName;
use PHPStan\BetterReflection\Reflector\FunctionReflector;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDoc\StubPhpDocProvider;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\Native\NativeFunctionReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
Expand All @@ -17,6 +19,8 @@
use PHPStan\Type\NullType;
use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;
use PHPStan\Type\UnionType;

class NativeFunctionReflectionProvider
Expand All @@ -31,11 +35,14 @@ class NativeFunctionReflectionProvider

private \PHPStan\Type\FileTypeMapper $fileTypeMapper;

public function __construct(SignatureMapProvider $signatureMapProvider, FunctionReflector $functionReflector, FileTypeMapper $fileTypeMapper)
private StubPhpDocProvider $stubPhpDocProvider;

public function __construct(SignatureMapProvider $signatureMapProvider, FunctionReflector $functionReflector, FileTypeMapper $fileTypeMapper, StubPhpDocProvider $stubPhpDocProvider)
{
$this->signatureMapProvider = $signatureMapProvider;
$this->functionReflector = $functionReflector;
$this->fileTypeMapper = $fileTypeMapper;
$this->stubPhpDocProvider = $stubPhpDocProvider;
}

public function findFunctionReflection(string $functionName): ?NativeFunctionReflection
Expand All @@ -48,17 +55,30 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
if (!$this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName)) {
return null;
}
$reflectionFunction = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null);

$phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static function (ParameterSignature $parameter): string {
return $parameter->getName();
}, $reflectionFunction->getParameters()));

$variants = [];
$i = 0;
while ($this->signatureMapProvider->hasFunctionSignature($lowerCasedFunctionName, $i)) {
$functionSignature = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null, $i);
$returnType = $functionSignature->getReturnType();
$variants[] = new FunctionVariant(
TemplateTypeMap::createEmpty(),
null,
array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName): NativeParameterReflection {
array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterReflection {
$type = $parameterSignature->getType();
$defaultValue = null;

$phpDocType = null;
if ($phpDoc !== null) {
$phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null;
if ($phpDocParam !== null) {
$phpDocType = $phpDocParam->getType();
}
}
if (
$parameterSignature->getName() === 'values'
&& (
Expand Down Expand Up @@ -94,17 +114,24 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
);
}

if (
$lowerCasedFunctionName === 'array_reduce'
&& $parameterSignature->getName() === 'initial'
) {
$defaultValue = new NullType();
}

return new NativeParameterReflection(
$parameterSignature->getName(),
$parameterSignature->isOptional(),
$type,
TypehintHelper::decideType($type, $phpDocType),
$parameterSignature->passedByReference(),
$parameterSignature->isVariadic(),
null
$defaultValue
);
}, $functionSignature->getParameters()),
$functionSignature->isVariadic(),
$returnType
TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null)
);

$i++;
Expand Down Expand Up @@ -145,4 +172,14 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
return $functionReflection;
}

private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type
{
$returnTag = $phpDoc->getReturnTag();
if ($returnTag === null) {
return null;
}

return $returnTag->getType();
}

}
2 changes: 1 addition & 1 deletion src/Type/CallableType.php
Expand Up @@ -139,7 +139,7 @@ function () use ($level): string {
return sprintf(
'callable(%s): %s',
implode(', ', array_map(
static function (NativeParameterReflection $param) use ($level): string {
static function (ParameterReflection $param) use ($level): string {
return sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level));
},
$this->getParameters()
Expand Down
67 changes: 67 additions & 0 deletions stubs/arrayFunctions.stub
@@ -0,0 +1,67 @@
<?php

/**
* @template TIn of mixed
* @template TReturn of mixed
*
* @param array<TIn> $one
* @param callable(TReturn, TIn): TReturn $two
* @param TReturn $three
*
* @return TReturn
*/
function array_reduce(
array $one,
callable $two,
$three = null
) {}

/**
* @template TKey of array-key
* @template TValue of mixed
* @template TUser of mixed
*
* @param array<TKey, TValue> $one
* @param callable(int, TKey, TUser=): mixed $two
* @param TUser $three
*
* @return true
*/
function array_walk(
array &$one,
callable $two,
$three = null
): bool {}

/**
* @template T of mixed
*
* @param array<T> $one
* @param callable(T, T): int $two
*/
function uasort(
array &$one,
callable $two
): bool {}

/**
* @template T of mixed
*
* @param array<T> $one
* @param callable(T, T): int $two
*/
function usort(
array &$one,
callable $two
): bool {}

/**
* @template T
*
* @param array<T, mixed> $one
* @param callable(T, T): int $two
*/
function uksort(
array &$one,
callable $two
): bool {}
84 changes: 84 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Expand Up @@ -7,6 +7,7 @@
use PHPStan\Rules\NullsafeCheck;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\RuleLevelHelper;
use const PHP_VERSION_ID;

/**
* @extends \PHPStan\Testing\RuleTestCase<CallToFunctionParametersRule>
Expand Down Expand Up @@ -554,4 +555,87 @@ public function testBugNumberFormatNamedArguments(): void
$this->analyse([__DIR__ . '/data/number-format-named-arguments.php'], []);
}

public function testArrayReduceCallback(): void
{
$this->analyse([__DIR__ . '/data/array_reduce.php'], [
[
'Parameter #2 $callback of function array_reduce expects callable(string, int|string): string, Closure(string, string): string given.',
5,
],
[
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.',
13,
],
[
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.',
22,
],
]);
}

public function testArrayWalkCallback(): void
{
$this->analyse([__DIR__ . '/data/array_walk.php'], [
[
'Parameter #2 $callback of function array_walk expects callable(int, string, *NEVER*): mixed, Closure(stdClass, float): \'\' given.',
6,
],
[
'Parameter #2 $callback of function array_walk expects callable(int, string, int|string): mixed, Closure(int, string, int): \'\' given.',
14,
],
]);
}

public function testUasortCallback(): void
{
$paramTwoName = PHP_VERSION_ID >= 80000
? 'callback'
: 'cmp_function';

$this->analyse([__DIR__ . '/data/uasort.php'], [
[
sprintf(
'Parameter #2 $%s of function uasort expects callable(int|string, int|string): int, Closure(string, string): 1 given.',
$paramTwoName
),
7,
],
]);
}

public function testUsortCallback(): void
{
$paramTwoName = PHP_VERSION_ID >= 80000
? 'callback'
: 'cmp_function';

$this->analyse([__DIR__ . '/data/usort.php'], [
[
sprintf(
'Parameter #2 $%s of function usort expects callable(int|string, int|string): int, Closure(string, string): 1 given.',
$paramTwoName
),
7,
],
]);
}

public function testUksortCallback(): void
{
$paramTwoName = PHP_VERSION_ID >= 80000
? 'callback'
: 'cmp_function';

$this->analyse([__DIR__ . '/data/uksort.php'], [
[
sprintf(
'Parameter #2 $%s of function uksort expects callable(stdClass|string, stdClass|string): int, Closure(stdClass, stdClass): 1 given.',
$paramTwoName
),
7,
],
]);
}

}

0 comments on commit a9dd9af

Please sign in to comment.