Skip to content
Merged
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
8 changes: 8 additions & 0 deletions phpstan.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
__DIR__ . '/tests/*/Generated/*',
],
],
[
'identifiers' => [
'shipmonk.deadMethod',
],
'paths' => [
__DIR__ . '/tests/StaleImportRemoval/ControllerWithStaleImport.php',
],
],
[
'identifiers' => [
'shipmonk.deadConstant',
Expand Down
2 changes: 2 additions & 0 deletions src/Executor/PlanExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Ruudk\GraphQLCodeGenerator\Generator\OperationClassGenerator;
use Ruudk\GraphQLCodeGenerator\GraphQL\AST\Printer;
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\OperationInjector;
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\StaleImportRemover;
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\UseStatementInserter;
use Ruudk\GraphQLCodeGenerator\Planner\OperationPlan;
use Ruudk\GraphQLCodeGenerator\Planner\Plan\DataClassPlan;
Expand Down Expand Up @@ -138,6 +139,7 @@ public function execute(PlannerResult $plan) : array

$newStmts = new NodeTraverser(
new NodeConnectingVisitor(),
new StaleImportRemover($this->config->namespace, $fqcns),
new UseStatementInserter($fqcns),
)->traverse($newStmts);

Expand Down
64 changes: 64 additions & 0 deletions src/PHP/Visitor/StaleImportRemover.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\PHP\Visitor;

use Override;
use PhpParser\Node;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;

/**
* Removes stale use statements for generated GraphQL classes.
*
* When a file is moved or refactored, the hash in the generated namespace changes.
* This visitor removes use statements that match the generated namespace pattern
* but don't match the current valid FQCNs.
*/
final class StaleImportRemover extends NodeVisitorAbstract
{
/**
* @var array<string, bool>
*/
private array $validFqcns;
private string $namespacePattern;

/**
* @param string $generatedNamespace The namespace for generated classes (e.g., "App\Generated")
* @param list<string> $validFqcns The list of valid/current FQCNs to keep
*/
public function __construct(
string $generatedNamespace,
array $validFqcns,
) {
$this->validFqcns = array_fill_keys($validFqcns, true);

// Build a regex pattern to match generated operation imports
// Pattern: {namespace}\{Query|Mutation}\{OperationNameWithHash}\{ClassName}
// The hash is 6 hex characters
$escapedNamespace = preg_quote($generatedNamespace, '/');
$this->namespacePattern = '/^' . $escapedNamespace . '\\\\(?:Query|Mutation)\\\\[A-Za-z]+[a-f0-9]{6}\\\\/';
}

#[Override]
public function enterNode(Node $node) : ?int
{
if ( ! $node instanceof Use_) {
return null;
}

// Check each use clause
foreach ($node->uses as $use) {
$fqcn = $use->name->toString();

// If this import matches the generated pattern but is not in our valid list, remove it
if (preg_match($this->namespacePattern, $fqcn) === 1 && ! isset($this->validFqcns[$fqcn])) {
return NodeVisitor::REMOVE_NODE;
}
}

return null;
}
}
24 changes: 24 additions & 0 deletions tests/StaleImportRemoval/ControllerWithStaleImport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;

use Ruudk\GraphQLCodeGenerator\Attribute\GeneratedGraphQLClient;
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\GetViewerQuery;

final readonly class ControllerWithStaleImport
{
private const string OPERATION = <<<'GRAPHQL'
query GetViewer {
viewer {
login
}
}
GRAPHQL;

public function __construct(
#[GeneratedGraphQLClient(self::OPERATION)]
public GetViewerQuery $query,
) {}
}
40 changes: 40 additions & 0 deletions tests/StaleImportRemoval/Generated/Query/GetViewer6b9a6d/Data.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;

use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\Data\Viewer;

// This file was automatically generated and should not be edited.

final class Data
{
public Viewer $viewer {
get => $this->viewer ??= new Viewer($this->data['viewer']);
}

/**
* @var list<Error>
*/
public readonly array $errors;

/**
* @param array{
* 'viewer': array{
* 'login': string,
* },
* } $data
* @param list<array{
* 'code': string,
* 'debugMessage'?: string,
* 'message': string,
* }> $errors
*/
public function __construct(
private readonly array $data,
array $errors,
) {
$this->errors = array_map(fn(array $error) => new Error($error), $errors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\Data;

// This file was automatically generated and should not be edited.

final class Viewer
{
public string $login {
get => $this->login ??= $this->data['login'];
}

/**
* @param array{
* 'login': string,
* } $data
*/
public function __construct(
private readonly array $data,
) {}
}
23 changes: 23 additions & 0 deletions tests/StaleImportRemoval/Generated/Query/GetViewer6b9a6d/Error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;

// This file was automatically generated and should not be edited.

final readonly class Error
{
public string $message;

/**
* @param array{
* 'debugMessage'?: string,
* 'message': string,
* } $error
*/
public function __construct(array $error)
{
$this->message = $error['debugMessage'] ?? $error['message'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;

use Ruudk\GraphQLCodeGenerator\TestClient;

// This file was automatically generated and should not be edited.

final readonly class GetViewerQuery {
public const string OPERATION_NAME = 'GetViewer';
public const string OPERATION_DEFINITION = <<<'GRAPHQL'
query GetViewer {
viewer {
login
}
}

GRAPHQL;

public function __construct(
private TestClient $client,
) {}

public function execute() : Data
{
$data = $this->client->graphql(
self::OPERATION_DEFINITION,
[
],
self::OPERATION_NAME,
);

return new Data(
$data['data'] ?? [], // @phpstan-ignore argument.type
$data['errors'] ?? [] // @phpstan-ignore argument.type
);
}
}
7 changes: 7 additions & 0 deletions tests/StaleImportRemoval/Schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Query {
viewer: Viewer!
}

type Viewer {
login: String!
}
99 changes: 99 additions & 0 deletions tests/StaleImportRemoval/StaleImportRemovalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;

use Override;
use Ruudk\GraphQLCodeGenerator\Config\Config;
use Ruudk\GraphQLCodeGenerator\Executor\PlanExecutor;
use Ruudk\GraphQLCodeGenerator\GraphQLTestCase;
use Ruudk\GraphQLCodeGenerator\Planner;

final class StaleImportRemovalTest extends GraphQLTestCase
{
#[Override]
public function getConfig() : Config
{
return parent::getConfig()
->withInlineProcessingDirectory(__DIR__);
}

public function testStaleImportsAreRemoved() : void
{
// First, create a "stale" version of the controller file with old imports
$staleContent = <<<'PHP'
<?php

declare(strict_types=1);

namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;

use Ruudk\GraphQLCodeGenerator\Attribute\GeneratedGraphQLClient;
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewerabc123\GetViewerQuery;
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewerdef456\GetViewerQuery as AliasedQuery;

final readonly class ControllerWithStaleImport
{
private const string OPERATION = <<<'GRAPHQL'
query GetViewer {
viewer {
login
}
}
GRAPHQL;

public function __construct(
#[GeneratedGraphQLClient(self::OPERATION)]
public GetViewerQuery $query,
) {}
}
PHP;

$controllerPath = __DIR__ . '/ControllerWithStaleImport.php';
$originalContent = file_get_contents($controllerPath);

try {
// Write the stale content
file_put_contents($controllerPath, $staleContent);

// Run the generator
$config = $this->getConfig();
$plan = new Planner($config)->plan();
$files = new PlanExecutor($config)->execute($plan);

// Check the output
self::assertArrayHasKey($controllerPath, $files, 'Controller file should be in output');
$output = $files[$controllerPath];

// The stale imports should be removed
self::assertStringNotContainsString(
'GetViewerabc123',
$output,
'Stale import with old hash should be removed',
);
self::assertStringNotContainsString(
'GetViewerdef456',
$output,
'Another stale import should also be removed',
);

// The correct import should be present
$matches = [];
preg_match_all(
'/use Ruudk\\\\GraphQLCodeGenerator\\\\StaleImportRemoval\\\\Generated\\\\Query\\\\GetViewer[a-f0-9]+\\\\GetViewerQuery/',
$output,
$matches,
);

self::assertCount(
1,
$matches[0],
'There should be exactly one import for GetViewerQuery with the correct hash',
);
} finally {
// Restore the original content
file_put_contents($controllerPath, $originalContent);
}
}
}
Loading