Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector\Fixture;

use Rector\Tests\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector\Source\ImplementedInterface;

final class WithInterface implements ImplementedInterface
{
public function trample()
Expand All @@ -13,27 +17,21 @@ final class WithInterface implements ImplementedInterface
}
}

interface ImplementedInterface
{
public function go();
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector\Fixture;

use Rector\Tests\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector\Source\ImplementedInterface;

final class WithInterface implements ImplementedInterface
{
public function go()
{
}
}

interface ImplementedInterface
{
public function go();
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector\Source;

interface ImplementedInterface
{
public function go();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,23 @@
*/
final class SkipUsedTrackingTest extends AbstractRectorTestCase
{
public function testMarksSkipUsedOnlyWhenRuleWouldChangeFile(): void
public function testMarksSkipUsedForEveryMatchedFile(): void
{
// file uses OldClass → rule would rename it → its skip is what prevents the change
$this->doTestFile(__DIR__ . '/FixtureSkipUsedTracking/skip_used_renames_old_class.php.inc');

// doTestFile() only cleans up the last processed temp file, so remove this one explicitly
FileSystem::delete(__DIR__ . '/FixtureSkipUsedTracking/skip_used_renames_old_class.php');

// file does not use OldClass → rule would not change it → its skip is unnecessary
$this->doTestFile(__DIR__ . '/FixtureSkipUsedTracking/skip_unused_no_old_class.php.inc');

$skipper = $this->make(Skipper::class);
$usedSkips = $skipper->provideUsedSkips();

$usedPaths = $usedSkips[RenameClassRector::class] ?? [];

// both skip masks match a processed file, so both are marked used
// the rule is skipped once per file whose path matches, so every matched mask is marked used
$this->assertContains('*skip_used_renames_old_class*', $usedPaths);

// the skip on a file the rule would not have touched is never marked used
$this->assertNotContains('*skip_unused_no_old_class*', $usedPaths);
$this->assertContains('*skip_unused_no_old_class*', $usedPaths);
}

public function provideConfigFilePath(): string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
OldClass::class => NewClass::class,
]);

// both paths are skipped, but only the first file actually uses OldClass, so only its skip
// prevents a real change - the second one would not have changed anyway
// both paths are skipped; the rule is skipped once per matching file, so both masks are marked used
$rectorConfig->skip([
RenameClassRector::class => ['*skip_used_renames_old_class*', '*skip_unused_no_old_class*'],
]);
Expand Down
3 changes: 2 additions & 1 deletion rules/Arguments/ArgumentDefaultValueReplacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,9 @@ private function processArgs(
&& str_contains($replaceArgumentDefaultValue->getValueAfter(), '::')) {
[$targetClass, $targetConstant] = explode('::', $replaceArgumentDefaultValue->getValueAfter());
$type = $this->nodeTypeResolver->getType($particularArg->value->class);
// already the target constant, possibly via self::/subclass that inherits it → no-op
if ($type instanceof FullyQualifiedObjectType
&& $type->getClassName() === $targetClass
&& ($type->getClassName() === $targetClass || $type->isInstanceOf($targetClass)->yes())
&& $particularArg->value->name instanceof Identifier
&& $particularArg->value->name->toString() === $targetConstant) {
return null;
Expand Down
25 changes: 25 additions & 0 deletions rules/Php81/NodeManipulator/NullToStrictStringIntConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public function convertIfNull(
return $funcCall;
}

// already cast to the target type on a previous run, re-casting would nest a redundant cast
if ($this->isAlreadyCastToTargetType($argValue, $targetType)) {
return null;
}

$wrapInParentheses = false;
if ($argValue instanceof Ternary && $argValue->cond instanceof CastString_) {
$wrapInParentheses = true;
Expand All @@ -118,6 +123,26 @@ public function convertIfNull(
return $funcCall;
}

/**
* Structural check (immune to stale resolved types on re-printed nodes): the value is already a
* cast/literal of the target type, or a ternary whose both branches are.
*/
private function isAlreadyCastToTargetType(Expr $expr, string $targetType): bool
{
if ($expr instanceof Ternary) {
// short ternary "A ?: B" keeps the left value in cond; full ternary uses if
$left = $expr->if ?? $expr->cond;
return $this->isAlreadyCastToTargetType($left, $targetType)
&& $this->isAlreadyCastToTargetType($expr->else, $targetType);
}

if ($targetType === 'string') {
return $expr instanceof CastString_ || $expr instanceof String_ || $expr instanceof InterpolatedString;
}

return $expr instanceof CastInt_ || $expr instanceof Int_;
}

private function shouldSkipValue(Expr $expr, Scope $scope, bool $isTrait, string $targetType): bool
{
// array dim fetch value is mixed, null is not known - skip to avoid wrong (string) cast
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ public function refactor(Node $node): ?Node
return null;
}

$scope = $node->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
}

$args = $node->getArgs();
$position = $this->argsAnalyzer->resolveArgPosition($args, 'limit', 2);

if (! isset($args[$position])) {
return null;
}

$scope = $node->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof Scope) {
return null;
}

$classReflection = $scope->getClassReflection();
$isTrait = $classReflection instanceof ClassReflection && $classReflection->isTrait();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,6 @@ public function configure(array $configuration): void

private function hasMatchingMethodName(ClassMethod $classMethod): bool
{
foreach ($this->methodVisibilities as $methodVisibility) {
if ($this->isName($classMethod, $methodVisibility->getMethod())) {
return true;
}
}

return false;
return array_any($this->methodVisibilities, fn(ChangeMethodVisibility $methodVisibility): bool => $this->isName($classMethod, $methodVisibility->getMethod()));
}
}
2 changes: 1 addition & 1 deletion src/Application/FileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function processFile(File $file, Configuration $configuration): FileProce
$file->changeHasChanged(false);

// 1. change nodes with Rector Rules
$newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts());
$newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts(), $file);

// 2. apply post rectors
$postNewStmts = $this->postFileProcessor->traverse($newStmts, $file);
Expand Down
18 changes: 18 additions & 0 deletions src/Application/RectorRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use PhpParser\Node;
use Rector\Configuration\ConfigurationRuleFilter;
use Rector\Contract\Rector\RectorInterface;
use Rector\Skipper\Skipper\Skipper;
use Rector\ValueObject\Application\File;
use Rector\VersionBonding\ComposerPackageConstraintFilter;
use Rector\VersionBonding\PhpVersionedFilter;

Expand Down Expand Up @@ -34,6 +36,7 @@ public function __construct(
private readonly PhpVersionedFilter $phpVersionedFilter,
private readonly ComposerPackageConstraintFilter $composerPackageConstraintFilter,
private readonly ConfigurationRuleFilter $configurationRuleFilter,
private readonly Skipper $skipper,
private array $rectors,
) {
}
Expand Down Expand Up @@ -75,6 +78,21 @@ public function forNode(Node $node): array
return $this->cache[$nodeClass];
}

/**
* @return RectorInterface[]
*/
public function forNodeAndFile(Node $node, File $file): array
{
$rectors = $this->forNode($node);

$filePath = $file->getFilePath();

return array_filter(
$rectors,
fn (RectorInterface $rector): bool => ! $this->skipper->shouldSkipRectorAndFile($rector, $filePath)
);
}

/**
* Filtering must happen lazily, after the configuration is set in ProcessCommand::execute(),
* otherwise options like --only are not yet available and get ignored.
Expand Down
63 changes: 0 additions & 63 deletions src/NodeDecorator/CreatedByRuleDecorator.php

This file was deleted.

20 changes: 11 additions & 9 deletions src/PhpParser/NodeTraverser/RectorNodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PhpParser\NodeVisitor;
use Rector\Application\RectorRegistry;
use Rector\Rector\RectorRunner;
use Rector\ValueObject\Application\File;

final class RectorNodeTraverser
{
Expand All @@ -26,19 +27,19 @@ public function __construct(
* @param Node[] $nodes
* @return Node[]
*/
public function traverse(array $nodes): array
public function traverse(array $nodes, File $file): array
{
$this->stopTraversal = false;

return $this->traverseArray($nodes);
return $this->traverseArray($nodes, $file);
}

private function traverseNode(Node $node): void
private function traverseNode(Node $node, File $file): void
{
foreach ($node->getSubNodeNames() as $name) {
$subNode = $node->{$name};
if (\is_array($subNode)) {
$node->{$name} = $this->traverseArray($subNode);
$node->{$name} = $this->traverseArray($subNode, $file);
if ($this->stopTraversal) {
break;
}
Expand All @@ -50,7 +51,7 @@ private function traverseNode(Node $node): void
continue;
}

$currentNodeVisitors = $this->rectorRegistry->forNode($subNode);
$currentNodeVisitors = $this->rectorRegistry->forNodeAndFile($subNode, $file);

foreach ($currentNodeVisitors as $currentNodeVisitor) {
$return = $this->rectorRunner->run($currentNodeVisitor, $subNode);
Expand All @@ -74,7 +75,8 @@ private function traverseNode(Node $node): void
}
}

$this->traverseNode($subNode);
$this->traverseNode($subNode, $file);

if ($this->stopTraversal) {
break;
}
Expand All @@ -85,7 +87,7 @@ private function traverseNode(Node $node): void
* @param array<Node|null> $nodes The null can be in case of empty list(, , )
* @return Node[]
*/
private function traverseArray(array $nodes): array
private function traverseArray(array $nodes, File $file): array
{
$doNodes = [];
foreach ($nodes as $i => $node) {
Expand All @@ -97,7 +99,7 @@ private function traverseArray(array $nodes): array
continue;
}

$currentNodeVisitors = $this->rectorRegistry->forNode($node);
$currentNodeVisitors = $this->rectorRegistry->forNodeAndFile($node, $file);

foreach ($currentNodeVisitors as $currentNodeVisitor) {
$return = $this->rectorRunner->run($currentNodeVisitor, $node);
Expand All @@ -123,7 +125,7 @@ private function traverseArray(array $nodes): array
}
}

$this->traverseNode($node);
$this->traverseNode($node, $file);
if ($this->stopTraversal) {
break;
}
Expand Down
Loading
Loading