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
9 changes: 5 additions & 4 deletions e2e/parallel-unused-skips/custom/config/rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@

$rectorConfig->reportUnusedSkips();

// two concrete paths under one rule: first matched in the worker (proves parallel aggregation),
// second never matches and must be reported per-path as unused
// two rule-scoped masks: first matched in the worker (proves parallel aggregation),
// second never matches and must be reported per-path as unused.
// a global mask "*/NonexistentGlobal/*" would NOT be reported, only rule-scoped ones are
$rectorConfig->skip([
SimplifyUselessVariableRector::class => [
dirname(__DIR__, 2) . '/src/SomeClass.php',
dirname(__DIR__, 2) . '/src/NonexistentUnused.php',
'*/src/*',
'*/NonexistentUnused/*',
],
]);
};
2 changes: 1 addition & 1 deletion e2e/parallel-unused-skips/expected-output.diff
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
[WARNING] This skip is unused, it never matched any element. You can remove it
from "->withSkip()"

* ./parallel-unused-skips/src/NonexistentUnused.php
* */NonexistentUnused/*
15 changes: 8 additions & 7 deletions src/Reporting/MissConfigurationReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ public function reportUnusedSkips(ProcessResult $processResult): void
return;
}

// only concrete path-scoped skips are trackable at runtime; skip-everywhere rule skips
// (null path) are forgotten from the container at boot, so they never reach the skipper
// rule-scoped path skips are trackable at runtime; skip-everywhere rule skips
// (null path) are forgotten from the container at boot, so they never reach the skipper.
// rule-scoped paths are intentional, so they are reported even as mask paths
$skippedClassPaths = [];
foreach ($this->skippedClassResolver->resolve() as $paths) {
if ($paths === null) {
Expand All @@ -47,14 +48,14 @@ public function reportUnusedSkips(ProcessResult $processResult): void
$skippedClassPaths = [...$skippedClassPaths, ...$paths];
}

$configuredSkips = [...$skippedClassPaths, ...$this->skippedPathsResolver->resolve()];

// skip mask paths like "*/some/*"; they are hard to spot and report false positives
$configuredSkips = array_filter(
$configuredSkips,
// global mask paths like "*/some/*" are hard to spot and report false positives, skip them
$globalPaths = array_filter(
$this->skippedPathsResolver->resolve(),
static fn (string $skip): bool => ! str_contains($skip, '*')
);

$configuredSkips = [...$skippedClassPaths, ...$globalPaths];

$unusedSkips = array_values(array_diff($configuredSkips, $processResult->getUsedSkips()));
if ($unusedSkips === []) {
return;
Expand Down
3 changes: 3 additions & 0 deletions src/Skipper/Matcher/FileInfoMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use Rector\Skipper\Fnmatcher;
use Rector\Skipper\RealpathMatcher;

/**
* @see \Rector\Tests\Skipper\Matcher\FileInfoMatcherTest
*/
final readonly class FileInfoMatcher
{
public function __construct(
Expand Down
35 changes: 18 additions & 17 deletions tests/Reporting/MissConfigurationReporterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@

final class MissConfigurationReporterTest extends AbstractLazyTestCase
{
private const string UNUSED_ENTITY_PATH = '/app/bundles/UserBundle/Entity';
private const string UNUSED_RULE_MASK = '*/dead-rule/*';

private const string USED_ENTITY_PATH = '/app/bundles/AdminBundle/Entity';
private const string USED_RULE_MASK = '*/matched-rule/*';

private const string MASK_PATH = '*/SomeMask/*';
private const string GLOBAL_MASK = '*/global-mask/*';

private BufferedOutput $bufferedOutput;

Expand All @@ -48,12 +48,12 @@ protected function setUp(): void
SimpleParameterProvider::setParameter(Option::SKIP, [
// skip-everywhere rule skip - forgotten at boot, not trackable, must be excluded
ThreeMan::class,
// concrete path-scoped class skip, never matched - must be reported
FifthElement::class => [self::UNUSED_ENTITY_PATH],
// concrete path-scoped class skip, matched - must not be reported
AnotherClassToSkip::class => [self::USED_ENTITY_PATH],
// mask path skip - hard to spot, false-positive prone, must not be reported
self::MASK_PATH,
// rule-scoped path skip, never matched - intentional, must be reported even as mask
FifthElement::class => [self::UNUSED_RULE_MASK],
// rule-scoped path skip, matched - must not be reported
AnotherClassToSkip::class => [self::USED_RULE_MASK],
// global mask path skip - hard to spot, false-positive prone, must not be reported
self::GLOBAL_MASK,
]);
}

Expand All @@ -67,20 +67,21 @@ public function testReportsOnlyTrackableUnusedSkips(): void
{
SimpleParameterProvider::setParameter(Option::REPORT_UNUSED_SKIPS, true);

// matched path is marked used by its concrete path, not by the rule class
$processResult = new ProcessResult([], [], 0, [self::USED_ENTITY_PATH]);
// matched rule-scoped path is marked used by its path, not by the rule class
$processResult = new ProcessResult([], [], 0, [self::USED_RULE_MASK]);
$this->missConfigurationReporter->reportUnusedSkips($processResult);

$output = $this->bufferedOutput->fetch();

// unused concrete path is reported by path, not rule class
$this->assertStringContainsString('UserBundle', $output);
// unused rule-scoped path is reported by path, not rule class
$this->assertStringContainsString('dead-rule', $output);
$this->assertStringNotContainsString('FifthElement', $output);

// matched concrete path is excluded
$this->assertStringNotContainsString('AdminBundle', $output);
// matched rule-scoped path is excluded
$this->assertStringNotContainsString('matched-rule', $output);

// mask path is excluded (hard to spot, false-positive prone)
$this->assertStringNotContainsString('SomeMask', $output);
// global mask path is excluded (hard to spot, false-positive prone)
$this->assertStringNotContainsString('global-mask', $output);

// skip-everywhere rule skip is excluded (not trackable at runtime)
$this->assertStringNotContainsString('ThreeMan', $output);
Expand Down
41 changes: 41 additions & 0 deletions tests/Skipper/Matcher/FileInfoMatcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Skipper\Matcher;

use Rector\Skipper\Matcher\FileInfoMatcher;
use Rector\Testing\PHPUnit\AbstractLazyTestCase;

final class FileInfoMatcherTest extends AbstractLazyTestCase
{
private FileInfoMatcher $fileInfoMatcher;

protected function setUp(): void
{
parent::setUp();

$this->fileInfoMatcher = $this->make(FileInfoMatcher::class);
}

public function testMatchPatternReturnsTheOriginalMatchedPattern(): void
{
$matchedPattern = $this->fileInfoMatcher->matchPattern('/project/src/Foo.php', ['*/tests/*', '*/src/*']);

// the original, un-normalized pattern is returned, so callers can report the exact path
$this->assertSame('*/src/*', $matchedPattern);
}

public function testMatchPatternReturnsNullWhenNoPatternMatches(): void
{
$matchedPattern = $this->fileInfoMatcher->matchPattern('/project/src/Foo.php', ['*/tests/*']);

$this->assertNull($matchedPattern);
}

public function testDoesFileInfoMatchPatternsStillReportsBoolean(): void
{
$this->assertTrue($this->fileInfoMatcher->doesFileInfoMatchPatterns('/project/src/Foo.php', ['*/src/*']));
$this->assertFalse($this->fileInfoMatcher->doesFileInfoMatchPatterns('/project/src/Foo.php', ['*/tests/*']));
}
}
27 changes: 27 additions & 0 deletions tests/Skipper/Skipper/UsedSkipCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Rector\Skipper\Skipper\UsedSkipCollector;
use Rector\Testing\PHPUnit\AbstractLazyTestCase;
use Rector\Tests\Skipper\Skipper\Fixture\Element\FifthElement;
use Rector\Tests\Skipper\Skipper\Source\AnotherClassToSkip;

final class UsedSkipCollectorTest extends AbstractLazyTestCase
{
Expand Down Expand Up @@ -63,4 +64,30 @@ public function testCollectsOnlyMatchedSkips(): void
// unmatched skip is never collected
$this->assertNotContains(self::UNUSED_SKIP_MARKER, $usedSkips);
}

public function testCollectsMatchedPathNotRuleClassForRuleScopedSkip(): void
{
SimpleParameterProvider::setParameter(Option::SKIP, [
AnotherClassToSkip::class => [
// matched path
'*/someDirectory/*',
// sibling path that never matches
self::UNUSED_SKIP_MARKER,
],
]);

$this->skipper->shouldSkipElementAndFilePath(
AnotherClassToSkip::class,
__DIR__ . '/Fixture/someDirectory/anotherFile.php'
);

$usedSkips = $this->usedSkipCollector->provide();

// the specific matched path is collected, not the rule class
$this->assertContains('*/someDirectory/*', $usedSkips);
$this->assertNotContains(AnotherClassToSkip::class, $usedSkips);

// the unmatched sibling path under the same rule is never collected
$this->assertNotContains(self::UNUSED_SKIP_MARKER, $usedSkips);
}
}
Loading