Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ jobs:
coverage: none
tools: none

- name: Remove PHPStan for incompatible PHP versions
if: ${{ matrix.php < '8.0' }}
run: composer remove --dev phpstan/phpstan phpstan/phpstan-phpunit

# Install dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-composer-dependencies
- name: "Install Composer dependencies"
Expand All @@ -82,7 +86,9 @@ jobs:
- name: Run unit tests
run: composer test

# It would be sufficient to run PHPStan with one supported PHP version
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Older PHP versions have quirks that sometimes need to be accounted for. IIRC, running PHPStan on those versions could sometimes point them out.

Though we cannot use PHPStan 2 on unsupported versions (and trying to support both PHPStan 1.0 and 2.0 would be too much) so the versions that would benefit from it the most will not be covered.

Even PHPStan itself runs static analysis on the complete compatible subset of the matrix: https://github.com/phpstan/phpstan-src/blob/53e98f9975b28de5d55b7b37cce3dcfc4adfca0e/.github/workflows/static-analysis.yml#L82-L87

🤷‍♀️

Copy link
Copy Markdown
Contributor Author

@Alkarex Alkarex Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From PHPStan 2.0, which supports a PHP version range, variations between PHP versions on the whole range are accounted for, for instance PHP functions that have changed signature over time. I am not aware of cases where it would make a difference to run PHPStan on multiple PHP versions, since the same rules will be checked (as opposed to PHPStan 1.x, where it could make a difference)

- name: Run static analysis
if: ${{ matrix.php >= '8.0' }}
run: composer phpstan -- --error-format=github

test-compiled:
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
"donatj/mock-webserver": "^2.7",
"friendsofphp/php-cs-fixer": "^2.19 || ^3.8",
"mf2/mf2": "^0.5.0",
"phpstan/phpstan": "~1.12.2",
"phpunit/phpunit": "^8 || ^9 || ^10",
"phpstan/phpstan": "~2.1.25",
"phpstan/phpstan-phpunit": "~2.0.7",
"phpunit/phpunit": "^8 || ^9",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot support PHPUnit >= 10 at the same time as PHP 7.2 for now.

Any idea why that is the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional debugging welcome (but maybe not urgent). I am facing a symbol discovery issue when trying to support both PHPUnit >= 10 and PHP 7.2 at the same time

"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1 || ^2 || ^3"
Expand Down Expand Up @@ -69,7 +70,7 @@
"coverage": "phpunit --coverage-html=.phpunit.cache/code-coverage",
"cs": "php-cs-fixer fix --verbose --dry-run --diff",
"fix": "php-cs-fixer fix --verbose --diff",
"phpstan": "phpstan analyze --memory-limit 512M",
"phpstan": "phpstan analyse --memory-limit 512M",
"test": "phpunit"
}
}
59 changes: 12 additions & 47 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
parameters:
phpVersion:
min: 70200 # PHP 7.2
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://phpstan.org/config-reference#phpversion

PHPStan will automatically infer the config.platform.php version from the last composer.json file it can find, if not configured in the PHPStan configuration file.

Looks like it uses the requires field:

https://github.com/phpstan/phpstan-src/blob/53e98f9975b28de5d55b7b37cce3dcfc4adfca0e/src/Php/ComposerPhpVersionFactory.php#L91

max: 80599 # PHP 8.5

level: 8

paths:
Expand All @@ -7,54 +11,15 @@ parameters:
- tests/
- utils/

ignoreErrors:
# Ignore that only one const exists atm
-
message: "#^Strict comparison using \\!\\=\\= between 'GET' and 'GET' will always evaluate to false\\.$#"
count: 1
path: src/HTTP/Psr18Client.php

# Not used since https://github.com/simplepie/simplepie/commit/b2eb0134d53921e75f0fa70b1cf901ed82b988b1 but cannot be removed due to BC.
- '(Constructor of class SimplePie\\Enclosure has an unused parameter \$javascript\.)'

# Testing legacy dynamic property usage.
- '(Access to an undefined property SimplePie.IRI::\$nonexistent_prop\.)'

-
message: '(^Strict comparison using === between string and false will always evaluate to false\.$)'
count: 1
path: src/HTTP/Parser.php
# Only occurs on PHP ≥ 8.0
reportUnmatched: false

-
message: '(^Strict comparison using === between string and false will always evaluate to false\.$)'
count: 1
path: src/IRI.php
# Only occurs on PHP ≥ 8.0
reportUnmatched: false

-
message: '(^Parameter #1 \$exception of method PHPUnit\\Framework\\TestCase::expectException\(\) expects class-string<Throwable>, string given\.$)'

count: 3
path: tests/Unit/Cache/Psr16Test.php
# Only occurs on PHP ≤ 7.4
reportUnmatched: false

-
message: '(^Parameter \$parser of method SimplePie\\Parser::(tag_open|cdata|tag_close)\(\) has invalid type XMLParser\.$)'

count: 3
path: src/Parser.php
# Only occurs on PHP ≤ 7.4
reportUnmatched: false
excludePaths:
analyseAndScan:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? None of these are listed under paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, mainly because it may be interesting to run PHPStan manually on selected parts of the code base.
Furthermore (more tests welcome), I have the impression that paths is not sufficient and files outside those paths are also scanned based on other discovery rules

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using echo '<?php \PHPStan\dumpType(1 + 1);' | tee .git/foo.php build/foo.php vendor/foo.php and composer phpstan -- -v ., I verified that .git is not necessary while the other two are.

- .git/*?
- build/*?
- vendor/*

# PHPStan stubs bug https://github.com/phpstan/phpstan/issues/8629
-
message: '(^Access to an undefined property XMLReader::\$\w+\.$)'
# Only occurs on PHP ≥ 8.2
reportUnmatched: false
treatPhpDocTypesAsCertain: false # TODO: Fix

includes:
- utils/PHPStan/extension.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
1 change: 1 addition & 0 deletions src/Cache/Psr16.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public function get_data(string $key, $default = null)
*/
public function set_data(string $key, array $value, ?int $ttl = null): bool
{
// @phpstan-ignore argument.type (mixed is not supported by PHP < 8.0, used by psr/simple-cache >= 2)
return $this->cache->set($key, $value, $ttl);
}

Expand Down
1 change: 1 addition & 0 deletions src/Enclosure.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ class Enclosure
* @param Rating[]|null $ratings
* @param Restriction[]|null $restrictions
* @param string[]|null $thumbnails
* @phpstan-ignore constructor.unusedParameter
*/
public function __construct(
?string $link = null,
Expand Down
1 change: 0 additions & 1 deletion src/HTTP/FileClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public function __construct(Registry $registry, array $options = [])
*/
public function request(string $method, string $url, array $headers = []): Response
{
// @phpstan-ignore-next-line Enforce PHPDoc type.
if ($method !== self::METHOD_GET) {
throw new InvalidArgumentException(sprintf(
'%s(): Argument #1 ($method) only supports method "%s".',
Expand Down
9 changes: 9 additions & 0 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,34 @@ public function parse(string &$data, string $encoding, string $url = '')
// Strip BOM:
// UTF-32 Big Endian BOM
if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, 4);
}
// UTF-32 Little Endian BOM
elseif (substr($data, 0, 4) === "\xFF\xFE\x00\x00") {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, 4);
}
// UTF-16 Big Endian BOM
elseif (substr($data, 0, 2) === "\xFE\xFF") {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, 2);
}
// UTF-16 Little Endian BOM
elseif (substr($data, 0, 2) === "\xFF\xFE") {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, 2);
}
// UTF-8 BOM
elseif (substr($data, 0, 3) === "\xEF\xBB\xBF") {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, 3);
}

if (substr($data, 0, 5) === '<?xml' && strspn(substr($data, 5, 1), "\x09\x0A\x0D\x20") && ($pos = strpos($data, '?>')) !== false) {
$declaration = $this->registry->create(DeclarationParser::class, [substr($data, 5, $pos - 5)]);
if ($declaration->parse()) {
/** @phpstan-ignore parameterByRef.type (for PHP < 8.0) */
$data = substr($data, $pos + 2);
$data = '<?xml version="' . $declaration->version . '" encoding="' . $encoding . '" standalone="' . (($declaration->standalone) ? 'yes' : 'no') . '"?>' . "\n" .
self::set_doctype($data);
Expand Down Expand Up @@ -276,6 +282,7 @@ public function get_data()
* @param XMLParser|resource|null $parser
* @param array<string, string> $attributes
* @return void
* @phpstan-ignore class.notFound (XMLParser requires PHP >= 8.0)
*/
public function tag_open($parser, string $tag, array $attributes)
{
Expand Down Expand Up @@ -332,6 +339,7 @@ public function tag_open($parser, string $tag, array $attributes)
/**
* @param XMLParser|resource|null $parser
* @return void
* @phpstan-ignore class.notFound (XMLParser requires PHP >= 8.0)
*/
public function cdata($parser, string $cdata)
{
Expand All @@ -345,6 +353,7 @@ public function cdata($parser, string $cdata)
/**
* @param XMLParser|resource|null $parser
* @return void
* @phpstan-ignore class.notFound (XMLParser requires PHP >= 8.0)
*/
public function tag_close($parser, string $tag)
{
Expand Down
1 change: 1 addition & 0 deletions src/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ public function &create($type, array $parameters = [])
$instance->set_registry($this);
} elseif (method_exists($instance, 'set_registry')) {
trigger_error(sprintf('Using the method "set_registry()" without implementing "%s" is deprecated since SimplePie 1.8.0, implement "%s" in "%s".', RegistryAware::class, RegistryAware::class, $class), \E_USER_DEPRECATED);
// @phpstan-ignore method.nonObject
$instance->set_registry($this);
}

Expand Down
1 change: 0 additions & 1 deletion src/Sanitize.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ public function pass_cache_data(bool $enable_cache = true, string $cache_locatio
$this->cache_location = $cache_location;
}

// @phpstan-ignore-next-line Enforce PHPDoc type.
if (!is_string($cache_name_function) && !$cache_name_function instanceof NameFilter) {
throw new InvalidArgumentException(sprintf(
'%s(): Argument #3 ($cache_name_function) must be of type %s',
Expand Down
3 changes: 0 additions & 3 deletions src/SimplePie.php
Original file line number Diff line number Diff line change
Expand Up @@ -1864,7 +1864,6 @@ protected function fetch_data(&$cache)
$cache = new BaseDataCache($cache);
}

// @phpstan-ignore-next-line Enforce PHPDoc type.
if ($cache !== false && !$cache instanceof DataCache) {
throw new InvalidArgumentException(sprintf(
'%s(): Argument #1 ($cache) must be of type %s|false',
Expand Down Expand Up @@ -3374,8 +3373,6 @@ public static function merge_items(array $urls, int $start = 0, int $end = 0, in
foreach ($urls as $arg) {
if ($arg instanceof SimplePie) {
$items = array_merge($items, $arg->get_items(0, $limit));

// @phpstan-ignore-next-line Enforce PHPDoc type.
} else {
trigger_error('Arguments must be SimplePie objects', E_USER_WARNING);
}
Expand Down
1 change: 1 addition & 0 deletions tests/Integration/HTTP/ClientsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private function runTestsWithClientGetContentOfLocalFile(Client $client): void

$response = $client->request(Client::METHOD_GET, $filepath);

// @phpstan-ignore staticMethod.alreadyNarrowedType
self::assertInstanceOf(Response::class, $response);
self::assertSame($filepath, $response->get_permanent_uri());
self::assertSame($filepath, $response->get_final_requested_uri());
Expand Down
1 change: 1 addition & 0 deletions tests/Integration/SimplePieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ public function testMicroformatItems(string $path, string $feedTitle, array $tit

self::assertSame($feedTitle, $feed->get_title());
$items = $feed->get_items();
// @phpstan-ignore phpunit.assertCount
self::assertSame(count($titles), count($items), 'Number of items does not match');
foreach (array_map(null, $titles, $items) as $i => [$expectedTitle, $item]) {
assert($item !== null); // For PHPStan
Expand Down
1 change: 1 addition & 0 deletions tests/Unit/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function testClassExists(): void

public function testFileExtendsResponse(): void
{
// @phpstan-ignore staticMethod.alreadyNarrowedType
self::assertInstanceOf(Response::class, new FileMock(''));
}

Expand Down
1 change: 1 addition & 0 deletions tests/Unit/HTTP/Psr18ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function testRequestReturnsResponse(): void
$this->createMock(UriFactoryInterface::class)
);

// @phpstan-ignore staticMethod.alreadyNarrowedType
self::assertInstanceOf(Response::class, $client->request(Client::METHOD_GET, 'https://example.com/feed.xml'));
}

Expand Down
1 change: 1 addition & 0 deletions tests/Unit/HTTP/Psr7ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Psr7ResponseTest extends TestCase
{
public function testPsr7ResponseExtendsResponse(): void
{
// @phpstan-ignore staticMethod.alreadyNarrowedType
self::assertInstanceOf(Response::class, new Psr7Response($this->createMock(ResponseInterface::class), '', ''));
}

Expand Down
1 change: 1 addition & 0 deletions tests/Unit/IRITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ function ($errno, $errstr): bool {
E_USER_NOTICE
);

// @phpstan-ignore property.notFound (we want to test that it fails)
$should_fail = $iri->nonexistent_prop;
}

Expand Down
9 changes: 6 additions & 3 deletions utils/PHPStan/RegistryCallMethodReturnTypeExtension.php
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling that properly in #969

Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
$classType = $scope->getType($classNameArg);
$methodType = $scope->getType($methodNameArg);

if (!$classType instanceof ConstantStringType || !$methodType instanceof ConstantStringType) {
if (
!$classType instanceof ConstantStringType || // @phpstan-ignore phpstanApi.instanceofType
!$methodType instanceof ConstantStringType // @phpstan-ignore phpstanApi.instanceofType
) {
return new MixedType();
}

Expand All @@ -81,9 +84,9 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
if ($argumentsArg !== null) {
$argumentsType = $scope->getType($argumentsArg);

if ($argumentsType instanceof ConstantArrayType) {
if ($argumentsType instanceof ConstantArrayType) { // @phpstan-ignore phpstanApi.instanceofType
$argumentTypes = $argumentsType->getValueTypes();
} elseif ($argumentsType instanceof ArrayType) {
} elseif ($argumentsType instanceof ArrayType) { // @phpstan-ignore phpstanApi.instanceofType
$argumentTypes = [$argumentsType->getItemType()];
} else {
return new MixedType();
Expand Down
Loading