Skip to content

Commit 1870036

Browse files
authored
Remove stale generated imports (#32)
When a file is moved or refactored, the hash in generated import paths changes. Previously, the generator would add the new correct import but leave the old stale import behind, requiring manual cleanup. This adds a StaleImportRemover visitor that automatically removes use statements matching the generated namespace pattern (Query/Mutation with 6-char hex hash) that don't match the current valid FQCNs.
1 parent 587a79f commit 1870036

File tree

11 files changed

+453
-0
lines changed

11 files changed

+453
-0
lines changed

phpstan.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
__DIR__ . '/tests/*/Generated/*',
6262
],
6363
],
64+
[
65+
'identifiers' => [
66+
'shipmonk.deadMethod',
67+
],
68+
'paths' => [
69+
__DIR__ . '/tests/StaleImportRemoval/ControllerWithStaleImport.php',
70+
],
71+
],
6472
[
6573
'identifiers' => [
6674
'shipmonk.deadConstant',

src/Executor/PlanExecutor.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Ruudk\GraphQLCodeGenerator\Generator\OperationClassGenerator;
2626
use Ruudk\GraphQLCodeGenerator\GraphQL\AST\Printer;
2727
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\OperationInjector;
28+
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\StaleImportRemover;
2829
use Ruudk\GraphQLCodeGenerator\PHP\Visitor\UseStatementInserter;
2930
use Ruudk\GraphQLCodeGenerator\Planner\OperationPlan;
3031
use Ruudk\GraphQLCodeGenerator\Planner\Plan\DataClassPlan;
@@ -138,6 +139,7 @@ public function execute(PlannerResult $plan) : array
138139

139140
$newStmts = new NodeTraverser(
140141
new NodeConnectingVisitor(),
142+
new StaleImportRemover($this->config->namespace, $fqcns),
141143
new UseStatementInserter($fqcns),
142144
)->traverse($newStmts);
143145

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\PHP\Visitor;
6+
7+
use Override;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Stmt\Use_;
10+
use PhpParser\NodeVisitor;
11+
use PhpParser\NodeVisitorAbstract;
12+
13+
/**
14+
* Removes stale use statements for generated GraphQL classes.
15+
*
16+
* When a file is moved or refactored, the hash in the generated namespace changes.
17+
* This visitor removes use statements that match the generated namespace pattern
18+
* but don't match the current valid FQCNs.
19+
*/
20+
final class StaleImportRemover extends NodeVisitorAbstract
21+
{
22+
/**
23+
* @var array<string, bool>
24+
*/
25+
private array $validFqcns;
26+
private string $namespacePattern;
27+
28+
/**
29+
* @param string $generatedNamespace The namespace for generated classes (e.g., "App\Generated")
30+
* @param list<string> $validFqcns The list of valid/current FQCNs to keep
31+
*/
32+
public function __construct(
33+
string $generatedNamespace,
34+
array $validFqcns,
35+
) {
36+
$this->validFqcns = array_fill_keys($validFqcns, true);
37+
38+
// Build a regex pattern to match generated operation imports
39+
// Pattern: {namespace}\{Query|Mutation}\{OperationNameWithHash}\{ClassName}
40+
// The hash is 6 hex characters
41+
$escapedNamespace = preg_quote($generatedNamespace, '/');
42+
$this->namespacePattern = '/^' . $escapedNamespace . '\\\\(?:Query|Mutation)\\\\[A-Za-z]+[a-f0-9]{6}\\\\/';
43+
}
44+
45+
#[Override]
46+
public function enterNode(Node $node) : ?int
47+
{
48+
if ( ! $node instanceof Use_) {
49+
return null;
50+
}
51+
52+
// Check each use clause
53+
foreach ($node->uses as $use) {
54+
$fqcn = $use->name->toString();
55+
56+
// If this import matches the generated pattern but is not in our valid list, remove it
57+
if (preg_match($this->namespacePattern, $fqcn) === 1 && ! isset($this->validFqcns[$fqcn])) {
58+
return NodeVisitor::REMOVE_NODE;
59+
}
60+
}
61+
62+
return null;
63+
}
64+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;
6+
7+
use Ruudk\GraphQLCodeGenerator\Attribute\GeneratedGraphQLClient;
8+
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\GetViewerQuery;
9+
10+
final readonly class ControllerWithStaleImport
11+
{
12+
private const string OPERATION = <<<'GRAPHQL'
13+
query GetViewer {
14+
viewer {
15+
login
16+
}
17+
}
18+
GRAPHQL;
19+
20+
public function __construct(
21+
#[GeneratedGraphQLClient(self::OPERATION)]
22+
public GetViewerQuery $query,
23+
) {}
24+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;
6+
7+
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\Data\Viewer;
8+
9+
// This file was automatically generated and should not be edited.
10+
11+
final class Data
12+
{
13+
public Viewer $viewer {
14+
get => $this->viewer ??= new Viewer($this->data['viewer']);
15+
}
16+
17+
/**
18+
* @var list<Error>
19+
*/
20+
public readonly array $errors;
21+
22+
/**
23+
* @param array{
24+
* 'viewer': array{
25+
* 'login': string,
26+
* },
27+
* } $data
28+
* @param list<array{
29+
* 'code': string,
30+
* 'debugMessage'?: string,
31+
* 'message': string,
32+
* }> $errors
33+
*/
34+
public function __construct(
35+
private readonly array $data,
36+
array $errors,
37+
) {
38+
$this->errors = array_map(fn(array $error) => new Error($error), $errors);
39+
}
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d\Data;
6+
7+
// This file was automatically generated and should not be edited.
8+
9+
final class Viewer
10+
{
11+
public string $login {
12+
get => $this->login ??= $this->data['login'];
13+
}
14+
15+
/**
16+
* @param array{
17+
* 'login': string,
18+
* } $data
19+
*/
20+
public function __construct(
21+
private readonly array $data,
22+
) {}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;
6+
7+
// This file was automatically generated and should not be edited.
8+
9+
final readonly class Error
10+
{
11+
public string $message;
12+
13+
/**
14+
* @param array{
15+
* 'debugMessage'?: string,
16+
* 'message': string,
17+
* } $error
18+
*/
19+
public function __construct(array $error)
20+
{
21+
$this->message = $error['debugMessage'] ?? $error['message'];
22+
}
23+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewer6b9a6d;
6+
7+
use Ruudk\GraphQLCodeGenerator\TestClient;
8+
9+
// This file was automatically generated and should not be edited.
10+
11+
final readonly class GetViewerQuery {
12+
public const string OPERATION_NAME = 'GetViewer';
13+
public const string OPERATION_DEFINITION = <<<'GRAPHQL'
14+
query GetViewer {
15+
viewer {
16+
login
17+
}
18+
}
19+
20+
GRAPHQL;
21+
22+
public function __construct(
23+
private TestClient $client,
24+
) {}
25+
26+
public function execute() : Data
27+
{
28+
$data = $this->client->graphql(
29+
self::OPERATION_DEFINITION,
30+
[
31+
],
32+
self::OPERATION_NAME,
33+
);
34+
35+
return new Data(
36+
$data['data'] ?? [], // @phpstan-ignore argument.type
37+
$data['errors'] ?? [] // @phpstan-ignore argument.type
38+
);
39+
}
40+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type Query {
2+
viewer: Viewer!
3+
}
4+
5+
type Viewer {
6+
login: String!
7+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;
6+
7+
use Override;
8+
use Ruudk\GraphQLCodeGenerator\Config\Config;
9+
use Ruudk\GraphQLCodeGenerator\Executor\PlanExecutor;
10+
use Ruudk\GraphQLCodeGenerator\GraphQLTestCase;
11+
use Ruudk\GraphQLCodeGenerator\Planner;
12+
13+
final class StaleImportRemovalTest extends GraphQLTestCase
14+
{
15+
#[Override]
16+
public function getConfig() : Config
17+
{
18+
return parent::getConfig()
19+
->withInlineProcessingDirectory(__DIR__);
20+
}
21+
22+
public function testStaleImportsAreRemoved() : void
23+
{
24+
// First, create a "stale" version of the controller file with old imports
25+
$staleContent = <<<'PHP'
26+
<?php
27+
28+
declare(strict_types=1);
29+
30+
namespace Ruudk\GraphQLCodeGenerator\StaleImportRemoval;
31+
32+
use Ruudk\GraphQLCodeGenerator\Attribute\GeneratedGraphQLClient;
33+
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewerabc123\GetViewerQuery;
34+
use Ruudk\GraphQLCodeGenerator\StaleImportRemoval\Generated\Query\GetViewerdef456\GetViewerQuery as AliasedQuery;
35+
36+
final readonly class ControllerWithStaleImport
37+
{
38+
private const string OPERATION = <<<'GRAPHQL'
39+
query GetViewer {
40+
viewer {
41+
login
42+
}
43+
}
44+
GRAPHQL;
45+
46+
public function __construct(
47+
#[GeneratedGraphQLClient(self::OPERATION)]
48+
public GetViewerQuery $query,
49+
) {}
50+
}
51+
PHP;
52+
53+
$controllerPath = __DIR__ . '/ControllerWithStaleImport.php';
54+
$originalContent = file_get_contents($controllerPath);
55+
56+
try {
57+
// Write the stale content
58+
file_put_contents($controllerPath, $staleContent);
59+
60+
// Run the generator
61+
$config = $this->getConfig();
62+
$plan = new Planner($config)->plan();
63+
$files = new PlanExecutor($config)->execute($plan);
64+
65+
// Check the output
66+
self::assertArrayHasKey($controllerPath, $files, 'Controller file should be in output');
67+
$output = $files[$controllerPath];
68+
69+
// The stale imports should be removed
70+
self::assertStringNotContainsString(
71+
'GetViewerabc123',
72+
$output,
73+
'Stale import with old hash should be removed',
74+
);
75+
self::assertStringNotContainsString(
76+
'GetViewerdef456',
77+
$output,
78+
'Another stale import should also be removed',
79+
);
80+
81+
// The correct import should be present
82+
$matches = [];
83+
preg_match_all(
84+
'/use Ruudk\\\\GraphQLCodeGenerator\\\\StaleImportRemoval\\\\Generated\\\\Query\\\\GetViewer[a-f0-9]+\\\\GetViewerQuery/',
85+
$output,
86+
$matches,
87+
);
88+
89+
self::assertCount(
90+
1,
91+
$matches[0],
92+
'There should be exactly one import for GetViewerQuery with the correct hash',
93+
);
94+
} finally {
95+
// Restore the original content
96+
file_put_contents($controllerPath, $originalContent);
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)