Skip to content

Commit 0dadfbb

Browse files
committed
Add phpIntSize parameter, infer 64bit from composer.json under bleeding edge
1 parent 43e9d32 commit 0dadfbb

20 files changed

Lines changed: 321 additions & 20 deletions

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ parameters:
2222
checkDynamicConstantNameValues: true
2323
unusedLabel: true
2424
newOnNonObject: true
25+
composerPhp64Bit: true

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ parameters:
4949
checkDynamicConstantNameValues: false
5050
unusedLabel: false
5151
newOnNonObject: false
52+
composerPhp64Bit: false
5253
fileExtensions:
5354
- php
5455
checkAdvancedIsset: false
@@ -103,6 +104,7 @@ parameters:
103104
minimumNumberOfJobsPerProcess: 2
104105
buffer: 134217728 # 128 MB
105106
loadLimit: 1.0
107+
phpIntSize: null
106108
phpVersion: null
107109
polluteScopeWithLoopInitialAssignments: true
108110
polluteScopeWithAlwaysIterableForeach: true

conf/parametersSchema.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ parametersSchema:
5151
checkDynamicConstantNameValues: bool()
5252
unusedLabel: bool()
5353
newOnNonObject: bool()
54+
composerPhp64Bit: bool()
5455
])
5556
fileExtensions: listOf(string())
5657
checkAdvancedIsset: bool()
@@ -110,6 +111,7 @@ parametersSchema:
110111
buffer: int(),
111112
loadLimit: schema(float(), nullable())
112113
])
114+
phpIntSize: schema(anyOf(8), nullable())
113115
phpVersion: schema(anyOf(
114116
schema(int(), min(70100), max(80599)),
115117
structure([

src/Analyser/ConstantResolver.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node\Name;
66
use PHPStan\DependencyInjection\AutowiredService;
77
use PHPStan\DependencyInjection\Container;
8+
use PHPStan\Php\ConfiguredPhpIntSizeHelper;
89
use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
910
use PHPStan\PhpDoc\TypeStringResolver;
1011
use PHPStan\Reflection\NamespaceAnswerer;
@@ -51,6 +52,7 @@ public function __construct(
5152
private ReflectionProviderProvider $reflectionProviderProvider,
5253
private array $dynamicConstantNames,
5354
private ConfiguredPhpVersionRangeHelper $configuredPhpVersionRangeHelper,
55+
private ConfiguredPhpIntSizeHelper $configuredPhpIntSizeHelper,
5456
private ?Container $container,
5557
)
5658
{
@@ -219,17 +221,30 @@ public function resolvePredefinedConstant(string $resolvedConstantName): ?Type
219221
]);
220222
}
221223
if ($resolvedConstantName === 'PHP_INT_MAX') {
222-
return PHP_INT_SIZE === 8
223-
? new UnionType([new ConstantIntegerType(2147483647), new ConstantIntegerType(9223372036854775807)])
224-
: new ConstantIntegerType(2147483647);
224+
// The 64bit literals below cannot be constructed when PHPStan itself runs on 32bit,
225+
// so they stay inside branches that are never taken there.
226+
if (PHP_INT_SIZE !== 8) {
227+
return new ConstantIntegerType(2147483647);
228+
}
229+
if ($this->configuredPhpIntSizeHelper->getIntSize() === 8) {
230+
return new ConstantIntegerType(9223372036854775807);
231+
}
232+
return new UnionType([new ConstantIntegerType(2147483647), new ConstantIntegerType(9223372036854775807)]);
225233
}
226234
if ($resolvedConstantName === 'PHP_INT_MIN') {
227235
// Why the -1 you might wonder, the answer is to fit it into an int :/ see https://3v4l.org/4SHIQ
228-
return PHP_INT_SIZE === 8
229-
? new UnionType([new ConstantIntegerType(-9223372036854775807 - 1), new ConstantIntegerType(-2147483647 - 1)])
230-
: new ConstantIntegerType(-2147483647 - 1);
236+
if (PHP_INT_SIZE !== 8) {
237+
return new ConstantIntegerType(-2147483647 - 1);
238+
}
239+
if ($this->configuredPhpIntSizeHelper->getIntSize() === 8) {
240+
return new ConstantIntegerType(-9223372036854775807 - 1);
241+
}
242+
return new UnionType([new ConstantIntegerType(-9223372036854775807 - 1), new ConstantIntegerType(-2147483647 - 1)]);
231243
}
232244
if ($resolvedConstantName === 'PHP_INT_SIZE') {
245+
if ($this->configuredPhpIntSizeHelper->getIntSize() === 8) {
246+
return new ConstantIntegerType(8);
247+
}
233248
return new UnionType([
234249
new ConstantIntegerType(4),
235250
new ConstantIntegerType(8),

src/Analyser/ConstantResolverFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\DependencyInjection\AutowiredService;
66
use PHPStan\DependencyInjection\Container;
77
use PHPStan\Php\ComposerPhpVersionFactory;
8+
use PHPStan\Php\ConfiguredPhpIntSizeHelper;
89
use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
910
use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
1011

@@ -30,6 +31,7 @@ public function create(): ConstantResolver
3031
$this->container->getParameter('phpVersion'),
3132
$composerFactory,
3233
),
34+
$this->container->getByType(ConfiguredPhpIntSizeHelper::class),
3335
$this->container,
3436
);
3537
}

src/DependencyInjection/ValidateIgnoredErrorsExtension.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\DependencyInjection\Type\UnaryOperatorTypeSpecifyingExtensionRegistryProvider;
1616
use PHPStan\File\FileExcluder;
1717
use PHPStan\Php\ComposerPhpVersionFactory;
18+
use PHPStan\Php\ConfiguredPhpIntSizeHelper;
1819
use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
1920
use PHPStan\Php\PhpVersion;
2021
use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider;
@@ -91,7 +92,7 @@ public function loadConfiguration(): void
9192

9293
try {
9394
$composerPhpVersionFactory = new ComposerPhpVersionFactory([]);
94-
$constantResolver = new ConstantResolver($reflectionProviderProvider, [], new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), container: null);
95+
$constantResolver = new ConstantResolver($reflectionProviderProvider, [], new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), new ConfiguredPhpIntSizeHelper(null, false, []), container: null);
9596

9697
$phpDocParserConfig = new ParserConfig([]);
9798
$ignoredRegexValidator = new IgnoredRegexValidator(

src/Php/ComposerPhpVersionFactory.php

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ final class ComposerPhpVersionFactory
2626
public function __construct(
2727
#[AutowiredParameter]
2828
private array $composerAutoloaderProjectPaths,
29+
#[AutowiredParameter(ref: '%featureToggles.composerPhp64Bit%')]
30+
private bool $composerPhp64Bit = false,
2931
)
3032
{
3133
}
@@ -37,14 +39,14 @@ private function initializeVersions(): void
3739
// don't limit minVersion... PHPStan can analyze even PHP5
3840
$this->maxVersion = new PhpVersion(PhpVersionFactory::MAX_PHP_VERSION);
3941

40-
// fallback to composer.json based php-version constraint
41-
$composerPhpVersion = $this->getComposerRequireVersion();
42-
if ($composerPhpVersion === null) {
42+
// fallback to composer.json based php-version constraints
43+
$composerPhpVersions = $this->getComposerRequireVersions();
44+
if (count($composerPhpVersions) === 0) {
4345
return;
4446
}
4547

4648
$parser = new ComposerPhpVersionParser();
47-
[$minVersion, $maxVersion] = $parser->parse($composerPhpVersion, static function (string $version, int $versionId, bool $isMaxVersion): PhpVersion {
49+
[$minVersion, $maxVersion] = $parser->parse($composerPhpVersions, static function (string $version, int $versionId, bool $isMaxVersion): PhpVersion {
4850
if ($isMaxVersion && $version === '6.0.0.0-dev') {
4951
$versionId = min($versionId, PhpVersionFactory::MAX_PHP5_VERSION);
5052
} elseif ($isMaxVersion && $version === '8.0.0.0-dev') {
@@ -83,22 +85,34 @@ public function getMaxVersion(): ?PhpVersion
8385
return $this->maxVersion;
8486
}
8587

86-
private function getComposerRequireVersion(): ?string
88+
/**
89+
* Composer registers php-64bit as a virtual package carrying the very same version as php,
90+
* so a requirement on either one constrains the PHP version, and requiring both means
91+
* both constraints have to hold at once.
92+
*
93+
* @return list<string>
94+
*/
95+
private function getComposerRequireVersions(): array
8796
{
88-
$composerPhpVersion = null;
97+
$packageNames = $this->composerPhp64Bit ? ['php', 'php-64bit'] : ['php'];
98+
$composerPhpVersions = [];
8999

90100
if (count($this->composerAutoloaderProjectPaths) > 0) {
91101
$composer = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths));
92102
if ($composer !== null) {
93-
$requiredVersion = $composer['require']['php'] ?? null;
103+
foreach ($packageNames as $packageName) {
104+
$requiredVersion = $composer['require'][$packageName] ?? null;
105+
106+
if (!is_string($requiredVersion)) {
107+
continue;
108+
}
94109

95-
if (is_string($requiredVersion)) {
96-
$composerPhpVersion = $requiredVersion;
110+
$composerPhpVersions[] = $requiredVersion;
97111
}
98112
}
99113
}
100114

101-
return $composerPhpVersion;
115+
return $composerPhpVersions;
102116
}
103117

104118
}

src/Php/ComposerPhpVersionParser.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,31 @@
22

33
namespace PHPStan\Php;
44

5+
use Composer\Semver\Constraint\MultiConstraint;
56
use Composer\Semver\VersionParser;
67
use Nette\Utils\Strings;
8+
use function array_map;
79
use function sprintf;
810

911
final class ComposerPhpVersionParser
1012
{
1113

1214
/**
15+
* @param non-empty-list<string> $versions constraints that all have to be satisfied at once,
16+
* like Composer's `php` and `php-64bit` requirements
1317
* @param callable(string, int, bool):PhpVersion $buildPhpVersion
1418
*
1519
* @return array{PhpVersion|null, PhpVersion|null}
1620
*/
17-
public function parse(string $version, callable $buildPhpVersion): array
21+
public function parse(array $versions, callable $buildPhpVersion): array
1822
{
1923
$minVersion = null;
2024

2125
$parser = new VersionParser();
22-
$constraint = $parser->parseConstraints($version);
26+
$constraint = MultiConstraint::create(
27+
array_map(static fn (string $version) => $parser->parseConstraints($version), $versions),
28+
true,
29+
);
2330

2431
if (!$constraint->getLowerBound()->isZero()) {
2532
$minVersion = $this->buildVersion($constraint->getLowerBound()->getVersion(), false, $buildPhpVersion);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Php;
4+
5+
use PHPStan\DependencyInjection\AutowiredParameter;
6+
use PHPStan\DependencyInjection\AutowiredService;
7+
use PHPStan\Internal\ComposerHelper;
8+
use function count;
9+
use function end;
10+
use function is_string;
11+
12+
/**
13+
* Tells whether the analysed code is guaranteed to run on a 64bit platform.
14+
*
15+
* The source is either the NEON config phpIntSize value, or a `php-64bit` requirement
16+
* in the project's composer.json. Composer only provides the `php-64bit` virtual package
17+
* when PHP_INT_SIZE === 8, so requiring it makes the assumption enforceable at install time.
18+
*
19+
* Reading composer.json turns the assumption on for projects that never asked for it,
20+
* so it only happens under the composerPhp64Bit feature toggle. Setting phpIntSize is
21+
* explicit and always wins.
22+
*/
23+
#[AutowiredService]
24+
final class ConfiguredPhpIntSizeHelper
25+
{
26+
27+
/** The only int size that can be assumed. 32bit semantics are not modelled anywhere else. */
28+
private const SUPPORTED_INT_SIZE = 8;
29+
30+
/** @var self::SUPPORTED_INT_SIZE|null */
31+
private ?int $intSize = null;
32+
33+
private bool $initialized = false;
34+
35+
/**
36+
* @param self::SUPPORTED_INT_SIZE|null $configPhpIntSize
37+
* @param string[] $composerAutoloaderProjectPaths
38+
*/
39+
public function __construct(
40+
#[AutowiredParameter(ref: '%phpIntSize%')]
41+
private ?int $configPhpIntSize,
42+
#[AutowiredParameter(ref: '%featureToggles.composerPhp64Bit%')]
43+
private bool $composerPhp64Bit,
44+
#[AutowiredParameter]
45+
private array $composerAutoloaderProjectPaths,
46+
)
47+
{
48+
}
49+
50+
/**
51+
* Size of an integer in bytes on the analysed platform,
52+
* or null when both 32bit and 64bit have to be taken into account.
53+
*
54+
* @return self::SUPPORTED_INT_SIZE|null
55+
*/
56+
public function getIntSize(): ?int
57+
{
58+
if (!$this->initialized) {
59+
$this->initialized = true;
60+
$this->intSize = $this->configPhpIntSize ?? ($this->composerRequiresPhp64Bit() ? self::SUPPORTED_INT_SIZE : null);
61+
}
62+
63+
return $this->intSize;
64+
}
65+
66+
private function composerRequiresPhp64Bit(): bool
67+
{
68+
if (!$this->composerPhp64Bit) {
69+
return false;
70+
}
71+
72+
if (count($this->composerAutoloaderProjectPaths) === 0) {
73+
return false;
74+
}
75+
76+
$composer = ComposerHelper::getComposerConfig(end($this->composerAutoloaderProjectPaths));
77+
if ($composer === null) {
78+
return false;
79+
}
80+
81+
return is_string($composer['require']['php-64bit'] ?? null);
82+
}
83+
84+
}

src/Testing/PHPStanTestCase.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\Node\Printer\ExprPrinter;
1717
use PHPStan\Parser\Parser;
1818
use PHPStan\Php\ComposerPhpVersionFactory;
19+
use PHPStan\Php\ConfiguredPhpIntSizeHelper;
1920
use PHPStan\Php\ConfiguredPhpVersionRangeHelper;
2021
use PHPStan\Php\PhpVersion;
2122
use PHPStan\PhpDoc\TypeNodeResolver;
@@ -96,7 +97,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider
9697

9798
$reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider);
9899
$composerPhpVersionFactory = $container->getByType(ComposerPhpVersionFactory::class);
99-
$constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), container: $container);
100+
$constantResolver = new ConstantResolver($reflectionProviderProvider, $dynamicConstantNames, new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), $container->getByType(ConfiguredPhpIntSizeHelper::class), container: $container);
100101

101102
$initializerExprTypeResolver = new InitializerExprTypeResolver(
102103
$constantResolver,

0 commit comments

Comments
 (0)