Skip to content

Commit

Permalink
Upgrade to PHPStan 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jtojnar committed Feb 23, 2025
1 parent 38976e3 commit 9c76fc9
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 122 deletions.
9 changes: 5 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
"php-http/guzzle7-adapter": "^1.0",
"php-http/mock-client": "^1.4",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.12",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"rector/rector": "^0.12.11",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"rector/rector": "^2.0.0",
"symfony/filesystem": "^5.4",
"symfony/phpunit-bridge": "^7.0.1"
},
"autoload": {
Expand Down
100 changes: 51 additions & 49 deletions maintenance/Rector/MockGrabyResponseRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,39 +23,39 @@
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Psr\Http\Message\ResponseInterface;
use Rector\Core\Application\FileSystem\RemovedAndAddedFilesCollector;
use Rector\Core\Rector\AbstractRector;
use Rector\FileSystemRector\ValueObject\AddedFileWithContent;
use Rector\Naming\Naming\VariableNaming;
use Rector\NodeNestingScope\ParentScopeFinder;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Value\ValueResolver;
use Rector\PostRector\Collector\UseNodesToAddCollector;
use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Symfony\Component\Filesystem\Path;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Symplify\SmartFileSystem\SmartFileInfo;

final class MockGrabyResponseRector extends AbstractRector
{
private const FIXTURE_DIRECTORY = __DIR__ . '/../../tests/fixtures/content';
private const MATCHING_ERROR_COMMENT = 'TODO: Rector was unable to evaluate this Graby config.';
private const IGNORE_COMMENT = 'Rector: do not add mock client';
private ParentScopeFinder $parentScopeFinder;
private RemovedAndAddedFilesCollector $removedAndAddedFilesCollector;
private BetterNodeFinder $betterNodeFinder;
private UseNodesToAddCollector $useNodesToAddCollector;
private ValueResolver $valueResolver;
private VariableNaming $variableNaming;

public function __construct(
ParentScopeFinder $parentScopeFinder,
RemovedAndAddedFilesCollector $removedAndAddedFilesCollector,
BetterNodeFinder $betterNodeFinder,
UseNodesToAddCollector $useNodesToAddCollector,
ValueResolver $valueResolver,
VariableNaming $variableNaming
) {
$this->parentScopeFinder = $parentScopeFinder;
$this->removedAndAddedFilesCollector = $removedAndAddedFilesCollector;
$this->betterNodeFinder = $betterNodeFinder;
$this->useNodesToAddCollector = $useNodesToAddCollector;
$this->valueResolver = $valueResolver;
$this->variableNaming = $variableNaming;
}

Expand Down Expand Up @@ -86,30 +86,37 @@ public function getRuleDefinition(): RuleDefinition
*/
public function getNodeTypes(): array
{
return [New_::class];
// PHPUnit tests are class methods.
return [ClassMethod::class];
}

/**
* @param New_ $node
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$new = $node;
if (!$this->nodeNameResolver->isName($new->class, Graby::class)) {
return null;
}
$assigns = $this->betterNodeFinder->find(
$node,
fn (Node $node): bool => $node instanceof Expression
&& ($assignment = $node->expr) instanceof Assign
&& ($new = $assignment->expr) instanceof New_
&& $this->nodeNameResolver->isName($new->class, Graby::class)
);

/** @var Node $parentNode */
$parentNode = $new->getAttribute(AttributeKey::PARENT_NODE);
$assignment = $parentNode;
if (!$assignment instanceof Assign) {
if (1 !== \count($assigns)) {
return null;
}

$statement = $this->betterNodeFinder->resolveCurrentStatement($new);
\assert(null !== $statement, 'Graby construction needs to be inside a statement.');
$assignStmt = $assigns[0];
\assert($assignStmt instanceof Expression);

$assignment = $assignStmt->expr;
\assert($assignment instanceof Assign);

$comments = $statement->getAttribute(AttributeKey::COMMENTS);
$new = $assignment->expr;
\assert($new instanceof New_);

$comments = $assignStmt->getAttribute(AttributeKey::COMMENTS);
if (null !== $comments) {
foreach ($comments as $comment) {
if (preg_match('(' . preg_quote(self::MATCHING_ERROR_COMMENT) . '|' . preg_quote(self::IGNORE_COMMENT) . ')', $comment->getText())) {
Expand All @@ -129,15 +136,12 @@ public function refactor(Node $node): ?Node
return null;
}

$scope = $this->parentScopeFinder->find($new);
if (null === $scope) {
return null;
}
$stmts = (array) $node->stmts;

$fetchUrls = array_map(
fn (Node $node): ?string => $this->getFetchUrl($grabyVariable, $node),
$this->betterNodeFinder->find(
(array) $scope->stmts,
$stmts,
fn (Node $foundNode): bool => null !== $this->getFetchUrl($grabyVariable, $foundNode)
)
);
Expand All @@ -150,14 +154,12 @@ public function refactor(Node $node): ?Node
$url = (string) $fetchUrls[0];

// Add imports.
$this->useNodesToAddCollector->addUseImport(
new AliasedObjectType('HttpMockClient', \Http\Mock\Client::class)
);
// `use Http\Mock\Client as HttpMockClient;` needs to be added manually.
$this->useNodesToAddCollector->addUseImport(
new FullyQualifiedObjectType(\GuzzleHttp\Psr7\Response::class)
);

$httpMockClientVariable = $this->createMockClientVariable($new);
$httpMockClientVariable = $this->createMockClientVariable($assignment);
// List of statements to be placed before the Graby construction.
$mockStatements = [
new Assign($httpMockClientVariable, new New_(new Name('HttpMockClient'))),
Expand All @@ -168,27 +170,25 @@ public function refactor(Node $node): ?Node
// Paste the config here if this failed.
$config = [];

$statement->setAttribute(
$assignStmt->setAttribute(
AttributeKey::COMMENTS,
array_merge(
$comments ?? [],
[new Comment('// ' . self::MATCHING_ERROR_COMMENT)]
)
);

return $new;
return $node;
}

foreach ($this->fetchResponses($url, $config) as $index => $response) {
$suffix = (0 !== $index ? '.' . $index : '') . preg_match('(\.[a-z0-9]+$)', $url) ? '' : '.html';
$fileName = preg_replace('([^a-zA-Z0-9-_\.])', '_', $url) . $suffix;

// Create a fixture.
$this->removedAndAddedFilesCollector->addAddedFile(
new AddedFileWithContent(
self::FIXTURE_DIRECTORY . '/' . $fileName,
(string) $response->getBody()
)
file_put_contents(
self::FIXTURE_DIRECTORY . '/' . $fileName,
(string) $response->getBody()
);

// Register a mocked response.
Expand All @@ -201,22 +201,25 @@ public function refactor(Node $node): ?Node
);
}

$this->nodesToAddCollector->addNodesBeforeNode($mockStatements, $new);

// Add the mocked client to Graby constructor.
$new->args[] = new Arg($httpMockClientVariable);

return $new;
$index = array_search($assignStmt, $stmts, true);
\assert(false !== $index, 'Assignment statement not direct child of the method');
\assert(!\is_string($index)); // For PHPStan.

array_splice($stmts, $index, 0, $mockStatements);

return $node;
}

/**
* Provides a new variable called $httpMockClient,
* optionally followed by number if the name is already taken.
*/
private function createMockClientVariable(New_ $new): Variable
private function createMockClientVariable(Assign $assignment): Variable
{
$currentStmt = $this->betterNodeFinder->resolveCurrentStatement($new);
$scope = $currentStmt->getAttribute(AttributeKey::SCOPE);
$scope = $assignment->getAttribute(AttributeKey::SCOPE);
\assert(null !== $scope); // For PHPStan.

return new Variable($this->variableNaming->createCountedValueName('httpMockClient', $scope));
Expand All @@ -229,8 +232,7 @@ private function createMockClientVariable(New_ $new): Variable
*/
private function createNewResponseExpression(ResponseInterface $response, string $fileName): New_
{
$fixtureDirectory = new SmartFileInfo(self::FIXTURE_DIRECTORY);
$relativeFixturePath = $fixtureDirectory->getRelativeFilePathFromDirectory($this->file->getSmartFileInfo()->getRealPathDirectory());
$relativeFixturePath = Path::makeRelative(self::FIXTURE_DIRECTORY, \dirname($this->file->getFilePath()));

return new New_(
new Name('Response'),
Expand Down
7 changes: 7 additions & 0 deletions maintenance/phpstan-baseline-lt-8.0.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: '#^Method Graby\\SiteConfig\\ConfigBuilder\:\:makeHostKey\(\) should return string but returns string\|false\.$#'
identifier: return.type
count: 1
path: ../src/SiteConfig/ConfigBuilder.php
14 changes: 14 additions & 0 deletions maintenance/phpstan-ignore-by-php-version.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

$includes = [];
if (\PHP_VERSION_ID < 80000) {
$includes[] = __DIR__ . '/phpstan-baseline-lt-8.0.neon';
}

$config = [];
$config['includes'] = $includes;
$config['parameters']['phpVersion'] = \PHP_VERSION_ID;

return $config;
36 changes: 16 additions & 20 deletions maintenance/rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,32 @@
declare(strict_types=1);

use Maintenance\Graby\Rector\MockGrabyResponseRector;
use Rector\Config\RectorConfig;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Set\ValueObject\LevelSetList;

return static function (Rector\Config\RectorConfig $rectorConfig): void {
$rectorConfig->sets([
return RectorConfig::configure()
->withSets([
LevelSetList::UP_TO_PHP_74,
]);
])

// Breaks creating new files.
// https://github.com/rectorphp/rector/issues/7231
$rectorConfig->disableParallel();
->withRules([
MockGrabyResponseRector::class,
])

$rectorConfig->rule(MockGrabyResponseRector::class);
$rectorConfig->paths([
->withPaths([
__DIR__ . '/../maintenance',
__DIR__ . '/../src',
__DIR__ . '/../tests',
]);
])

$phpunitBridges = glob(__DIR__ . '/../vendor/bin/.phpunit/phpunit-*');
$latestPhpunitBridge = end($phpunitBridges);
assert(false !== $latestPhpunitBridge, 'There must be at least one PHPUnit version installed by Symfony PHPUnit bridge, please run `vendor/bin/simple-phpunit install`.');
$rectorConfig->bootstrapFiles([
$latestPhpunitBridge . '/vendor/autoload.php',
]);
->withBootstrapFiles([
__DIR__ . '/../vendor/bin/.phpunit/phpunit/vendor/autoload.php',
__DIR__ . '/../vendor/autoload.php',
])

$rectorConfig->skip([
->withSkip([
// nodeNameResolver requires string.
StringClassNameToClassConstantRector::class => __DIR__ . '/Rector/**',
]);

$rectorConfig->phpstanConfig(__DIR__ . '/../phpstan.neon');
};
])
;
3 changes: 3 additions & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
includes:
- maintenance/phpstan-ignore-by-php-version.php

parameters:
level: 7
paths:
Expand Down
13 changes: 4 additions & 9 deletions src/Extractor/ContentExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class ContentExtractor
{
public ?Readability $readability = null;
private ?\DOMXPath $xpath = null;
private ?string $html = null;
private ContentExtractorConfig $config;
private ?SiteConfig $siteConfig = null;
private ?string $title = null;
Expand Down Expand Up @@ -70,7 +69,6 @@ public function setLogger(LoggerInterface $logger): void
public function reset(): void
{
$this->xpath = null;
$this->html = null;
$this->readability = null;
$this->siteConfig = null;
$this->title = null;
Expand Down Expand Up @@ -589,7 +587,6 @@ public function process(string $html, UriInterface $url, ?SiteConfig $siteConfig

foreach (['src', 'srcset'] as $attr) {
if (\array_key_exists($attr, $attributes)
&& null !== $attributes[$attr]
&& !empty($attributes[$attr])) {
$e->setAttribute($attr, $attributes[$attr]);
}
Expand Down Expand Up @@ -1418,12 +1415,10 @@ private function extractJsonLdInformation(\DOMXPath $xpath): void
}
}

if (\is_array($candidateNames) && \count($candidateNames) > 0) {
foreach ($candidateNames as $name) {
if (!\in_array($name, $ignoreNames, true)) {
$this->title = $name;
$this->logger->info('title matched from JsonLd: {{title}}', ['title' => $name]);
}
foreach ($candidateNames as $name) {
if (!\in_array($name, $ignoreNames, true)) {
$this->title = $name;
$this->logger->info('title matched from JsonLd: {{title}}', ['title' => $name]);
}
}
}
Expand Down
Loading

0 comments on commit 9c76fc9

Please sign in to comment.