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
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,18 @@ parameters:
count: 1
path: src/DependencyInjection/NeonAdapter.php

-
rawMessage: 'Call to method getIndentationBefore() of internal class PhpParser\Internal\TokenStream from outside its root namespace PhpParser.'
identifier: method.internalClass
count: 1
path: src/Fixable/PhpPrinter.php

-
rawMessage: 'Call to method getTokenCode() of internal class PhpParser\Internal\TokenStream from outside its root namespace PhpParser.'
identifier: method.internalClass
count: 1
path: src/Fixable/PhpPrinter.php

-
rawMessage: 'Call to method getTokenCode() of internal class PhpParser\Internal\TokenStream from outside its root namespace PhpParser.'
identifier: method.internalClass
Expand Down
75 changes: 47 additions & 28 deletions src/Analyser/RuleErrorTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\File\FileReader;
use PHPStan\Fixable\NewClassNamesVisitor;
use PHPStan\Fixable\PathCloningVisitor;
use PHPStan\Fixable\PhpPrinter;
use PHPStan\Fixable\PhpPrinterIndentationDetectorVisitor;
use PHPStan\Fixable\ReplacingNodeVisitor;
use PHPStan\Fixable\SharedNodeCloningVisitor;
use PHPStan\Node\VirtualNode;
use PHPStan\Parser\Parser as AnalysisParser;
use PHPStan\Parser\ParserErrorsException;
Expand All @@ -35,6 +37,7 @@
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
use function array_key_exists;
use function array_map;
use function array_shift;
use function array_slice;
use function count;
Expand Down Expand Up @@ -217,50 +220,66 @@ private function fix(string $fixingFile, array $fileNodes, array $fixes): ?Fixed
$parsedFile = $this->getParsedFile($fixingFile);

$indentDetector = null;
$pathCloningVisitor = null;
$visitors = [];
$fixedNodes = array_map(static fn (NodeFix $fix): Node => $fix->node, $fixes);
if ($parsedFile['indent'] === null) {
$indentDetector = new PhpPrinterIndentationDetectorVisitor(new TokenStream($parsedFile['tokens'], PhpPrinter::TAB_WIDTH));
$visitors[] = $indentDetector;
$visitors[] = new CloningVisitor();
} elseif (PathCloningVisitor::canFind($fixedNodes)) {
$pathCloningVisitor = new PathCloningVisitor($fixedNodes);
$visitors[] = $pathCloningVisitor;
} else {
$visitors[] = new CloningVisitor();
}
$visitors[] = new CloningVisitor();
$replacingVisitor = new ReplacingNodeVisitor($fixes);
$visitors[] = $replacingVisitor;

/** @var Stmt[] $newStmts */
$newStmts = (new NodeTraverser(...$visitors))->traverse($fileNodes);
try {
/** @var Stmt[] $newStmts */
$newStmts = (new NodeTraverser(...$visitors))->traverse($fileNodes);

$indent = $parsedFile['indent'];
if ($indentDetector !== null) {
if (str_contains($indentDetector->indentCharacter, "\t")) {
$indent = "\t";
} else {
$indent = str_repeat($indentDetector->indentCharacter, $indentDetector->indentSize);
}

$indent = $parsedFile['indent'];
if ($indentDetector !== null) {
if (str_contains($indentDetector->indentCharacter, "\t")) {
$indent = "\t";
} else {
$indent = str_repeat($indentDetector->indentCharacter, $indentDetector->indentSize);
if ($indentDetector->found) {
$this->parsedFiles[$fixingFile]['indent'] = $indent;
}
}

if ($indentDetector->found) {
$this->parsedFiles[$fixingFile]['indent'] = $indent;
if (!$replacingVisitor->isComplete()) {
return null;
}
}

if (!$replacingVisitor->isComplete()) {
return null;
}
if (NewClassNamesVisitor::hasNewNames($replacingVisitor->getNewNodes())) {
if ($pathCloningVisitor !== null) {
// the visitors below change nodes in place, so the original nodes must not be shared
/** @var Stmt[] $newStmts */
$newStmts = (new NodeTraverser(new SharedNodeCloningVisitor()))->traverse($newStmts);
}
$nameResolver = new NameResolver(new Collecting(), ['replaceNodes' => false]);
/** @var Stmt[] $newStmts */
$newStmts = (new NodeTraverser(
$nameResolver,
new NewClassNamesVisitor($nameResolver->getNameContext(), $this->importNames, $this->preferSelf),
))->traverse($newStmts);
}

if (NewClassNamesVisitor::hasNewNames($replacingVisitor->getNewNodes())) {
$nameResolver = new NameResolver(new Collecting(), ['replaceNodes' => false]);
/** @var Stmt[] $newStmts */
$newStmts = (new NodeTraverser(
$nameResolver,
new NewClassNamesVisitor($nameResolver->getNameContext(), $this->importNames, $this->preferSelf),
))->traverse($newStmts);
}
$newCode = $this->getPrinter($indent)->printFormatPreserving($newStmts, $fileNodes, $parsedFile['tokens']);
if ($parsedFile['code'] === $newCode) {
return null;
}

$newCode = $this->getPrinter($indent)->printFormatPreserving($newStmts, $fileNodes, $parsedFile['tokens']);
if ($parsedFile['code'] === $newCode) {
return null;
return new FixedErrorDiff($fixingFile, $parsedFile['hash'], $this->diff($parsedFile['code'], $newCode));
} finally {
$pathCloningVisitor?->restoreOriginalNodes();
}

return new FixedErrorDiff($fixingFile, $parsedFile['hash'], $this->diff($parsedFile['code'], $newCode));
}

/**
Expand Down
141 changes: 141 additions & 0 deletions src/Fixable/PathCloningVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php declare(strict_types = 1);

namespace PHPStan\Fixable;

use Override;
use PhpParser\Node;
use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;
use function array_pop;

/**
* Clones only the nodes that a fix can change: the fixed nodes with all their
* children, and the parents of the fixed nodes. Each other node stays the
* original object and gets itself as its origNode attribute, so that
* PhpPrinter can copy its original code without printing its children.
*
* Call restoreOriginalNodes() after printing, because the original nodes
* belong to the analysed AST.
*/
final class PathCloningVisitor extends NodeVisitorAbstract
{

/** @var list<array{int, int}> */
private array $targetRanges;

/** @var array<string, true> */
private array $targetKeys = [];

private int $insideTarget = 0;

/** @var list<bool> */
private array $enteredTargets = [];

/** @var list<Node> */
private array $sharedNodes = [];

/**
* @param non-empty-list<Node> $targets
*/
public function __construct(array $targets)
{
$ranges = [];
foreach ($targets as $target) {
$ranges[] = [$target->getStartFilePos(), $target->getEndFilePos()];
$this->targetKeys[self::getKey($target)] = true;
}
$this->targetRanges = $ranges;
}

/**
* @param non-empty-list<Node> $targets
*/
public static function canFind(array $targets): bool
{
foreach ($targets as $target) {
if ($target->getStartFilePos() < 0 || $target->getEndFilePos() < 0) {
return false;
}
}

return true;
}

#[Override]
public function enterNode(Node $node): Node|int
{
if ($this->insideTarget > 0) {
$this->enteredTargets[] = false;
return $this->cloneNode($node);
}

if (!$this->containsTarget($node)) {
$this->enteredTargets[] = false;
$node->setAttribute('origNode', $node);
$this->sharedNodes[] = $node;
return NodeVisitor::DONT_TRAVERSE_CHILDREN;
}

$isTarget = isset($this->targetKeys[self::getKey($node)]);
$this->enteredTargets[] = $isTarget;
if ($isTarget) {
$this->insideTarget++;
}

return $this->cloneNode($node);
}

#[Override]
public function leaveNode(Node $node): ?Node
{
if (array_pop($this->enteredTargets) === true) {
$this->insideTarget--;
}

return null;
}

/**
* Removes the origNode attribute from the original nodes again.
*/
public function restoreOriginalNodes(): void
{
foreach ($this->sharedNodes as $node) {
$attributes = $node->getAttributes();
unset($attributes['origNode']);
$node->setAttributes($attributes);
}
$this->sharedNodes = [];
}

private function containsTarget(Node $node): bool
{
$start = $node->getStartFilePos();
$end = $node->getEndFilePos();
if ($start < 0 || $end < 0) {
return true;
}

foreach ($this->targetRanges as [$targetStart, $targetEnd]) {
if ($start <= $targetStart && $targetEnd <= $end) {
return true;
}
}

return false;
}

private function cloneNode(Node $node): Node
{
$clone = clone $node;
$clone->setAttribute('origNode', $node);

return $clone;
}

private static function getKey(Node $node): string
{
return $node::class . ':' . $node->getStartFilePos() . ':' . $node->getEndFilePos();
}

}
30 changes: 30 additions & 0 deletions src/Fixable/PhpPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,34 @@ protected function pCommaSeparated(array $nodes): string
return $result;
}

/**
* A node that is its own origNode was not cloned, so a fix did not change it
* (see PathCloningVisitor). Its original code is copied as it is, without
* printing its children one by one.
*/
#[Override]
protected function p(
Node $node,
int $precedence = self::MAX_PRECEDENCE,
int $lhsPrecedence = self::MAX_PRECEDENCE,
bool $parentFormatPreserved = false,
): string
{
if (
$parentFormatPreserved
&& $this->origTokens !== null
&& $node->getAttribute('origNode') === $node
) {
$startPos = $node->getStartTokenPos();
$endPos = $node->getEndTokenPos();
if ($startPos >= 0 && $endPos >= 0) {
$indentAdjustment = $this->indentLevel - $this->origTokens->getIndentationBefore($startPos);

return $this->origTokens->getTokenCode($startPos, $endPos + 1, $indentAdjustment);
}
}

return parent::p($node, $precedence, $lhsPrecedence, $parentFormatPreserved);
}

}
54 changes: 54 additions & 0 deletions src/Fixable/SharedNodeCloningVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Fixable;

use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use function array_pop;

/**
* Clones every node of an AST that PathCloningVisitor produced, so that later
* visitors can change nodes in place without changing the analysed AST.
*
* A clone keeps the origNode of the node it copies. In a subtree that
* PathCloningVisitor shared, the nodes have no origNode yet, so each clone
* gets the original node as its origNode, the same as CloningVisitor does.
*/
final class SharedNodeCloningVisitor extends NodeVisitorAbstract
{

private int $insideShared = 0;

/** @var list<bool> */
private array $enteredShared = [];

#[Override]
public function enterNode(Node $node): Node
{
$origNode = $node->getAttribute('origNode');
$isShared = $origNode === $node;
$this->enteredShared[] = $isShared;
if ($isShared) {
$this->insideShared++;
}

$clone = clone $node;
if ($this->insideShared > 0) {
$clone->setAttribute('origNode', $node);
}

return $clone;
}

#[Override]
public function leaveNode(Node $node): ?Node
{
if (array_pop($this->enteredShared) === true) {
$this->insideShared--;
}

return null;
}

}
Loading
Loading