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
18 changes: 18 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,24 @@ jobs:
../../bin/phpstan clear-result-cache
../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon"
../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon"
- script: |
cd e2e/bug-14993
# https://github.com/phpstan/phpstan/issues/14993
# An inline @phpstan-ignore must also suppress errors reported in the context of
# each class using the trait, with both an empty and a primed result cache.
../../bin/phpstan clear-result-cache
../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw"
../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw"
# With the ignore removed, the error is reported once per using class.
sed -i 's# // @phpstan-ignore function.alreadyNarrowedType##' src/CheckedTrait.php
../../bin/phpstan clear-result-cache
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw")
../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\First):13:Call to function is_subclass_of() with 'Bug14993\\\\First' and 'Countable' will always evaluate to true." "$OUTPUT"
../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\Second):13:Call to function is_subclass_of() with 'Bug14993\\\\Second' and 'Countable' will always evaluate to true." "$OUTPUT"
# A generated baseline suppresses both errors on re-run.
../../bin/phpstan --generate-baseline=baseline.neon
../../bin/phpstan clear-result-cache
../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon"
- script: |
cd e2e/result-cache-meta-extension
composer install
Expand Down
1 change: 1 addition & 0 deletions e2e/bug-14993/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/baseline.neon
4 changes: 4 additions & 0 deletions e2e/bug-14993/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: 8
paths:
- src
18 changes: 18 additions & 0 deletions e2e/bug-14993/src/CheckedTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Bug14993;

use Countable;
use RuntimeException;

trait CheckedTrait
{

public static function check(): void
{
if (!is_subclass_of(self::class, Countable::class)) { // @phpstan-ignore function.alreadyNarrowedType
throw new RuntimeException('not countable');
}
}

}
17 changes: 17 additions & 0 deletions e2e/bug-14993/src/First.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Bug14993;

use Countable;

class First implements Countable
{

use CheckedTrait;

public function count(): int
{
return 0;
}

}
17 changes: 17 additions & 0 deletions e2e/bug-14993/src/Second.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Bug14993;

use Countable;

class Second implements Countable
{

use CheckedTrait;

public function count(): int
{
return 0;
}

}
3 changes: 3 additions & 0 deletions e2e/bug-14993/with-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
includes:
- phpstan.neon
- baseline.neon
32 changes: 31 additions & 1 deletion src/Analyser/AnalyserResultFinalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Registry as RuleRegistry;
use Throwable;
use function array_key_exists;
use function array_merge;
use function count;
use function get_class;
use function sprintf;

/**
* @phpstan-import-type LinesToIgnore from FileAnalyserResult
*/
#[AutowiredService]
final class AnalyserResultFinalizer
{
Expand Down Expand Up @@ -120,7 +124,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
$collectorErrors = [];
$locallyIgnoredCollectorErrors = [];
foreach ($tempCollectorErrors as $tempCollectorError) {
$file = $tempCollectorError->getTraitFilePath() ?? $tempCollectorError->getFilePath();
$file = $this->resolveAnalysedFileWithLineIgnores($tempCollectorError, $allLinesToIgnore);
$linesToIgnore = $allLinesToIgnore[$file] ?? [];
$unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? [];
$localIgnoresProcessorResult = $this->localIgnoresProcessor->process(
Expand Down Expand Up @@ -159,6 +163,32 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
), $collectorErrors, $locallyIgnoredCollectorErrors);
}

/**
* Line ignores are keyed by the file whose analysis discovered them. For an error
* reported in a trait that's the file using the trait when the error keeps its trait
* context, and the trait file itself when the context was removed (Error::removeTraitContext())
* or when the trait was analysed on its own.
*
* @param array<string, LinesToIgnore> $allLinesToIgnore
*/
private function resolveAnalysedFileWithLineIgnores(Error $error, array $allLinesToIgnore): string
{
$traitFilePath = $error->getTraitFilePath();
if ($traitFilePath === null) {
return $error->getFilePath();
}

foreach ([$error->getFilePath(), $traitFilePath] as $analysedFile) {
if (!array_key_exists($error->getFile(), $allLinesToIgnore[$analysedFile] ?? [])) {
continue;
}

return $analysedFile;
}

return $traitFilePath;
}

private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult
{
return new AnalyserResult(
Expand Down
61 changes: 61 additions & 0 deletions tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,53 @@ public function testUnititializedReadonlyPropertyAccessedInTrait(): void
$this->assertSame($expectedFile, $error->getFile());
}

public function testIgnoreErrorsReportedInContextOfEachClassUsingTheTrait(): void
{
$errors = $this->runAnalyseAndFinalize([
__DIR__ . '/traits/ignore/IgnoreErrorsClasses.php',
__DIR__ . '/traits/ignore/IgnoreErrorsTrait.php',
]);
$this->assertNoErrors($errors);
}

public function testIgnoreErrorsReportedInContextOfClassesLivingInSeparateFiles(): void
{
$errors = $this->runAnalyseAndFinalize([
__DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesFirst.php',
__DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesSecond.php',
__DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesTrait.php',
]);
$this->assertNoErrors($errors);
}

public function testIgnoreErrorsReportedDirectlyInTraitUsedByASingleClass(): void
{
$errors = $this->runAnalyseAndFinalize([
__DIR__ . '/traits/ignore/IgnoreErrorsSingleClass.php',
__DIR__ . '/traits/ignore/IgnoreErrorsSingleTrait.php',
]);
$this->assertNoErrors($errors);
}

public function testIgnoreErrorsReportedInContextOfClassUsingNestedTrait(): void
{
$errors = $this->runAnalyseAndFinalize([
__DIR__ . '/traits/ignore/IgnoreErrorsNestedClasses.php',
__DIR__ . '/traits/ignore/IgnoreErrorsNestedTrait.php',
__DIR__ . '/traits/ignore/IgnoreErrorsNestedInnerTrait.php',
]);
$this->assertNoErrors($errors);
}

public function testIgnoreErrorsReportedInContextOfAnonymousClassUsingTrait(): void
{
$errors = $this->runAnalyseAndFinalize([
__DIR__ . '/traits/ignore/IgnoreErrorsAnonymousClasses.php',
__DIR__ . '/traits/ignore/IgnoreErrorsAnonymousTrait.php',
]);
$this->assertNoErrors($errors);
}

/**
* @param string[] $files
* @return Error[]
Expand All @@ -214,6 +261,20 @@ private function runAnalyse(array $files): array
return $analyser->analyse($files)->getErrors();
}

/**
* @param string[] $files
* @return list<Error>
*/
private function runAnalyseAndFinalize(array $files): array
{
$files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files);
/** @var Analyser $analyser */
$analyser = self::getContainer()->getByType(Analyser::class);
$finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class);

return $finalizer->finalize($analyser->analyse($files), false, true)->getErrors();
}

public static function getAdditionalConfigFiles(): array
{
return array_unique(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsAnonymous;

use Countable;

function createFirst(): Countable
{
return new class implements Countable {

use IgnoreErrorsAnonymousTrait;

public function count(): int
{
return 1;
}

};
}

function createSecond(): Countable
{
return new class implements Countable {

use IgnoreErrorsAnonymousTrait;

public function count(): int
{
return 2;
}

};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsAnonymous;

use Countable;
use RuntimeException;
use function is_subclass_of;

trait IgnoreErrorsAnonymousTrait
{

public function check(): void
{
/* @phpstan-ignore function.alreadyNarrowedType */
if (!is_subclass_of(static::class, Countable::class)) {
throw new RuntimeException('not countable');
}
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsClasses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrors;

use Countable;

class First implements Countable
{

use IgnoreErrorsTrait;

public function count(): int
{
return 0;
}

}

class Second implements Countable
{

use IgnoreErrorsTrait {
check as checkSecond;
}

public function count(): int
{
return 0;
}

}
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsNestedClasses.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsNested;

use Countable;

class NestedFirst implements Countable
{

use IgnoreErrorsNestedTrait;

public function count(): int
{
return 0;
}

}

class NestedSecond implements Countable
{

use IgnoreErrorsNestedTrait;

public function count(): int
{
return 0;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsNested;

use Countable;
use RuntimeException;
use function is_subclass_of;

trait IgnoreErrorsNestedInnerTrait
{

public static function check(): void
{
/* @phpstan-ignore function.alreadyNarrowedType */
if (!is_subclass_of(self::class, Countable::class)) {
throw new RuntimeException('not countable');
}
}

public static function compare(): bool
{
/** @phpstan-ignore identical.alwaysFalse */
return self::class === 'TraitsIgnoreErrorsNested\Nonexistent';
}

}
10 changes: 10 additions & 0 deletions tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsNestedTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsNested;

trait IgnoreErrorsNestedTrait
{

use IgnoreErrorsNestedInnerTrait;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace TraitsIgnoreErrorsSeparate;

use Countable;

class SeparateFirst implements Countable
{

use IgnoreErrorsSeparateFilesTrait;

public function count(): int
{
return 0;
}

}
Loading
Loading