Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ jobs:
extensions: ""
- script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src -c e2e/only-files-not-analysed-trait/ignore.neon"
extensions: ""
- script: "bin/phpstan analyse -c e2e/shadowed-native-function/phpstan.neon"
extensions: ""
- script: "bin/phpstan analyse e2e/only-files-not-analysed-trait/src/Foo.php e2e/only-files-not-analysed-trait/src/BarTrait.php -c e2e/only-files-not-analysed-trait/no-ignore.neon"
extensions: ""
- script: |
Expand Down
38 changes: 38 additions & 0 deletions e2e/shadowed-native-function/helpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

// The mockery/mockery shape of a polyfill: declared behind a function_exists()
// guard, so it never runs on a PHP version that has the function natively -
// and shipping a PHPDoc that contradicts the native signature.

if (!function_exists('str_contains')) {
/**
* @param non-empty-string $haystack
* @param non-empty-string $needle
*/
function str_contains(string $haystack, string $needle): bool
{
return $needle === '' || strpos($haystack, $needle) !== false;
}
}

if (!function_exists('str_starts_with')) {
/**
* @param non-empty-string $haystack
* @param non-empty-string $needle
*/
function str_starts_with(string $haystack, string $needle): bool
{
return strncmp($haystack, $needle, strlen($needle)) === 0;
}
}

if (!function_exists('str_ends_with')) {
/**
* @param non-empty-string $haystack
* @param non-empty-string $needle
*/
function str_ends_with(string $haystack, string $needle): bool
{
return $needle === '' || substr_compare($haystack, $needle, -strlen($needle)) === 0;
}
}
6 changes: 6 additions & 0 deletions e2e/shadowed-native-function/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
level: 8
paths:
- src
scanFiles:
- helpers.php
10 changes: 10 additions & 0 deletions e2e/shadowed-native-function/src/test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace ShadowedNativeFunctionE2e;

function doFoo(string $s): bool
{
return str_contains($s, 'x')
|| str_starts_with($s, 'y')
|| str_ends_with(haystack: $s, needle: 'z');
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use PHPStan\Reflection\BetterReflection\SourceLocator\RewriteClassAliasSourceLocator;
use PHPStan\Reflection\BetterReflection\SourceLocator\SkipClassAliasSourceLocator;
use PHPStan\Reflection\BetterReflection\SourceLocator\SkipPolyfillSourceLocator;
use PHPStan\Reflection\ConditionallyDeclaredSymbolDetector;
use PHPStan\Turbo\TurboExtensionEnabler;
use function array_merge;
use function array_unique;
Expand Down Expand Up @@ -64,6 +65,7 @@ public function __construct(
private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker,
private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory,
private FileNodesFetcher $fileNodesFetcher,
private ConditionallyDeclaredSymbolDetector $conditionallyDeclaredSymbolDetector,
#[AutowiredParameter]
private array $scanFiles,
#[AutowiredParameter]
Expand Down Expand Up @@ -163,7 +165,7 @@ public function create(): SourceLocator
}

if (count($composerLocators) > 0) {
$fileLocators[] = new SkipPolyfillSourceLocator(new AggregateSourceLocator($composerLocators), $this->phpVersion);
$fileLocators[] = new SkipPolyfillSourceLocator(new AggregateSourceLocator($composerLocators), $this->phpVersion, $this->conditionallyDeclaredSymbolDetector, $this->phpstormStubsSourceStubber);
}

if (extension_loaded('phar')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@
use PHPStan\BetterReflection\Reflection\ReflectionConstant;
use PHPStan\BetterReflection\Reflection\ReflectionFunction;
use PHPStan\BetterReflection\Reflector\Reflector;
use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ConditionallyDeclaredSymbolDetector;
use function str_contains;
use function str_replace;

final class SkipPolyfillSourceLocator implements SourceLocator
{

public function __construct(private SourceLocator $sourceLocator, private PhpVersion $phpVersion)
public function __construct(
private SourceLocator $sourceLocator,
private PhpVersion $phpVersion,
private ConditionallyDeclaredSymbolDetector $conditionallyDeclaredSymbolDetector,
private PhpStormStubsSourceStubber $phpstormStubsSourceStubber,
)
{
}

Expand Down Expand Up @@ -52,12 +59,39 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
if (str_contains($normalized, '/symfony/polyfill-php85/') && $this->phpVersion->getVersionId() >= 80500) {
return null;
}
if ($this->isShadowingNativeSymbol($reflection, $fileName)) {
return null;
}
}
}

return $reflection;
}

/**
* A polyfill guards its declaration so that it never runs when PHP provides
* the symbol natively. Its shape is then only an approximation of the real
* one and must not be reflected instead of it.
*
* Functions are left alone here: hiding them would also hide their
* existence from a PHP version that does not have them natively yet.
* NativeFunctionReflectionProvider prefers the native signature instead.
*/
private function isShadowingNativeSymbol(ReflectionClass|ReflectionFunction|ReflectionConstant $reflection, string $fileName): bool
{
if ($reflection instanceof ReflectionClass) {
return $this->conditionallyDeclaredSymbolDetector->isConditionallyDeclaredClass($fileName, $reflection->getName())
&& $this->phpstormStubsSourceStubber->isPresentClass($reflection->getName()) === true;
}

if ($reflection instanceof ReflectionConstant) {
return $this->conditionallyDeclaredSymbolDetector->isConditionallyDeclaredConstant($fileName, $reflection->getName())
&& $this->phpstormStubsSourceStubber->generateConstantStub($reflection->getName()) !== null;
}

return false;
}

#[Override]
public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array
{
Expand Down
156 changes: 156 additions & 0 deletions src/Reflection/ConditionallyDeclaredSymbolDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection;

use PhpParser\Node;
use PhpParser\Node\Stmt;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Parser\Parser;
use Throwable;
use function array_key_exists;
use function count;
use function strtolower;

/**
* Answers whether a symbol is declared inside a conditional block, which is how
* every polyfill guards its declaration (`function_exists()`, `class_exists()`,
* `defined()`, a `PHP_VERSION_ID` comparison, ...). Such a declaration is dead
* code whenever PHP provides the symbol natively, so it must not shadow it.
*
* @phpstan-type Symbols array{functions: array<string, true>, classes: array<string, true>, constants: array<string, true>}
*/
#[AutowiredService]
final class ConditionallyDeclaredSymbolDetector
{

private const FILE_CACHE_LIMIT = 128;

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

public function __construct(
#[AutowiredParameter(ref: '@php8Parser')]
private Parser $parser,
)
{
}

public function isConditionallyDeclaredFunction(string $fileName, string $functionName): bool
{
return array_key_exists(strtolower($functionName), $this->getSymbols($fileName)['functions']);
}

public function isConditionallyDeclaredClass(string $fileName, string $className): bool
{
return array_key_exists(strtolower($className), $this->getSymbols($fileName)['classes']);
}

/** Constant names are case-sensitive, unlike function and class names. */
public function isConditionallyDeclaredConstant(string $fileName, string $constantName): bool
{
return array_key_exists($constantName, $this->getSymbols($fileName)['constants']);
}

/**
* @return Symbols
*/
private function getSymbols(string $fileName): array
{
if (array_key_exists($fileName, $this->cache)) {
return $this->cache[$fileName];
}

$symbols = [
'functions' => [],
'classes' => [],
'constants' => [],
];
try {
$this->findInStmts($this->parser->parseFile($fileName), false, $symbols);
} catch (Throwable) {
// an unparseable or unreadable file tells us nothing

Check warning on line 72 in src/Reflection/ConditionallyDeclaredSymbolDetector.php

View workflow job for this annotation

GitHub Actions / Check for typos

"unparseable" should be "unparsable".
}

if (count($this->cache) >= self::FILE_CACHE_LIMIT) {
$this->cache = [];
}

return $this->cache[$fileName] = $symbols;
}

/**
* @param Stmt[] $stmts
* @param Symbols $symbols
*/
private function findInStmts(array $stmts, bool $conditional, array &$symbols): void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof Stmt\Function_) {
if ($conditional) {
$symbols['functions'][strtolower((string) ($stmt->namespacedName ?? $stmt->name))] = true;
}
continue;
}

if ($stmt instanceof Stmt\ClassLike) {
if ($conditional && $stmt->name !== null) {
$symbols['classes'][strtolower((string) ($stmt->namespacedName ?? $stmt->name))] = true;
}
continue;
}

if ($stmt instanceof Stmt\Expression) {
if ($conditional) {
$this->findDefineCall($stmt->expr, $symbols);
}
continue;
}

if (
$stmt instanceof Stmt\Namespace_
|| $stmt instanceof Stmt\Declare_
|| $stmt instanceof Stmt\Block
) {
$this->findInStmts($stmt->stmts ?? [], $conditional, $symbols);
continue;
}

if (!$stmt instanceof Stmt\If_) {
continue;
}

$this->findInStmts($stmt->stmts, true, $symbols);
foreach ($stmt->elseifs as $elseIf) {
$this->findInStmts($elseIf->stmts, true, $symbols);
}
if ($stmt->else === null) {
continue;
}

$this->findInStmts($stmt->else->stmts, true, $symbols);
}
}

/**
* @param Symbols $symbols
*/
private function findDefineCall(Node\Expr $expr, array &$symbols): void
{
if (!$expr instanceof Node\Expr\FuncCall) {
return;
}

if (!$expr->name instanceof Node\Name || $expr->name->toLowerString() !== 'define') {
return;
}

$args = $expr->getArgs();
if (!isset($args[0]) || !$args[0]->value instanceof Node\Scalar\String_) {
return;
}

$symbols['constants'][$args[0]->value->value] = true;
}

}
22 changes: 20 additions & 2 deletions src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction;
use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
use PHPStan\BetterReflection\Reflector\Reflector;
use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDoc\StubPhpDocProvider;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\AttributeReflectionFactory;
use PHPStan\Reflection\ConditionallyDeclaredSymbolDetector;
use PHPStan\Reflection\ExtendedFunctionVariant;
use PHPStan\Reflection\InitializerExprContext;
use PHPStan\Reflection\Native\ExtendedNativeParameterReflection;
Expand All @@ -25,7 +27,6 @@
use PHPStan\Type\TypehintHelper;
use function array_key_exists;
use function array_map;
use function str_contains;
use function strtolower;

#[AutowiredService]
Expand All @@ -43,6 +44,8 @@ public function __construct(
private StubPhpDocProvider $stubPhpDocProvider,
private AttributeReflectionFactory $attributeReflectionFactory,
private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider,
private ConditionallyDeclaredSymbolDetector $conditionallyDeclaredSymbolDetector,
private PhpStormStubsSourceStubber $phpstormStubsSourceStubber,
)
{
}
Expand Down Expand Up @@ -78,7 +81,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
$isDeprecated = $reflectionFunction->isDeprecated();
if ($reflectionFunction->getFileName() !== null) {
$fileName = $reflectionFunction->getFileName();
if (!$reflectionFunctionAdapter->isInternal() && !str_contains(strtolower($fileName), 'polyfill')) {
if (!$reflectionFunctionAdapter->isInternal() && !$this->isPolyfill($fileName, $realFunctionName)) {
return null;
}
$docComment = $reflectionFunction->getDocComment();
Expand Down Expand Up @@ -196,6 +199,21 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
return $functionReflection;
}

/**
* A userland declaration of a function PHP provides natively can only be a
* polyfill - PHP would fatal on the redeclaration otherwise - so it is
* guarded by a conditional and never runs. Its PHPDoc must not replace the
* native signature.
*/
private function isPolyfill(string $fileName, string $functionName): bool
{
if ($this->phpstormStubsSourceStubber->isPresentFunction($functionName) !== true) {
return false;
}

return $this->conditionallyDeclaredSymbolDetector->isConditionallyDeclaredFunction($fileName, $functionName);
}

private function getReturnTypeFromPhpDoc(ResolvedPhpDocBlock $phpDoc): ?Type
{
$returnTag = $phpDoc->getReturnTag();
Expand Down
4 changes: 3 additions & 1 deletion src/Testing/TestCaseSourceLocatorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository;
use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator;
use PHPStan\Reflection\BetterReflection\SourceLocator\SkipPolyfillSourceLocator;
use PHPStan\Reflection\ConditionallyDeclaredSymbolDetector;
use ReflectionClass;
use function dirname;
use function hash;
Expand All @@ -44,6 +45,7 @@ public function __construct(
private PhpStormStubsSourceStubber $phpstormStubsSourceStubber,
private ReflectionSourceStubber $reflectionSourceStubber,
private PhpVersion $phpVersion,
private ConditionallyDeclaredSymbolDetector $conditionallyDeclaredSymbolDetector,
private array $fileExtensions,
private ?array $excludePaths,
)
Expand Down Expand Up @@ -84,7 +86,7 @@ public function create(): SourceLocator
$composerLocators[] = $composerSourceLocator;
}

self::$composerSourceLocatorsCache[$cacheKey] = [new SkipPolyfillSourceLocator(new AggregateSourceLocator($composerLocators), $this->phpVersion)];
self::$composerSourceLocatorsCache[$cacheKey] = [new SkipPolyfillSourceLocator(new AggregateSourceLocator($composerLocators), $this->phpVersion, $this->conditionallyDeclaredSymbolDetector, $this->phpstormStubsSourceStubber)];
}

$locators = self::$composerSourceLocatorsCache[$cacheKey] ?? [];
Expand Down
Loading
Loading