Skip to content

Commit 95d9e8b

Browse files
committed
Write class names added by fixes relative to namespace and uses
NewClassNamesVisitor runs after every fix and writes the fully qualified class names a fix added, in PHP code and in PHPDocs, relative to the current namespace and existing use statements. With fixer.importNames (on by default) it adds a use statement for a name from another namespace unless its short name already means something in the file. fixer.preferSelf writes the current class as self. The migrate:neon2attributes command now builds fully qualified names and lets the visitor import them, replacing ImportScope. Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce
1 parent 74f4da6 commit 95d9e8b

26 files changed

Lines changed: 1056 additions & 175 deletions

‎conf/config.neon‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ parameters:
248248
errorFormat: null
249249
sysGetTempDir: ::sys_get_temp_dir()
250250
sourceLocatorPlaygroundMode: false
251+
fixer:
252+
importNames: true
253+
preferSelf: false
251254
pro:
252255
dnsServers:
253256
- '1.1.1.2'

‎conf/parametersSchema.neon‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ parametersSchema:
189189
editorUrl: schema(string(), nullable())
190190
editorUrlTitle: schema(string(), nullable())
191191
errorFormat: schema(string(), nullable())
192+
fixer: structure([
193+
importNames: bool(),
194+
preferSelf: bool()
195+
])
192196
pro: structure([
193197
dnsServers: schema(listOf(string()), min(1)),
194198
tmpDir: string()

‎src/Analyser/RuleErrorTransformer.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
namespace PHPStan\Analyser;
44

5+
use PhpParser\ErrorHandler\Collecting;
56
use PhpParser\Internal\TokenStream;
67
use PhpParser\Node;
78
use PhpParser\Node\Stmt;
89
use PhpParser\NodeTraverser;
910
use PhpParser\NodeVisitor\CloningVisitor;
11+
use PhpParser\NodeVisitor\NameResolver;
1012
use PhpParser\Parser;
1113
use PhpParser\Token;
1214
use PHPStan\DependencyInjection\AutowiredParameter;
1315
use PHPStan\DependencyInjection\AutowiredService;
1416
use PHPStan\File\FileReader;
17+
use PHPStan\Fixable\NewClassNamesVisitor;
1518
use PHPStan\Fixable\PhpPrinter;
1619
use PHPStan\Fixable\PhpPrinterIndentationDetectorVisitor;
1720
use PHPStan\Fixable\ReplacingNodeVisitor;
@@ -68,6 +71,10 @@ public function __construct(
6871
private Parser $parser,
6972
#[AutowiredParameter(ref: '@defaultAnalysisParser')]
7073
private AnalysisParser $analysisParser,
74+
#[AutowiredParameter(ref: '%fixer.importNames%')]
75+
private bool $importNames,
76+
#[AutowiredParameter(ref: '%fixer.preferSelf%')]
77+
private bool $preferSelf,
7178
)
7279
{
7380
$this->diffOutputBuilder = new UnifiedDiffOutputBuilder('', addLineNumbers: true);
@@ -239,6 +246,13 @@ private function fix(string $fixingFile, array $fileNodes, array $fixes): ?Fixed
239246
return null;
240247
}
241248

249+
$nameResolver = new NameResolver(new Collecting(), ['replaceNodes' => false]);
250+
/** @var Stmt[] $newStmts */
251+
$newStmts = (new NodeTraverser(
252+
$nameResolver,
253+
new NewClassNamesVisitor($nameResolver->getNameContext(), $this->importNames, $this->preferSelf),
254+
))->traverse($newStmts);
255+
242256
$newCode = $this->getPrinter($indent)->printFormatPreserving($newStmts, $fileNodes, $parsedFile['tokens']);
243257
if ($parsedFile['code'] === $newCode) {
244258
return null;

‎src/Command/Neon2Attributes/ImportScope.php‎

Lines changed: 0 additions & 120 deletions
This file was deleted.

‎src/Command/Neon2Attributes/PhpClassAttributesWriter.php‎

Lines changed: 30 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
namespace PHPStan\Command\Neon2Attributes;
44

5+
use PhpParser\ErrorHandler\Collecting;
56
use PhpParser\Internal\TokenStream;
67
use PhpParser\Node;
78
use PhpParser\NodeTraverser;
89
use PhpParser\NodeVisitor\CloningVisitor;
10+
use PhpParser\NodeVisitor\NameResolver;
911
use PhpParser\ParserFactory;
1012
use PHPStan\DependencyInjection\AutowiredService;
1113
use PHPStan\File\FileReader;
14+
use PHPStan\Fixable\NewClassNamesVisitor;
1215
use PHPStan\Fixable\PhpPrinter;
1316
use PHPStan\Fixable\PhpPrinterIndentationDetectorVisitor;
1417
use PHPStan\ShouldNotHappenException;
1518
use function array_is_list;
16-
use function array_key_exists;
1719
use function count;
1820
use function is_array;
1921
use function is_bool;
@@ -24,9 +26,6 @@
2426
use function str_contains;
2527
use function str_repeat;
2628
use function strcasecmp;
27-
use function strrpos;
28-
use function strtolower;
29-
use function substr;
3029

3130
/**
3231
* Adds attributes to classes and their constructor parameters, keeping the rest of the file as it was.
@@ -62,28 +61,35 @@ public function write(string $file, array $edits): string
6261
$this->addAttributes($file, $newStmts, $edit);
6362
}
6463

64+
$nameResolver = new NameResolver(new Collecting(), ['replaceNodes' => false]);
65+
/** @var list<Node\Stmt> $newStmts */
66+
$newStmts = (new NodeTraverser(
67+
$nameResolver,
68+
new NewClassNamesVisitor($nameResolver->getNameContext(), importNames: true, preferSelf: false),
69+
))->traverse($newStmts);
70+
6571
return (new AttributesPrinter(['indent' => $indent]))->printFormatPreserving($newStmts, $oldStmts, $tokens);
6672
}
6773

6874
/**
6975
* @param list<Node\Stmt> $stmts
7076
*/
71-
private function addAttributes(string $file, array &$stmts, ClassAttributes $edit): void
77+
private function addAttributes(string $file, array $stmts, ClassAttributes $edit): void
7278
{
7379
$className = $edit->reflection->getName();
7480
$namespace = $edit->reflection->getNamespaceName();
7581
$shortName = $edit->reflection->getShortName();
7682

7783
if ($namespace === '') {
78-
$this->addAttributesInScope($stmts, '', $shortName, $edit);
84+
$this->addAttributesInScope($stmts, $shortName, $edit);
7985
return;
8086
}
8187

8288
foreach ($stmts as $stmt) {
8389
if (!$stmt instanceof Node\Stmt\Namespace_ || $stmt->name === null || strcasecmp($stmt->name->toString(), $namespace) !== 0) {
8490
continue;
8591
}
86-
if ($this->addAttributesInScope($stmt->stmts, $namespace, $shortName, $edit)) {
92+
if ($this->addAttributesInScope($stmt->stmts, $shortName, $edit)) {
8793
return;
8894
}
8995
}
@@ -94,7 +100,7 @@ private function addAttributes(string $file, array &$stmts, ClassAttributes $edi
94100
/**
95101
* @param list<Node\Stmt> $stmts statements of a namespace, or of a file without one
96102
*/
97-
private function addAttributesInScope(array &$stmts, string $namespace, string $shortName, ClassAttributes $edit): bool
103+
private function addAttributesInScope(array $stmts, string $shortName, ClassAttributes $edit): bool
98104
{
99105
$class = null;
100106
foreach ($stmts as $stmt) {
@@ -109,8 +115,7 @@ private function addAttributesInScope(array &$stmts, string $namespace, string $
109115
return false;
110116
}
111117

112-
$scope = new ImportScope($namespace, $stmts);
113-
$class->attrGroups[] = new Node\AttributeGroup([$this->createAttribute($scope, $edit->attribute)]);
118+
$class->attrGroups[] = new Node\AttributeGroup([$this->createAttribute($edit->attribute)]);
114119

115120
$constructor = $class->getMethod('__construct');
116121
$isOneLine = $constructor !== null
@@ -125,41 +130,44 @@ private function addAttributesInScope(array &$stmts, string $namespace, string $
125130
continue;
126131
}
127132

128-
$param->attrGroups[] = new Node\AttributeGroup([$this->createAttribute($scope, $attribute)]);
133+
$param->attrGroups[] = new Node\AttributeGroup([$this->createAttribute($attribute)]);
129134
$param->setAttribute(AttributesPrinter::INLINE_ATTRIBUTES, $isOneLine);
130135
}
131136
}
132137

133-
$stmts = $scope->stmts;
134-
135138
return true;
136139
}
137140

138-
private function createAttribute(ImportScope $scope, AttributeSpec $attribute): Node\Attribute
141+
private function createAttribute(AttributeSpec $attribute): Node\Attribute
139142
{
140-
return new Node\Attribute($this->createName($scope, self::DI_NAMESPACE . $attribute->name), $this->createArgs($scope, $attribute));
143+
return new Node\Attribute(new Node\Name\FullyQualified(self::DI_NAMESPACE . $attribute->name), $this->createArgs($attribute));
141144
}
142145

143146
/**
144147
* @return list<Node\Arg>
145148
*/
146-
private function createArgs(ImportScope $scope, AttributeSpec $attribute): array
149+
private function createArgs(AttributeSpec $attribute): array
147150
{
148151
$args = [];
149152
foreach ($attribute->arguments as $name => $value) {
150-
$args[] = new Node\Arg($this->createExpr($scope, $value), name: new Node\Identifier($name));
153+
$args[] = new Node\Arg($this->createExpr($value), name: new Node\Identifier($name));
151154
}
152155

153156
return $args;
154157
}
155158

156-
private function createExpr(ImportScope $scope, mixed $value): Node\Expr
159+
private function createExpr(mixed $value): Node\Expr
157160
{
158161
if ($value instanceof AttributeSpec) {
159-
return new Node\Expr\New_($this->createName($scope, self::DI_NAMESPACE . $value->name), $this->createArgs($scope, $value));
162+
return new Node\Expr\New_(new Node\Name\FullyQualified(self::DI_NAMESPACE . $value->name), $this->createArgs($value));
160163
}
161164
if ($value instanceof ClassReference) {
162-
return new Node\Expr\ClassConstFetch($this->createName($scope, $value->className), 'class');
165+
$className = ltrim($value->className, '\\');
166+
if ($className === '') {
167+
throw new ShouldNotHappenException('Empty class name.');
168+
}
169+
170+
return new Node\Expr\ClassConstFetch(new Node\Name\FullyQualified($className), 'class');
163171
}
164172
if (is_string($value)) {
165173
return new Node\Scalar\String_($value);
@@ -175,8 +183,8 @@ private function createExpr(ImportScope $scope, mixed $value): Node\Expr
175183
$isList = array_is_list($value);
176184
foreach ($value as $key => $item) {
177185
$items[] = new Node\ArrayItem(
178-
$this->createExpr($scope, $item),
179-
$isList ? null : $this->createExpr($scope, $key),
186+
$this->createExpr($item),
187+
$isList ? null : $this->createExpr($key),
180188
);
181189
}
182190

@@ -186,36 +194,4 @@ private function createExpr(ImportScope $scope, mixed $value): Node\Expr
186194
throw new ShouldNotHappenException();
187195
}
188196

189-
/**
190-
* Uses the short name and adds a use statement unless the short name already means another class in the file.
191-
*/
192-
private function createName(ImportScope $scope, string $className): Node\Name
193-
{
194-
$className = ltrim($className, '\\');
195-
$position = strrpos($className, '\\');
196-
$shortName = $position === false ? $className : substr($className, $position + 1);
197-
if ($className === '' || $shortName === '') {
198-
throw new ShouldNotHappenException(sprintf('Invalid class name %s.', $className));
199-
}
200-
$lowerShortName = strtolower($shortName);
201-
202-
if (array_key_exists($lowerShortName, $scope->uses)) {
203-
return strcasecmp($scope->uses[$lowerShortName], $className) === 0
204-
? new Node\Name($shortName)
205-
: new Node\Name\FullyQualified($className);
206-
}
207-
208-
$localName = $scope->namespace === '' ? $shortName : $scope->namespace . '\\' . $shortName;
209-
if (strcasecmp($localName, $className) === 0) {
210-
return new Node\Name($shortName);
211-
}
212-
if (array_key_exists($lowerShortName, $scope->unqualifiedNames)) {
213-
return new Node\Name\FullyQualified($className);
214-
}
215-
216-
$scope->addUse($className, $lowerShortName);
217-
218-
return new Node\Name($shortName);
219-
}
220-
221197
}

0 commit comments

Comments
 (0)