diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 29d608b..8da1849 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -16,7 +16,9 @@ jobs: strategy: matrix: php: + - '7.4' - '8.3' + - '8.4' steps: - uses: 'actions/checkout@v2' - uses: 'shivammathur/setup-php@v2' @@ -28,5 +30,5 @@ jobs: # Require PHPStan via command-line instead of adding to Composer's # "require-dev"; we only want to run static analysis once on the highest # version of PHP available. - - run: 'composer require --dev phpstan/phpstan phpstan/phpstan-deprecation-rules' + - run: 'composer require --dev "phpstan/phpstan:^2" "phpstan/phpstan-deprecation-rules"' - run: './vendor/bin/phpstan analyze --no-progress --error-format="github"' diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index a435bb7..283c929 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -26,12 +26,13 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' steps: - uses: 'actions/checkout@v2' - uses: 'shivammathur/setup-php@v2' with: php-version: '${{ matrix.php }}' - - run: 'find src/ -type f -name "*.php" -print0 | xargs -0 -n1 -P4 php -l -n | (! grep -v "No syntax errors detected" )' + - run: 'find src/ -type f -name "*.php" -print0 | xargs -0 -n1 -P4 php -d"error_reporting=E_ALL&~E_DEPRECATED" -l -n | (! grep -v "No syntax errors detected" )' unit-testing: runs-on: 'ubuntu-20.04' @@ -48,6 +49,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' steps: - uses: 'actions/checkout@v2' - uses: 'shivammathur/setup-php@v2' diff --git a/README.md b/README.md index 01e23e5..6181f61 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Full documentation is available in the [`docs/`](docs/) folder. ## Compatibility This library has extensive test coverage using PHPUnit on PHP versions: `5.6`, -`7.0`, `7.1`, `7.2`, `7.3`, `7.4`, `8.0`, `8.1`, `8.2`, and `8.3`. +`7.0`, `7.1`, `7.2`, `7.3`, `7.4`, `8.0`, `8.1`, `8.2`, `8.3` and `8.4`. -Static analysis is performed with PHPStan at `max` level on PHP `8.3`, using +Static analysis is performed with PHPStan at `max` level on PHP `8.4`, using core, bleeding edge, and deprecation rules. > The Doctrine features for this library have been split off into their own diff --git a/phpstan.neon b/phpstan.neon index 8d0f28f..2e9f9ad 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,7 +2,6 @@ parameters: level: 'max' paths: [ 'src', 'tests' ] checkFunctionNameCase: true - checkGenericClassInNonGenericObjectType: true reportUnmatchedIgnoredErrors: true treatPhpDocTypesAsCertain: false parallel: @@ -10,7 +9,12 @@ parameters: ignoreErrors: - # This project purposefully uses variable constructors and "new static()". - message: "#^Unsafe usage of new static\\(\\)\\.$#" + identifier: new.static + - + # We cannot fix PHP 8.4 deprecation errors without removing support for PHP versions <7.1 + # (but it means this error won't be matched on PHP versoins <8.4) + identifier: parameter.implicitlyNullable + reportUnmatched: false includes: - 'vendor/phpstan/phpstan-deprecation-rules/rules.neon' diff --git a/src/Formatter/ConsistentFormatter.php b/src/Formatter/ConsistentFormatter.php index f6d071e..1259131 100644 --- a/src/Formatter/ConsistentFormatter.php +++ b/src/Formatter/ConsistentFormatter.php @@ -73,7 +73,7 @@ private function ntopVersion4($binary) // $pack return type is `string|false` below PHP 8 and `string` // above PHP 8. $pack = \pack('A4', $binary); - /** @phpstan-ignore-next-line (@phpstan-ignore identical.alwaysFalse) */ + // @phpstan-ignore identical.alwaysFalse if (false === $pack || false === $protocol = \inet_ntop($pack)) { throw new FormatException($binary); } diff --git a/src/Formatter/NativeFormatter.php b/src/Formatter/NativeFormatter.php index 41afd54..c065431 100644 --- a/src/Formatter/NativeFormatter.php +++ b/src/Formatter/NativeFormatter.php @@ -18,7 +18,7 @@ public function ntop($binary) $pack = \pack(\sprintf('A%d', $length), $binary); // $pack return type is `string|false` below PHP 8 and `string` // above PHP 8. - /** @phpstan-ignore-next-line (@phpstan-ignore identical.alwaysFalse) */ + // @phpstan-ignore identical.alwaysFalse if (false === $pack || false === $protocol = \inet_ntop($pack)) { throw new FormatException($binary); } @@ -36,17 +36,23 @@ public function pton($binary) if (\is_string($binary)) { if (\filter_var($binary, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { $number = \inet_pton($binary); - if (false === $number || false === $sequence = \unpack('a4', $number)) { + if (false === $number + || false === ($sequence = \unpack('a4', $number)) + || !is_string($return = \current($sequence)) + ) { throw new FormatException($binary); } - return \current($sequence); + return $return; } if (\filter_var($binary, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $number = \inet_pton($binary); - if (false === $number || false === $sequence = \unpack('a16', $number)) { + if (false === $number + || false === ($sequence = \unpack('a16', $number)) + || !is_string($return = \current($sequence)) + ) { throw new FormatException($binary); } - return \current($sequence); + return $return; } $length = MbString::getLength($binary); if ($length === 4 || $length === 16) { diff --git a/src/Util/Binary.php b/src/Util/Binary.php index 7668ea7..b479a23 100644 --- a/src/Util/Binary.php +++ b/src/Util/Binary.php @@ -27,10 +27,10 @@ public static function toHex($binary) if (!\is_string($binary)) { throw new \InvalidArgumentException('Cannot convert non-string to hexadecimal.'); } - if (false === $data = \unpack('H*', $binary)) { + if (false === ($data = \unpack('H*', $binary)) || !is_string($hex = \reset($data))) { throw new \InvalidArgumentException('Unknown error converting string to hexadecimal.'); } - return \reset($data); + return $hex; } /** diff --git a/src/Util/MbString.php b/src/Util/MbString.php index 03aab39..a7f46ec 100644 --- a/src/Util/MbString.php +++ b/src/Util/MbString.php @@ -23,9 +23,14 @@ public static function getLength($str) */ public static function subString($str, $start, $length = null) { - return \function_exists('\\mb_substr') - ? (\mb_substr($str, $start, $length, '8bit') ?: '') - : (\substr($str, $start, $length) ?: ''); + if (\function_exists('\\mb_substr')) { + return (\mb_substr($str, $start, $length, '8bit') ?: ''); + } + return is_int($length) + ? (\substr($str, $start, $length) ?: '') + // On PHP versions 7.2 to 7.4, the $length argument cannot be null. + // The official PHP docs do not mention this peculiarity. + : (\substr($str, $start) ?: ''); } /** diff --git a/tests/Version/IPv4Test.php b/tests/Version/IPv4Test.php index eb59681..902c8b1 100644 --- a/tests/Version/IPv4Test.php +++ b/tests/Version/IPv4Test.php @@ -73,7 +73,7 @@ public function testExceptionIsThrownOnInstantiationWithInvalidAddresses($value) $this->expectException(\Darsyn\IP\Exception\InvalidIpAddressException::class); $this->expectExceptionMessage('The IP address supplied is not valid.'); try { - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type IP::factory($value); } catch (InvalidIpAddressException $e) { $this->assertSame($value, $e->getSuppliedIp()); @@ -197,7 +197,7 @@ public function testCidrMasks($cidr, $expectedMaskHex) $reflect = new \ReflectionClass($ip); $method = $reflect->getMethod('generateBinaryMask'); $method->setAccessible(true); - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type $actualMask = unpack('H*hex', $method->invoke($ip, $cidr, 4)); $this->assertSame($expectedMaskHex, is_array($actualMask) ? $actualMask['hex'] : null); } @@ -220,7 +220,7 @@ public function testExceptionIsThrownFromInvalidCidrValues($cidr) $method->setAccessible(true); try { $method->invoke($ip, $cidr, 4); - /** @phpstan-ignore-next-line (@phpstan-ignore catch.neverThrown) */ + // @phpstan-ignore catch.neverThrown } catch (InvalidCidrException $e) { $this->assertSame($cidr, $e->getSuppliedCidr()); throw $e; @@ -288,7 +288,7 @@ public function testInRangeThrowsExceptionOnInvalidCidr($cidr) $first = IP::factory('12.34.56.78'); $second = IP::factory('12.34.56.78'); $this->expectException(InvalidCidrException::class); - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type $first->inRange($second, $cidr); } diff --git a/tests/Version/IPv6Test.php b/tests/Version/IPv6Test.php index e2aee60..807cd0c 100644 --- a/tests/Version/IPv6Test.php +++ b/tests/Version/IPv6Test.php @@ -76,7 +76,7 @@ public function testExceptionIsThrownOnInstantiationWithInvalidAddresses($value) $this->expectException(\Darsyn\IP\Exception\InvalidIpAddressException::class); $this->expectExceptionMessage('The IP address supplied is not valid.'); try { - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type $ip = IP::factory($value); } catch (InvalidIpAddressException $e) { $this->assertSame($value, $e->getSuppliedIp()); @@ -246,7 +246,7 @@ public function testCidrMasks($cidr, $expectedMaskHex) $reflect = new \ReflectionClass($ip); $method = $reflect->getMethod('generateBinaryMask'); $method->setAccessible(true); - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type $actualMask = unpack('H*hex', $method->invoke($ip, $cidr, 16)); $this->assertSame($expectedMaskHex, is_array($actualMask) ? $actualMask['hex'] : null); } @@ -269,7 +269,7 @@ public function testExceptionIsThrownFromInvalidCidrValues($cidr) $method->setAccessible(true); try { $method->invoke($ip, $cidr, 16); - /** @phpstan-ignore-next-line (@phpstan-ignore catch.neverThrown) */ + // @phpstan-ignore catch.neverThrown } catch (InvalidCidrException $e) { $this->assertSame($cidr, $e->getSuppliedCidr()); throw $e; diff --git a/tests/Version/MultiTest.php b/tests/Version/MultiTest.php index ee2074e..efe5b72 100644 --- a/tests/Version/MultiTest.php +++ b/tests/Version/MultiTest.php @@ -121,7 +121,7 @@ public function testExceptionIsThrownOnInstantiationWithInvalidAddresses($value) $this->expectException(InvalidIpAddressException::class); $this->expectExceptionMessage('The IP address supplied is not valid.'); try { - /** @phpstan-ignore-next-line (@phpstan-ignore argument.type) */ + // @phpstan-ignore argument.type $ip = IP::factory($value); } catch (InvalidIpAddressException $e) { $this->assertSame($value, $e->getSuppliedIp());