Skip to content

Refactor ComposerPhpVersionFactory, ConstantResolver #3627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 13, 2024
Merged
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
1 change: 0 additions & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,6 @@ services:
-
class: PHPStan\Php\ComposerPhpVersionFactory
arguments:
phpVersion: %phpVersion%
composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths%

-
Expand Down
89 changes: 68 additions & 21 deletions src/Analyser/ConstantResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
namespace PHPStan\Analyser;

use PhpParser\Node\Name;
use PHPStan\Php\ComposerPhpVersionFactory;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\NamespaceAnswerer;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
Expand All @@ -21,6 +23,8 @@
use PHPStan\Type\UnionType;
use function array_key_exists;
use function in_array;
use function is_array;
use function is_int;
use function max;
use function sprintf;
use const INF;
Expand All @@ -35,12 +39,13 @@ final class ConstantResolver

/**
* @param string[] $dynamicConstantNames
* @param int|array{min: int, max: int}|null $phpVersion
*/
public function __construct(
private ReflectionProviderProvider $reflectionProviderProvider,
private array $dynamicConstantNames,
private ?PhpVersion $composerMinPhpVersion,
private ?PhpVersion $composerMaxPhpVersion,
private int|array|null $phpVersion,
private ComposerPhpVersionFactory $composerPhpVersionFactory,
)
{
}
Expand Down Expand Up @@ -83,15 +88,23 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type
new AccessoryNonFalsyStringType(),
]);
}

$minPhpVersion = null;
$maxPhpVersion = null;
if (in_array($resolvedConstantName, ['PHP_VERSION_ID', 'PHP_MAJOR_VERSION', 'PHP_MINOR_VERSION', 'PHP_RELEASE_VERSION'], true)) {
$minPhpVersion = $this->getMinPhpVersion();
$maxPhpVersion = $this->getMaxPhpVersion();
}

if ($resolvedConstantName === 'PHP_MAJOR_VERSION') {
$minMajor = 5;
$maxMajor = null;

if ($this->composerMinPhpVersion !== null) {
$minMajor = max($minMajor, $this->composerMinPhpVersion->getMajorVersionId());
if ($minPhpVersion !== null) {
$minMajor = max($minMajor, $minPhpVersion->getMajorVersionId());
}
if ($this->composerMaxPhpVersion !== null) {
$maxMajor = $this->composerMaxPhpVersion->getMajorVersionId();
if ($maxPhpVersion !== null) {
$maxMajor = $maxPhpVersion->getMajorVersionId();
}

return $this->createInteger($minMajor, $maxMajor);
Expand All @@ -101,12 +114,12 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type
$maxMinor = null;

if (
$this->composerMinPhpVersion !== null
&& $this->composerMaxPhpVersion !== null
&& $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId()
$minPhpVersion !== null
&& $maxPhpVersion !== null
&& $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId()
) {
$minMinor = $this->composerMinPhpVersion->getMinorVersionId();
$maxMinor = $this->composerMaxPhpVersion->getMinorVersionId();
$minMinor = $minPhpVersion->getMinorVersionId();
$maxMinor = $maxPhpVersion->getMinorVersionId();
}

return $this->createInteger($minMinor, $maxMinor);
Expand All @@ -116,25 +129,25 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type
$maxRelease = null;

if (
$this->composerMinPhpVersion !== null
&& $this->composerMaxPhpVersion !== null
&& $this->composerMaxPhpVersion->getMajorVersionId() === $this->composerMinPhpVersion->getMajorVersionId()
&& $this->composerMaxPhpVersion->getMinorVersionId() === $this->composerMinPhpVersion->getMinorVersionId()
$minPhpVersion !== null
&& $maxPhpVersion !== null
&& $maxPhpVersion->getMajorVersionId() === $minPhpVersion->getMajorVersionId()
&& $maxPhpVersion->getMinorVersionId() === $minPhpVersion->getMinorVersionId()
) {
$minRelease = $this->composerMinPhpVersion->getPatchVersionId();
$maxRelease = $this->composerMaxPhpVersion->getPatchVersionId();
$minRelease = $minPhpVersion->getPatchVersionId();
$maxRelease = $maxPhpVersion->getPatchVersionId();
}

return $this->createInteger($minRelease, $maxRelease);
}
if ($resolvedConstantName === 'PHP_VERSION_ID') {
$minVersion = 50207;
$maxVersion = null;
if ($this->composerMinPhpVersion !== null) {
$minVersion = max($minVersion, $this->composerMinPhpVersion->getVersionId());
if ($minPhpVersion !== null) {
$minVersion = max($minVersion, $minPhpVersion->getVersionId());
}
if ($this->composerMaxPhpVersion !== null) {
$maxVersion = $this->composerMaxPhpVersion->getVersionId();
if ($maxPhpVersion !== null) {
$maxVersion = $maxPhpVersion->getVersionId();
}

return $this->createInteger($minVersion, $maxVersion);
Expand Down Expand Up @@ -351,6 +364,40 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type
return null;
}

private function getMinPhpVersion(): ?PhpVersion
{
if (is_int($this->phpVersion)) {
return null;
}

if (is_array($this->phpVersion)) {
if ($this->phpVersion['max'] < $this->phpVersion['min']) {
throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.');
}

return new PhpVersion($this->phpVersion['min']);
}

return $this->composerPhpVersionFactory->getMinVersion();
}

private function getMaxPhpVersion(): ?PhpVersion
{
if (is_int($this->phpVersion)) {
return null;
}

if (is_array($this->phpVersion)) {
if ($this->phpVersion['max'] < $this->phpVersion['min']) {
throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.');
}

return new PhpVersion($this->phpVersion['max']);
}

return $this->composerPhpVersionFactory->getMaxVersion();
}

public function resolveConstantType(string $constantName, Type $constantType): Type
{
if ($constantType->isConstantValue()->yes() && in_array($constantName, $this->dynamicConstantNames, true)) {
Expand Down
4 changes: 2 additions & 2 deletions src/Analyser/ConstantResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public function create(): ConstantResolver
return new ConstantResolver(
$this->reflectionProviderProvider,
$this->container->getParameter('dynamicConstantNames'),
$composerFactory->getMinVersion(),
$composerFactory->getMaxVersion(),
$this->container->getParameter('phpVersion'),
$composerFactory,
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Command\IgnoredRegexValidator;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\File\FileExcluder;
use PHPStan\Php\ComposerPhpVersionFactory;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider;
use PHPStan\PhpDoc\TypeNodeResolver;
Expand Down Expand Up @@ -65,7 +66,8 @@ public function loadConfiguration(): void
$reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
ReflectionProviderStaticAccessor::registerInstance($reflectionProvider);
PhpVersionStaticAccessor::registerInstance(new PhpVersion(PHP_VERSION_ID));
$constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, null);
$composerPhpVersionFactory = new ComposerPhpVersionFactory([]);
$constantResolver = new ConstantResolver($reflectionProviderProvider, [], null, $composerPhpVersionFactory);

$phpDocParserConfig = new ParserConfig([]);
$ignoredRegexValidator = new IgnoredRegexValidator(
Expand Down
55 changes: 9 additions & 46 deletions src/Php/ComposerPhpVersionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
namespace PHPStan\Php;

use Composer\Semver\VersionParser;
use Nette\Utils\Json;
use Nette\Utils\JsonException;
use Nette\Utils\Strings;
use PHPStan\File\CouldNotReadFileException;
use PHPStan\File\FileReader;
use PHPStan\ShouldNotHappenException;
use PHPStan\Internal\ComposerHelper;
use function count;
use function end;
use function is_array;
use function is_file;
use function is_int;
use function is_string;
use function min;
use function sprintf;
Expand All @@ -29,11 +22,9 @@ final class ComposerPhpVersionFactory

/**
* @param string[] $composerAutoloaderProjectPaths
* @param int|array{min: int, max: int}|null $phpVersion
*/
public function __construct(
private array $composerAutoloaderProjectPaths,
private int|array|null $phpVersion,
)
{
}
Expand All @@ -42,23 +33,6 @@ private function initializeVersions(): void
{
$this->initialized = true;

$phpVersion = $this->phpVersion;

if (is_int($phpVersion)) {
throw new ShouldNotHappenException();
}

if (is_array($phpVersion)) {
if ($phpVersion['max'] < $phpVersion['min']) {
throw new ShouldNotHappenException('Invalid PHP version range: phpVersion.max should be greater or equal to phpVersion.min.');
}

$this->minVersion = new PhpVersion($phpVersion['min']);
$this->maxVersion = new PhpVersion($phpVersion['max']);

return;
}

// don't limit minVersion... PHPStan can analyze even PHP5
$this->maxVersion = new PhpVersion(PhpVersionFactory::MAX_PHP_VERSION);

Expand Down Expand Up @@ -87,10 +61,6 @@ private function initializeVersions(): void

public function getMinVersion(): ?PhpVersion
{
if (is_int($this->phpVersion)) {
return null;
}

if ($this->initialized === false) {
$this->initializeVersions();
}
Expand All @@ -100,10 +70,6 @@ public function getMinVersion(): ?PhpVersion

public function getMaxVersion(): ?PhpVersion
{
if (is_int($this->phpVersion)) {
return null;
}

if ($this->initialized === false) {
$this->initializeVersions();
}
Expand All @@ -114,21 +80,18 @@ public function getMaxVersion(): ?PhpVersion
private function getComposerRequireVersion(): ?string
{
$composerPhpVersion = null;

if (count($this->composerAutoloaderProjectPaths) > 0) {
$composerJsonPath = end($this->composerAutoloaderProjectPaths) . '/composer.json';
if (is_file($composerJsonPath)) {
try {
$composerJsonContents = FileReader::read($composerJsonPath);
$composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY);
$requiredVersion = $composer['require']['php'] ?? null;
if (is_string($requiredVersion)) {
$composerPhpVersion = $requiredVersion;
}
} catch (CouldNotReadFileException | JsonException) {
// pass
$composer = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths));
if ($composer !== null) {
$requiredVersion = $composer['require']['php'] ?? null;

if (is_string($requiredVersion)) {
$composerPhpVersion = $requiredVersion;
}
}
}

return $composerPhpVersion;
}

Expand Down
4 changes: 3 additions & 1 deletion src/Testing/PHPStanTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\Internal\DirectoryCreatorException;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Parser\Parser;
use PHPStan\Php\ComposerPhpVersionFactory;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDoc\TypeStringResolver;
Expand Down Expand Up @@ -137,7 +138,8 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider
}

$reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
$constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, null);
$composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class);
$constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, null, $composerPhpVersionFactory);

$initializerExprTypeResolver = new InitializerExprTypeResolver(
$constantResolver,
Expand Down
20 changes: 10 additions & 10 deletions tests/PHPStan/Analyser/nsrt/bug-4434.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class HelloWorld
public function testSendEmailToLog(): void
{
foreach ([1] as $emailFile) {
assertType('int<5, max>', PHP_MAJOR_VERSION);
assertType('int<5, max>', \PHP_MAJOR_VERSION);
assertType('int<5, 8>', PHP_MAJOR_VERSION);
assertType('int<5, 8>', \PHP_MAJOR_VERSION);
if (PHP_MAJOR_VERSION === 7) {
assertType('7', PHP_MAJOR_VERSION);
assertType('7', \PHP_MAJOR_VERSION);
} else {
assertType('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION);
assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION);
assertType('8|int<5, 6>', PHP_MAJOR_VERSION);
assertType('8|int<5, 6>', \PHP_MAJOR_VERSION);
}
}
}
Expand All @@ -28,14 +28,14 @@ class HelloWorld2
public function testSendEmailToLog(): void
{
foreach ([1] as $emailFile) {
assertType('int<5, max>', PHP_MAJOR_VERSION);
assertType('int<5, max>', \PHP_MAJOR_VERSION);
assertType('int<5, 8>', PHP_MAJOR_VERSION);
assertType('int<5, 8>', \PHP_MAJOR_VERSION);
if (PHP_MAJOR_VERSION === 100) {
assertType('100', PHP_MAJOR_VERSION);
assertType('100', \PHP_MAJOR_VERSION);
assertType('*NEVER*', PHP_MAJOR_VERSION);
assertType('*NEVER*', \PHP_MAJOR_VERSION);
} else {
assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION);
assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION);
assertType('int<5, 8>', PHP_MAJOR_VERSION);
assertType('int<5, 8>', \PHP_MAJOR_VERSION);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/predefined-constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

// core, https://www.php.net/manual/en/reserved.constants.php
assertType('non-falsy-string', PHP_VERSION);
assertType('int<5, max>', PHP_MAJOR_VERSION);
assertType('int<5, 8>', PHP_MAJOR_VERSION);
assertType('int<0, max>', PHP_MINOR_VERSION);
assertType('int<0, max>', PHP_RELEASE_VERSION);
assertType('int<50207, max>', PHP_VERSION_ID);
assertType('int<50207, 80499>', PHP_VERSION_ID);
assertType('string', PHP_EXTRA_VERSION);
assertType('0|1', PHP_ZTS);
assertType('0|1', PHP_DEBUG);
Expand Down
Loading