diff --git a/build/baseline-32bit.neon b/build/baseline-32bit.neon new file mode 100644 index 0000000000..82adbae209 --- /dev/null +++ b/build/baseline-32bit.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$value of class PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType constructor expects int, float given\\.$#" + count: 2 + path: ../src/Analyser/MutatingScope.php diff --git a/build/ignore-by-architecture.neon.php b/build/ignore-by-architecture.neon.php new file mode 100644 index 0000000000..a6c86b46cf --- /dev/null +++ b/build/ignore-by-architecture.neon.php @@ -0,0 +1,11 @@ +load(__DIR__ . '/baseline-32bit.neon'); +} + +return []; diff --git a/build/phpstan.neon b/build/phpstan.neon index a233ed286d..aea35d06cc 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -8,6 +8,7 @@ includes: - ../conf/bleedingEdge.neon - ../phpstan-baseline.neon - ignore-by-php-version.neon.php + - ignore-by-architecture.neon.php parameters: level: 8 paths: diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5da3325cf8..319f2c81b9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -135,6 +135,7 @@ use function usort; use const PHP_INT_MAX; use const PHP_INT_MIN; +use const PHP_INT_SIZE; class MutatingScope implements Scope { @@ -2008,6 +2009,182 @@ private function resolveType(Expr $node): Type if ($this->reflectionProvider->hasConstant($node->name, $this)) { /** @var string $resolvedConstantName */ $resolvedConstantName = $this->reflectionProvider->resolveConstantName($node->name, $this); + // core, https://www.php.net/manual/en/reserved.constants.php + if ($resolvedConstantName === 'PHP_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { + return IntegerRangeType::fromInterval(5, null); + } + if ($resolvedConstantName === 'PHP_MINOR_VERSION') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_RELEASE_VERSION') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_VERSION_ID') { + return IntegerRangeType::fromInterval(50207, null); + } + if ($resolvedConstantName === 'PHP_ZTS') { + return new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]); + } + if ($resolvedConstantName === 'PHP_DEBUG') { + return new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]); + } + if ($resolvedConstantName === 'PHP_MAXPATHLEN') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === 'PHP_OS') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_OS_FAMILY') { + return new UnionType([ + new ConstantStringType('Windows'), + new ConstantStringType('BSD'), + new ConstantStringType('Darwin'), + new ConstantStringType('Solaris'), + new ConstantStringType('Linux'), + new ConstantStringType('Unknown'), + ]); + } + if ($resolvedConstantName === 'PHP_SAPI') { + return new UnionType([ + new ConstantStringType('apache'), + new ConstantStringType('apache2handler'), + new ConstantStringType('cgi'), + new ConstantStringType('cli'), + new ConstantStringType('cli-server'), + new ConstantStringType('embed'), + new ConstantStringType('fpm-fcgi'), + new ConstantStringType('litespeed'), + new ConstantStringType('phpdbg'), + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), + ]); + } + if ($resolvedConstantName === 'PHP_EOL') { + return new UnionType([ + new ConstantStringType("\n"), + new ConstantStringType("\r\n"), + ]); + } + if ($resolvedConstantName === 'PHP_INT_MAX') { + return PHP_INT_SIZE === 8 + ? new UnionType([new ConstantIntegerType(2147483647), new ConstantIntegerType(9223372036854775807)]) + : new ConstantIntegerType(2147483647); + } + if ($resolvedConstantName === 'PHP_INT_MIN') { + // Why the -1 you might wonder, the answer is to fit it into an int :/ see https://3v4l.org/4SHIQ + return PHP_INT_SIZE === 8 + ? new UnionType([new ConstantIntegerType(-9223372036854775807 - 1), new ConstantIntegerType(-2147483647 - 1)]) + : new ConstantIntegerType(-2147483647 - 1); + } + if ($resolvedConstantName === 'PHP_INT_SIZE') { + return new UnionType([ + new ConstantIntegerType(4), + new ConstantIntegerType(8), + ]); + } + if ($resolvedConstantName === 'PHP_FLOAT_DIG') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === 'PHP_EXTENSION_DIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_PREFIX') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINARY') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_MANDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LIBDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_DATADIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_SYSCONFDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LOCALSTATEDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_CONFIG_FILE_PATH') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_SHLIB_SUFFIX') { + return new UnionType([ + new ConstantStringType('so'), + new ConstantStringType('dll'), + ]); + } + if ($resolvedConstantName === 'PHP_FD_SETSIZE') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { + return IntegerRangeType::fromInterval(0, null); + } + // core other, https://www.php.net/manual/en/info.constants.php + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_MAJOR') { + return IntegerRangeType::fromInterval(4, null); + } + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_MINOR') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_BUILD') { + return IntegerRangeType::fromInterval(1, null); + } + // dir, https://www.php.net/manual/en/dir.constants.php if ($resolvedConstantName === 'DIRECTORY_SEPARATOR') { return new UnionType([ new ConstantStringType('/'), @@ -2020,16 +2197,25 @@ private function resolveType(Expr $node): Type new ConstantStringType(';'), ]); } - if ($resolvedConstantName === 'PHP_EOL') { - return new UnionType([ - new ConstantStringType("\n"), - new ConstantStringType("\r\n"), + // iconv, https://www.php.net/manual/en/iconv.constants.php + if ($resolvedConstantName === 'ICONV_IMPL') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), ]); } - if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { - return new IntegerType(); + // libxml, https://www.php.net/manual/en/libxml.constants.php + if ($resolvedConstantName === 'LIBXML_VERSION') { + return IntegerRangeType::fromInterval(1, null); } - if ($resolvedConstantName === 'PHP_INT_MAX') { + if ($resolvedConstantName === 'LIBXML_DOTTED_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + // openssl, https://www.php.net/manual/en/openssl.constants.php + if ($resolvedConstantName === 'OPENSSL_VERSION_NUMBER') { return IntegerRangeType::fromInterval(1, null); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 3325578f77..d7db5d136a 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -6,6 +6,7 @@ use stdClass; use function define; use function extension_loaded; +use const PHP_INT_SIZE; use const PHP_VERSION_ID; class NodeScopeResolverTest extends TypeInferenceTestCase @@ -34,7 +35,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/date.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-key.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/intersection-static.php'); @@ -512,7 +515,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php'); + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php'); @@ -598,6 +603,19 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6293.php'); } + if (PHP_VERSION_ID >= 70200) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php72.php'); + } + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php74.php'); + } + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-64bit.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-32bit.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs-phpstanPropertyPrefix.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-destructuring-types.php'); diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 3107168fbe..4f9e0a4ce4 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -245,7 +245,7 @@ public function testGetConstantType(): void $scope = $scopeFactory->create(ScopeContext::create(__DIR__ . '/data/compiler-halt-offset.php')); $node = new ConstFetch(new FullyQualified('__COMPILER_HALT_OFFSET__')); $type = $scope->getType($node); - $this->assertSame('int', $type->describe(VerbosityLevel::precise())); + $this->assertSame('int<0, max>', $type->describe(VerbosityLevel::precise())); } } diff --git a/tests/PHPStan/Analyser/data/bug-4434.php b/tests/PHPStan/Analyser/data/bug-4434.php index b60b751ed1..7e0b97d7fb 100644 --- a/tests/PHPStan/Analyser/data/bug-4434.php +++ b/tests/PHPStan/Analyser/data/bug-4434.php @@ -10,14 +10,14 @@ class HelloWorld public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('int<5, max>', PHP_MAJOR_VERSION); + assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 7) { assertType('int', PHP_MAJOR_VERSION); assertType('int', \PHP_MAJOR_VERSION); } else { - assertType('int|int<8, max>', PHP_MAJOR_VERSION); - assertType('int|int<8, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION); + assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); } } } @@ -28,14 +28,14 @@ class HelloWorld2 public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('int<5, max>', PHP_MAJOR_VERSION); + assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 100) { assertType('int', PHP_MAJOR_VERSION); assertType('int', \PHP_MAJOR_VERSION); } else { - assertType('int|int<101, max>', PHP_MAJOR_VERSION); - assertType('int|int<101, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION); + assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION); } } } diff --git a/tests/PHPStan/Analyser/data/bug-5072.php b/tests/PHPStan/Analyser/data/bug-5072.php index 0de9da5e91..25f273346b 100644 --- a/tests/PHPStan/Analyser/data/bug-5072.php +++ b/tests/PHPStan/Analyser/data/bug-5072.php @@ -24,6 +24,6 @@ public function incorrect(array $params): void public function incorrectWithConstant(): void { - assertType('int<1, max>', max(1, PHP_INT_MAX)); + assertType('2147483647|9223372036854775807', max(1, PHP_INT_MAX)); } } diff --git a/tests/PHPStan/Analyser/data/predefined-constants-32bit.php b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php new file mode 100644 index 0000000000..8d0fe24eaf --- /dev/null +++ b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php @@ -0,0 +1,7 @@ +', PHP_FLOAT_DIG); +assertType('float', PHP_FLOAT_EPSILON); +assertType('float', PHP_FLOAT_MIN); +assertType('float', PHP_FLOAT_MAX); +assertType('int<1, max>', PHP_FD_SETSIZE); diff --git a/tests/PHPStan/Analyser/data/predefined-constants-php74.php b/tests/PHPStan/Analyser/data/predefined-constants-php74.php new file mode 100644 index 0000000000..e0b7f0d156 --- /dev/null +++ b/tests/PHPStan/Analyser/data/predefined-constants-php74.php @@ -0,0 +1,7 @@ +', 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('string', PHP_EXTRA_VERSION); +assertType('0|1', PHP_ZTS); +assertType('0|1', PHP_DEBUG); +assertType('int<1, max>', PHP_MAXPATHLEN); +assertType('non-empty-string', PHP_OS); +assertType('\'apache\'|\'apache2handler\'|\'cgi\'|\'cli\'|\'cli-server\'|\'embed\'|\'fpm-fcgi\'|\'litespeed\'|\'phpdbg\'|non-empty-string', PHP_SAPI); +assertType("'\n'|'\r\n'", PHP_EOL); +assertType('4|8', PHP_INT_SIZE); +assertType('string', DEFAULT_INCLUDE_PATH); +assertType('string', PEAR_INSTALL_DIR); +assertType('string', PEAR_EXTENSION_DIR); +assertType('non-empty-string', PHP_EXTENSION_DIR); +assertType('non-empty-string', PHP_PREFIX); +assertType('non-empty-string', PHP_BINDIR); +assertType('non-empty-string', PHP_BINARY); +assertType('non-empty-string', PHP_MANDIR); +assertType('non-empty-string', PHP_LIBDIR); +assertType('non-empty-string', PHP_DATADIR); +assertType('non-empty-string', PHP_SYSCONFDIR); +assertType('non-empty-string', PHP_LOCALSTATEDIR); +assertType('non-empty-string', PHP_CONFIG_FILE_PATH); +assertType('string', PHP_CONFIG_FILE_SCAN_DIR); +assertType('\'dll\'|\'so\'', PHP_SHLIB_SUFFIX); +assertType('1', E_ERROR); +assertType('2', E_WARNING); +assertType('4', E_PARSE); +assertType('8', E_NOTICE); +assertType('16', E_CORE_ERROR); +assertType('32', E_CORE_WARNING); +assertType('64', E_COMPILE_ERROR); +assertType('128', E_COMPILE_WARNING); +assertType('256', E_USER_ERROR); +assertType('512', E_USER_WARNING); +assertType('1024', E_USER_NOTICE); +assertType('4096', E_RECOVERABLE_ERROR); +assertType('8192', E_DEPRECATED); +assertType('16384', E_USER_DEPRECATED); +assertType('32767', E_ALL); +assertType('2048', E_STRICT); +assertType('int<0, max>', __COMPILER_HALT_OFFSET__); +assertType('true', true); +assertType('false', false); +assertType('null', null); + +// core other, https://www.php.net/manual/en/info.constants.php +assertType('int<4, max>', PHP_WINDOWS_VERSION_MAJOR); +assertType('int<0, max>', PHP_WINDOWS_VERSION_MINOR); +assertType('int<1, max>', PHP_WINDOWS_VERSION_BUILD); + +// dir, https://www.php.net/manual/en/dir.constants.php +assertType('\'/\'|\'\\\\\'', DIRECTORY_SEPARATOR); +assertType('\':\'|\';\'', PATH_SEPARATOR); + +// iconv, https://www.php.net/manual/en/iconv.constants.php +assertType('non-empty-string', ICONV_IMPL); + +// libxml, https://www.php.net/manual/en/libxml.constants.php +assertType('int<1, max>', LIBXML_VERSION); +assertType('non-empty-string', LIBXML_DOTTED_VERSION); + +// openssl, https://www.php.net/manual/en/openssl.constants.php +assertType('int<1, max>', OPENSSL_VERSION_NUMBER); + +// other +assertType('bool', ZEND_DEBUG_BUILD); +assertType('bool', ZEND_THREAD_SAFE); diff --git a/tests/PHPStan/Analyser/data/random-int.php b/tests/PHPStan/Analyser/data/random-int.php index 4f501352d3..4e2df9ef57 100644 --- a/tests/PHPStan/Analyser/data/random-int.php +++ b/tests/PHPStan/Analyser/data/random-int.php @@ -22,9 +22,9 @@ function (int $i) { }; assertType('0', random_int(0, 0)); -assertType('int', random_int(PHP_INT_MIN, PHP_INT_MAX)); -assertType('int<0, max>', random_int(0, PHP_INT_MAX)); -assertType('int', random_int(PHP_INT_MIN, 0)); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(PHP_INT_MIN, PHP_INT_MAX)); +assertType('int<0, 9223372036854775807>', random_int(0, PHP_INT_MAX)); +assertType('int<-9223372036854775808, 0>', random_int(PHP_INT_MIN, 0)); assertType('int<-1, 1>', random_int(-1, 1)); assertType('int<0, 30>', random_int(0, random_int(0, 30))); assertType('int<0, 100>', random_int(random_int(0, 10), 100)); @@ -36,7 +36,7 @@ function (int $i) { assertType('int<0, 1>', random_int(random_int(0, 1), 1)); assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5))); -assertType('int', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); assertType('int<-5, 5>', rand(-5, 5)); assertType('int<0, max>', rand()); diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php index 129ece4297..4b9ab4e23c 100644 --- a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_INT_SIZE; /** * @extends RuleTestCase @@ -18,7 +19,7 @@ protected function getRule(): Rule public function testFile(): void { - $this->analyse([__DIR__ . '/data/random-int.php'], [ + $expectedErrors = [ [ 'Parameter #1 $min (1) of function random_int expects lower number than parameter #2 $max (0).', 8, @@ -55,7 +56,16 @@ public function testFile(): void 'Parameter #1 $min (int<0, 10>) of function random_int expects lower number than parameter #2 $max (int<0, 10>).', 31, ], - ]); + ]; + if (PHP_INT_SIZE === 4) { + // TODO: should fail on 64-bit in a similar fashion, guess it does not because of the union type + $expectedErrors[] = [ + 'Parameter #1 $min (2147483647) of function random_int expects lower number than parameter #2 $max (-2147483648).', + 33, + ]; + } + + $this->analyse([__DIR__ . '/data/random-int.php'], $expectedErrors); } public function testBug6361(): void