phpstorm-stubs/tests/StubsTypeHintsTest.php

355 lines
18 KiB
PHP

<?php
declare(strict_types=1);
namespace StubTests;
use PHPUnit\Framework\Exception;
use RuntimeException;
use StubTests\Model\PHPClass;
use StubTests\Model\PHPFunction;
use StubTests\Model\PHPInterface;
use StubTests\Model\PHPMethod;
use StubTests\Model\PHPParameter;
use StubTests\Model\PhpVersions;
use StubTests\Model\StubProblemType;
use StubTests\Parsers\Utils;
use StubTests\TestData\Providers\EntitiesFilter;
use StubTests\TestData\Providers\PhpStormStubsSingleton;
use StubTests\TestData\Providers\ReflectionStubsSingleton;
class StubsTypeHintsTest extends BaseStubsTest
{
/**
* @dataProvider \StubTests\TestData\Providers\Reflection\ReflectionFunctionsProvider::allFunctionsProvider
* @param PHPFunction $function
* @throws RuntimeException
*/
public function testFunctionsReturnTypeHints(PHPFunction $function)
{
$functionName = $function->name;
$allEqualStubFunctions = EntitiesFilter::getFiltered(
PhpStormStubsSingleton::getPhpStormStubs()->getFunctions(),
fn (PHPFunction $stubFunction) => $stubFunction->name !== $functionName ||
!in_array(PhpVersions::getLatest(), Utils::getAvailableInVersions($stubFunction))
);
/** @var PHPFunction $stubFunction */
$stubFunction = array_pop($allEqualStubFunctions);
$conditionToCompareWithSignature = BaseStubsTest::ifReflectionTypesExistInSignature($function->returnTypesFromSignature, $stubFunction->returnTypesFromSignature);
$conditionToCompareWithAttribute = BaseStubsTest::ifReflectionTypesExistInAttributes($function->returnTypesFromSignature, $stubFunction->returnTypesFromAttribute);
$testCondition = $conditionToCompareWithSignature || $conditionToCompareWithAttribute;
self::assertTrue($testCondition, "Function $functionName has invalid return type.
Reflection function has return type " . implode('|', $function->returnTypesFromSignature) . ' but stubs has return type ' .
implode('|', $stubFunction->returnTypesFromSignature) . ' in signature and attribute has types ' .
BaseStubsTest::getStringRepresentationOfTypeHintsFromAttributes($stubFunction->returnTypesFromAttribute));
}
/**
* @dataProvider \StubTests\TestData\Providers\Reflection\ReflectionParametersProvider::functionParametersProvider
* @param PHPFunction $function
* @param PHPParameter $parameter
* @throws RuntimeException
*/
public function testFunctionsParametersTypeHints(PHPFunction $function, PHPParameter $parameter)
{
$functionName = $function->name;
$phpstormFunction = PhpStormStubsSingleton::getPhpStormStubs()->getFunction($functionName);
/** @var PHPParameter $stubParameter */
$stubParameter = current(array_filter($phpstormFunction->parameters, fn (PHPParameter $stubParameter) => $stubParameter->name === $parameter->name));
self::assertNotFalse($stubParameter, "Parameter $$parameter->name not found at $phpstormFunction->name(" .
StubsParameterNamesTest::printParameters($phpstormFunction->parameters) . ')');
self::compareTypeHintsWithReflection($parameter, $stubParameter, $functionName);
if (!$parameter->hasMutedProblem(StubProblemType::PARAMETER_REFERENCE)) {
self::assertEquals(
$parameter->is_passed_by_ref,
$stubParameter->is_passed_by_ref,
"Invalid pass by ref $functionName: \$$parameter->name "
);
}
self::assertEquals(
$parameter->is_vararg,
$stubParameter->is_vararg,
"Invalid vararg $functionName: \$$parameter->name "
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Reflection\ReflectionMethodsProvider::classMethodsWithReturnTypeHintProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $method
* @throws RuntimeException
*/
public function testMethodsReturnTypeHints(PHPClass|PHPInterface $class, PHPMethod $method)
{
$functionName = $method->name;
if ($class instanceof PHPClass) {
$stubMethod = PhpStormStubsSingleton::getPhpStormStubs()->getClass($class->name)->methods[$functionName];
} else {
$stubMethod = PhpStormStubsSingleton::getPhpStormStubs()->getInterface($class->name)->methods[$functionName];
}
self::assertEquals(
$method->returnTypesFromSignature,
$stubMethod->returnTypesFromSignature,
"Method $class->name::$functionName has invalid return type"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Reflection\ReflectionParametersProvider::methodParametersProvider
* @param PHPClass|PHPInterface $reflectionClass
* @param PHPMethod $reflectionMethod
* @param PHPParameter $reflectionParameter
* @throws RuntimeException
*/
public function testMethodsParametersTypeHints(PHPClass|PHPInterface $reflectionClass, PHPMethod $reflectionMethod, PHPParameter $reflectionParameter)
{
$className = $reflectionClass->name;
$methodName = $reflectionMethod->name;
if ($reflectionClass instanceof PHPClass) {
$stubMethod = PhpStormStubsSingleton::getPhpStormStubs()->getClass($className)->methods[$methodName];
} else {
$stubMethod = PhpStormStubsSingleton::getPhpStormStubs()->getInterface($className)->methods[$methodName];
}
/** @var PHPParameter $stubParameter */
$stubParameter = current(array_filter(
$stubMethod->parameters,
fn (PHPParameter $stubParameter) => $stubParameter->name === $reflectionParameter->name
));
self::assertNotFalse($stubParameter, "Parameter $$reflectionParameter->name not found at
$reflectionClass->name::$stubMethod->name(" .
StubsParameterNamesTest::printParameters($stubMethod->parameters) . ')');
if (!$reflectionParameter->hasMutedProblem(StubProblemType::PARAMETER_REFERENCE)) {
self::assertEquals(
$reflectionParameter->is_passed_by_ref,
$stubParameter->is_passed_by_ref,
"Invalid pass by ref $className::$methodName: \$$reflectionParameter->name "
);
}
self::assertEquals(
$reflectionParameter->is_vararg,
$stubParameter->is_vararg,
"Invalid pass by ref $className::$methodName: \$$reflectionParameter->name "
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForScalarTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $parameter
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveScalarTypeHintsInParameters(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $parameter)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
self::assertEmpty(
array_intersect(['int', 'float', 'string', 'bool'], $parameter->typesFromSignature),
"Method '$class->name::$stubMethod->name' with @since '$sinceVersion'
has parameter '$parameter->name' with typehint '" . implode('|', $parameter->typesFromSignature) .
"' but typehints available only since php 7"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForNullableTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $parameter
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveNullableTypeHintsInParameters(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $parameter)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
self::assertEmpty(
array_filter($parameter->typesFromSignature, fn (string $type) => str_contains($type, '?')),
"Method '$class->name::$stubMethod->name' with @since '$sinceVersion'
has nullable parameter '$parameter->name' with typehint '" . implode('|', $parameter->typesFromSignature) . "'
but nullable typehints available only since php 7.1"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForUnionTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $parameter
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveUnionTypeHintsInParameters(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $parameter)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
self::assertLessThan(
2,
count($parameter->typesFromSignature),
"Method '$class->name::$stubMethod->name' with @since '$sinceVersion'
has parameter '$parameter->name' with union typehint '" . implode('|', $parameter->typesFromSignature) . "'
but union typehints available only since php 8.0"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubMethodsProvider::methodsForReturnTypeHintTestsProvider
* @param PHPMethod $stubMethod
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveReturnTypeHint(PHPMethod $stubMethod)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
self::assertEmpty($stubMethod->returnTypesFromSignature, "Method '$stubMethod->parentName::$stubMethod->name' has since version '$sinceVersion'
but has return typehint '" . implode('|', $stubMethod->returnTypesFromSignature) . "' that supported only since PHP 7. Please declare return type via PhpDoc");
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubMethodsProvider::methodsForNullableReturnTypeHintTestsProvider
* @param PHPMethod $stubMethod
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveNullableReturnTypeHint(PHPMethod $stubMethod)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
$returnTypes = $stubMethod->returnTypesFromSignature;
self::assertEmpty(
array_filter($returnTypes, fn (string $type) => str_contains($type, '?')),
"Method '$stubMethod->parentName::$stubMethod->name' has since version '$sinceVersion'
but has nullable return typehint '" . implode('|', $returnTypes) . "' that supported only since PHP 7.1.
Please declare return type via PhpDoc"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubMethodsProvider::methodsForUnionReturnTypeHintTestsProvider
* @param PHPMethod $stubMethod
* @throws RuntimeException
*/
public static function testMethodDoesNotHaveUnionReturnTypeHint(PHPMethod $stubMethod)
{
$sinceVersion = Utils::getDeclaredSinceVersion($stubMethod);
self::assertLessThan(
2,
count($stubMethod->returnTypesFromSignature),
"Method '$stubMethod->parentName::$stubMethod->name' has since version '$sinceVersion'
but has union return typehint '" . implode('|', $stubMethod->returnTypesFromSignature) . "' that supported only since PHP 8.0.
Please declare return type via PhpDoc"
);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForAllowedScalarTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $stubParameter
* @throws RuntimeException
*/
public function testMethodScalarTypeHintsInParametersMatchReflection(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $stubParameter)
{
$reflectionMethods = array_filter(
ReflectionStubsSingleton::getReflectionStubs()->getClass($class->name)->methods,
fn (PHPMethod $method) => $method->name === $stubMethod->name
);
/** @var PHPMethod $reflectionMethod */
$reflectionMethod = array_pop($reflectionMethods);
$reflectionParameters = array_filter($reflectionMethod->parameters, fn (PHPParameter $parameter) => $parameter->name === $stubParameter->name);
$reflectionParameter = array_pop($reflectionParameters);
self::compareTypeHintsWithReflection($reflectionParameter, $stubParameter, $stubMethod->name);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForAllowedNullableTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $stubParameter
* @throws RuntimeException
*/
public function testMethodNullableTypeHintsInParametersMatchReflection(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $stubParameter)
{
$reflectionMethods = array_filter(
ReflectionStubsSingleton::getReflectionStubs()->getClass($class->name)->methods,
fn (PHPMethod $method) => $method->name === $stubMethod->name
);
/** @var PHPMethod $reflectionMethod */
$reflectionMethod = array_pop($reflectionMethods);
$reflectionParameters = array_filter($reflectionMethod->parameters, fn (PHPParameter $parameter) => $parameter->name === $stubParameter->name);
$reflectionParameter = array_pop($reflectionParameters);
self::compareTypeHintsWithReflection($reflectionParameter, $stubParameter, $stubMethod->name);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubsParametersProvider::parametersForAllowedUnionTypeHintTestsProvider
* @param PHPClass|PHPInterface $class
* @param PHPMethod $stubMethod
* @param PHPParameter $stubParameter
* @throws RuntimeException
*/
public function testMethodUnionTypeHintsInParametersMatchReflection(PHPClass|PHPInterface $class, PHPMethod $stubMethod, PHPParameter $stubParameter)
{
$reflectionMethods = array_filter(
ReflectionStubsSingleton::getReflectionStubs()->getClass($class->name)->methods,
fn (PHPMethod $method) => $method->name === $stubMethod->name
);
/** @var PHPMethod $reflectionMethod */
$reflectionMethod = array_pop($reflectionMethods);
$reflectionParameters = array_filter($reflectionMethod->parameters, fn (PHPParameter $parameter) => $parameter->name === $stubParameter->name);
$reflectionParameter = array_pop($reflectionParameters);
self::compareTypeHintsWithReflection($reflectionParameter, $stubParameter, $stubMethod->name);
}
/**
* @dataProvider \StubTests\TestData\Providers\Stubs\StubMethodsProvider::allFunctionAndMethodsWithReturnTypeHintsProvider
* @param PHPFunction|PHPMethod $method
* @throws Exception
*/
public static function testSignatureTypeHintsComplainPhpDocInMethods(PHPFunction|PHPMethod $method)
{
$functionName = $method->name;
$unifiedPhpDocTypes = array_map(function (string $type) {
$typeParts = explode('\\', $type);
$typeName = end($typeParts);
// replace array notations like int[] or array<string,mixed> to match the array type
return preg_replace(['/\w+\[]/', '/array<[a-z,\s]+>/'], 'array', $typeName);
}, $method->returnTypesFromPhpDoc);
$unifiedSignatureTypes = $method->returnTypesFromSignature;
if (count($unifiedSignatureTypes) === 1) {
$unifiedSignatureTypes = [];
$type = array_pop($method->returnTypesFromSignature);
if (str_contains($type, '?')) {
array_push($unifiedSignatureTypes, 'null');
}
$typeParts = explode('\\', ltrim($type, '?'));
$typeName = end($typeParts);
array_push($unifiedSignatureTypes, $typeName);
}
$typesIntersection = array_intersect($unifiedSignatureTypes, $unifiedPhpDocTypes);
self::assertSameSize(
$unifiedSignatureTypes,
$typesIntersection,
$method instanceof PHPMethod ? "Method $method->parentName::" : 'Function ' .
"$functionName has mismatch in phpdoc return type and signature return type\n
signature has " . implode('|', $unifiedSignatureTypes) . "\n
but phpdoc has " . implode('|', $unifiedPhpDocTypes)
);
}
private static function compareTypeHintsWithReflection(PHPParameter $parameter, PHPParameter $stubParameter, ?string $functionName): void
{
$unifiedStubsParameterTypes = [];
$unifiedStubsAttributesParameterTypes = [];
$unifiedReflectionParameterTypes = [];
self::convertNullableTypesToUnion($parameter->typesFromSignature, $unifiedReflectionParameterTypes);
if (!empty($stubParameter->typesFromSignature)) {
self::convertNullableTypesToUnion($stubParameter->typesFromSignature, $unifiedStubsParameterTypes);
} else {
foreach ($stubParameter->typesFromAttribute as $languageVersion => $listOfTypes) {
$unifiedStubsAttributesParameterTypes[$languageVersion] = [];
self::convertNullableTypesToUnion($listOfTypes, $unifiedStubsAttributesParameterTypes[$languageVersion]);
}
}
$conditionToCompareWithSignature = BaseStubsTest::ifReflectionTypesExistInSignature($unifiedReflectionParameterTypes, $unifiedStubsParameterTypes);
$conditionToCompareWithAttribute = BaseStubsTest::ifReflectionTypesExistInAttributes($unifiedReflectionParameterTypes, $unifiedStubsAttributesParameterTypes);
$testCondition = $conditionToCompareWithSignature || $conditionToCompareWithAttribute;
self::assertTrue($testCondition, "Type mismatch $functionName: \$$parameter->name \n
Reflection parameter has type '" . implode('|', $unifiedReflectionParameterTypes) .
"' but stub parameter has type '" . implode('|', $unifiedStubsParameterTypes) . "' in signature and " .
BaseStubsTest::getStringRepresentationOfTypeHintsFromAttributes($unifiedStubsAttributesParameterTypes) . ' in attribute');
}
}