diff --git a/bin/coverage-guard b/bin/coverage-guard index 332b3fd..03f24d0 100755 --- a/bin/coverage-guard +++ b/bin/coverage-guard @@ -2,6 +2,7 @@ createForHostVersion(); $stopwatch = new Stopwatch(); - $coverageGuard = new CoverageGuard($stderrPrinter, $phpParser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); + $fileTraverser = new FileTraverser($phpParser); + $coverageGuard = new CoverageGuard($stderrPrinter, $fileTraverser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); $errorFormatter = new ErrorFormatter($pathHelper, $stdoutPrinter); $registry = new CommandRegistry(); diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 6116ed0..cfcaec1 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -4,6 +4,7 @@ use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; $config = new Configuration(); +$config->ignoreUnknownClasses(['PhpParser\Node\Stmt\Throw_']); // old version of php-parser $config->ignoreErrorsOnPackage('phpunit/php-code-coverage', [ErrorType::DEV_DEPENDENCY_IN_PROD]); // optional dependency to load .cov files $config->ignoreErrorsOnPackage('sebastian/diff', [ErrorType::DEV_DEPENDENCY_IN_PROD]); // optional dependency to parse patch files $config->ignoreErrorsOnExtension('ext-tokenizer', [ErrorType::SHADOW_DEPENDENCY]); // optional dependency to have syntax highlighting diff --git a/coverage-guard.php b/coverage-guard.php index 0381446..1c106ff 100644 --- a/coverage-guard.php +++ b/coverage-guard.php @@ -1,6 +1,7 @@ addExecutableLineExcluder(new IgnoreThrowNewExceptionLineExcluder([LogicException::class])); + $localConfig = __DIR__ . '/coverage-guard.local.php'; if (is_file($localConfig)) { require $localConfig; // handy for $config->setEditorUrl() diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f26dea6..fa59963 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -60,6 +60,7 @@ parameters: ShipMonk\CoverageGuard\Rule\CoverageRule: Rule ShipMonk\CoverageGuard\Writer\CoverageWriter: CoverageWriter ShipMonk\CoverageGuard\Extractor\CoverageExtractor: Extractor + ShipMonk\CoverageGuard\Excluder\ExecutableLineExcluder: LineExcluder ignoreErrors: # allow calling internal methods from SebastianBergmann\CodeCoverage @@ -75,6 +76,13 @@ parameters: paths: - src/Utils/PatchParser.php + # support even old nikic/php-parser + - + message: '#PhpParser\\Node\\Stmt\\Throw_#' + identifier: class.notFound + paths: + - src/Excluder/IgnoreThrowNewExceptionLineExcluder.php + # do not track checked exceptions in tests - identifiers: diff --git a/src/Ast/FileTraverser.php b/src/Ast/FileTraverser.php new file mode 100644 index 0000000..16a9834 --- /dev/null +++ b/src/Ast/FileTraverser.php @@ -0,0 +1,57 @@ + $fileLines + * + * @throws ErrorException + */ + public function traverse( + string $file, + array $fileLines, + CodeBlockAnalyser $analyser, + ): void + { + $nameResolver = new NameResolver(); + + $nameResolvingTraverser = new NodeTraverser(); + $nameResolvingTraverser->addVisitor($nameResolver); + + $analyserTraverser = new NodeTraverser(); + $analyserTraverser->addVisitor($analyser); + + try { + /** @throws ParseError */ + $ast = $this->phpParser->parse(implode(PHP_EOL, $fileLines)); + } catch (ParseError $e) { + throw new ErrorException("Failed to parse PHP code in file {$file}: {$e->getMessage()}", $e); + } + + if ($ast === null) { + throw new LogicException("Failed to parse PHP code in file {$file}. Should never happen as Throwing error handler is used."); + } + + $analyserTraverser->traverse($nameResolvingTraverser->traverse($ast)); + } + +} diff --git a/src/CodeBlockAnalyser.php b/src/CodeBlockAnalyser.php index 56b8451..ad1b69b 100644 --- a/src/CodeBlockAnalyser.php +++ b/src/CodeBlockAnalyser.php @@ -7,35 +7,49 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeVisitorAbstract; +use ShipMonk\CoverageGuard\Excluder\ExecutableLineExcluder; use ShipMonk\CoverageGuard\Hierarchy\ClassMethodBlock; use ShipMonk\CoverageGuard\Hierarchy\LineOfCode; use ShipMonk\CoverageGuard\Report\ReportedError; use ShipMonk\CoverageGuard\Rule\CoverageRule; use ShipMonk\CoverageGuard\Rule\InspectionContext; -use function assert; +use function array_pop; +use function end; use function range; final class CodeBlockAnalyser extends NodeVisitorAbstract { - private ?string $currentClass = null; - - private ?string $currentMethod = null; + /** + * Anonymous classes can be nested + * + * @var list + */ + private array $currentClassStack = []; - private bool $inAnonymousClass = false; + /** + * Anonymous classes can cause nested methods + * + * @var list + */ + private array $currentMethodStack = []; /** * @var list */ private array $reportedErrors = []; - private InspectionContext $context; + /** + * @var array + */ + private array $excludedLines = []; /** * @param array $linesChanged line => line * @param array $linesCoverage executable_line => hits * @param array $linesContents * @param list $rules + * @param list $excluders */ public function __construct( private readonly bool $patchMode, @@ -44,70 +58,74 @@ public function __construct( private readonly array $linesCoverage, private readonly array $linesContents, private readonly array $rules, + private readonly array $excluders, ) { - $this->updateContext(); } public function enterNode(Node $node): ?int { - if ($node instanceof ClassLike) { - if ($node->name === null) { - $this->inAnonymousClass = true; - } else { - assert($node->namespacedName !== null); // using NameResolver - $this->currentClass = $node->namespacedName->toString(); - $this->updateContext(); + foreach ($this->excluders as $excluder) { + $excludedExecutableLineRange = $excluder->getExcludedLineRange($node); + if ($excludedExecutableLineRange !== null) { + foreach (range($excludedExecutableLineRange->getStart(), $excludedExecutableLineRange->getEnd()) as $excludedLine) { + $this->excludedLines[$excludedLine] = $excludedLine; + } } } - if ($node instanceof ClassMethod && $node->stmts !== null) { - if ($this->inAnonymousClass) { - return null; // ClassMethodBlock is emitted only for real methods - } - if ($this->currentClass === null) { - throw new LogicException('Found class method without a class, should never happen'); - } + if ($node instanceof ClassLike) { + $this->currentClassStack[] = $node->namespacedName?->toString(); + } + + if ($node instanceof ClassMethod) { + $this->currentMethodStack[] = $node->name->name; + } + + return null; + } + public function leaveNode(Node $node): mixed + { + if ($node instanceof ClassMethod && $node->stmts !== null) { + $currentClass = end($this->currentClassStack) !== false ? end($this->currentClassStack) : null; + $currentMethod = end($this->currentMethodStack) !== false ? end($this->currentMethodStack) : null; $startLine = $node->name->getStartLine(); $endLine = $node->getEndLine(); - $methodName = $node->name->toString(); $lines = $this->getLines($startLine, $endLine); if ($lines === []) { - return null; + $classStr = $currentClass ?? 'unknown'; + $methodStr = $currentMethod ?? 'unknown'; + throw new LogicException("Class method '{$classStr}::{$methodStr}' has no executable lines although it has some statements"); } $block = new ClassMethodBlock( $node, $lines, ); - - $this->currentMethod = $methodName; - $this->updateContext(); + $context = new InspectionContext( + className: $currentClass, + methodName: $currentMethod, + filePath: $this->filePath, + patchMode: $this->patchMode, + ); if ($this->patchMode && $block->getChangedLinesCount() === 0) { return null; // unchanged methods not passed to rules in patch mode } - foreach ($this->inspectCodeBlock($block) as $reportedError) { + foreach ($this->inspectCodeBlock($block, $context) as $reportedError) { $this->reportedErrors[] = $reportedError; } - - return null; } - return null; - } - - public function leaveNode(Node $node): mixed - { if ($node instanceof ClassLike) { - if ($node->name !== null) { - $this->currentClass = null; - } else { - $this->inAnonymousClass = false; - } + array_pop($this->currentClassStack); + } + + if ($node instanceof ClassMethod) { + array_pop($this->currentMethodStack); } return null; @@ -130,6 +148,7 @@ private function getLines( $executableLines[] = new LineOfCode( number: $lineNumber, executable: isset($this->linesCoverage[$lineNumber]), + excluded: isset($this->excludedLines[$lineNumber]), covered: isset($this->linesCoverage[$lineNumber]) && $this->linesCoverage[$lineNumber] > 0, changed: isset($this->linesChanged[$lineNumber]), contents: $this->linesContents[$lineNumber], @@ -141,11 +160,14 @@ private function getLines( /** * @return list */ - private function inspectCodeBlock(ClassMethodBlock $block): array + private function inspectCodeBlock( + ClassMethodBlock $block, + InspectionContext $context, + ): array { $reportedErrors = []; foreach ($this->rules as $rule) { - $coverageError = $rule->inspect($block, $this->context); + $coverageError = $rule->inspect($block, $context); if ($coverageError !== null) { $reportedErrors[] = new ReportedError($this->filePath, $block, $coverageError); @@ -163,14 +185,4 @@ public function getReportedErrors(): array return $this->reportedErrors; } - private function updateContext(): void - { - $this->context = new InspectionContext( - className: $this->currentClass, - methodName: $this->currentMethod, - filePath: $this->filePath, - patchMode: $this->patchMode, - ); - } - } diff --git a/src/Command/PatchCoverageCommand.php b/src/Command/PatchCoverageCommand.php index a6eac4c..4336b0a 100644 --- a/src/Command/PatchCoverageCommand.php +++ b/src/Command/PatchCoverageCommand.php @@ -79,7 +79,7 @@ public function __invoke( $this->stdoutPrinter->printLine('Patch Coverage Statistics:'); $this->stdoutPrinter->printLine(''); - $this->stdoutPrinter->printLine(" Changed executable lines: {$totalChangedLines}"); + $this->stdoutPrinter->printLine(" Changed executable lines: {$totalChangedLines}"); // TODO excluders should be used $this->stdoutPrinter->printLine(" Covered lines: {$totalCoveredLines}"); $this->stdoutPrinter->printLine(' Uncovered lines: ' . ($totalChangedLines - $totalCoveredLines) . ''); $this->stdoutPrinter->printLine(" Coverage: {$percentageFormatted}%"); diff --git a/src/Config.php b/src/Config.php index 097e83c..c72f3b3 100644 --- a/src/Config.php +++ b/src/Config.php @@ -3,6 +3,7 @@ namespace ShipMonk\CoverageGuard; use ShipMonk\CoverageGuard\Exception\ErrorException; +use ShipMonk\CoverageGuard\Excluder\ExecutableLineExcluder; use ShipMonk\CoverageGuard\Rule\CoverageRule; use ShipMonk\CoverageGuard\Utils\FileUtils; use function file_exists; @@ -29,6 +30,11 @@ final class Config */ private array $rules = []; + /** + * @var list + */ + private array $excluders = []; + private ?string $editorUrl = null; public function __construct() @@ -81,6 +87,15 @@ public function addRule(CoverageRule $rule): self return $this; } + /** + * Allows you to ignore/exclude certain executable lines from coverage calculations. + */ + public function addExecutableLineExcluder(ExecutableLineExcluder $excluder): self + { + $this->excluders[] = $excluder; + return $this; + } + /** * Set the editor URL pattern to make filepaths clickable in CLI output via OSC 8 hyperlink * @@ -113,6 +128,14 @@ public function getCoveragePathMapping(): array return $this->coveragePathMapping; } + /** + * @return list + */ + public function getExecutableLineExcluders(): array + { + return $this->excluders; + } + /** * @return list */ diff --git a/src/CoverageGuard.php b/src/CoverageGuard.php index 33ca22f..bbade4a 100644 --- a/src/CoverageGuard.php +++ b/src/CoverageGuard.php @@ -2,14 +2,11 @@ namespace ShipMonk\CoverageGuard; -use LogicException; -use PhpParser\Error as ParseError; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitor\NameResolver; -use PhpParser\Parser as PhpParser; +use ShipMonk\CoverageGuard\Ast\FileTraverser; use ShipMonk\CoverageGuard\Coverage\ExecutableLine; use ShipMonk\CoverageGuard\Coverage\FileCoverage; use ShipMonk\CoverageGuard\Exception\ErrorException; +use ShipMonk\CoverageGuard\Excluder\ExecutableLineExcluder; use ShipMonk\CoverageGuard\Report\CoverageReport; use ShipMonk\CoverageGuard\Report\ReportedError; use ShipMonk\CoverageGuard\Rule\CoverageRule; @@ -22,16 +19,14 @@ use function array_keys; use function array_map; use function count; -use function implode; use function range; -use const PHP_EOL; final class CoverageGuard { public function __construct( private readonly Printer $printer, - private readonly PhpParser $phpParser, + private readonly FileTraverser $fileTraverser, private readonly PathHelper $pathHelper, private readonly PatchParser $patchParser, private readonly CoverageProvider $coverageProvider, @@ -64,6 +59,7 @@ public function checkCoverage( $rules[] = new EnforceCoverageForMethodsRule(minExecutableLines: 5); } + $excluders = $config->getExecutableLineExcluders(); $analysedFiles = []; $reportedErrors = []; @@ -91,7 +87,7 @@ public function checkCoverage( $this->printer->printLine("{$relativePath} - $coveragePercentage%"); } - foreach ($this->getReportedErrors($rules, $patchMode, $file, $changedLinesOrNull, $fileCoverage) as $reportedError) { + foreach ($this->getReportedErrors($rules, $excluders, $patchMode, $file, $changedLinesOrNull, $fileCoverage) as $reportedError) { $reportedErrors[] = $reportedError; } } @@ -103,6 +99,7 @@ public function checkCoverage( /** * @param list $rules + * @param list $excluders * @param list|null $linesChanged * @return list * @@ -110,6 +107,7 @@ public function checkCoverage( */ private function getReportedErrors( array $rules, + array $excluders, bool $patchMode, string $file, ?array $linesChanged, @@ -119,7 +117,6 @@ private function getReportedErrors( $codeLines = FileUtils::readFileLines($file); $lineNumbers = range(1, count($codeLines)); - $nameResolver = new NameResolver(); $linesChangedMap = $linesChanged === null ? array_combine($lineNumbers, $lineNumbers) : array_combine($linesChanged, $linesChanged); @@ -131,25 +128,11 @@ private function getReportedErrors( $linesContents = array_combine($lineNumbers, $codeLines); - $extractor = new CodeBlockAnalyser($patchMode, $file, $linesChangedMap, $linesCoverage, $linesContents, $rules); - $traverser = new NodeTraverser(); - $traverser->addVisitor($nameResolver); - $traverser->addVisitor($extractor); + $analyser = new CodeBlockAnalyser($patchMode, $file, $linesChangedMap, $linesCoverage, $linesContents, $rules, $excluders); - try { - /** @throws ParseError */ - $ast = $this->phpParser->parse(implode(PHP_EOL, $codeLines)); - } catch (ParseError $e) { - throw new ErrorException("Failed to parse PHP code in file {$file}: {$e->getMessage()}", $e); - } - - if ($ast === null) { - throw new LogicException("Failed to parse PHP code in file {$file}. Should never happen as Throwing error handler is used."); - } - - $traverser->traverse($ast); + $this->fileTraverser->traverse($file, $codeLines, $analyser); - return $extractor->getReportedErrors(); + return $analyser->getReportedErrors(); } } diff --git a/src/Excluder/ExcludedLineRange.php b/src/Excluder/ExcludedLineRange.php new file mode 100644 index 0000000..bfff44e --- /dev/null +++ b/src/Excluder/ExcludedLineRange.php @@ -0,0 +1,39 @@ + $end) { + throw new LogicException('Start must be less than or equal to end.'); + } + if ($start < 1) { + throw new LogicException('Start must be greater than or equal to 1.'); + } + if ($end < 1) { + throw new LogicException('End must be greater than or equal to 1.'); + } + } + + public function getStart(): int + { + return $this->start; + } + + public function getEnd(): int + { + return $this->end; + } + +} diff --git a/src/Excluder/ExecutableLineExcluder.php b/src/Excluder/ExecutableLineExcluder.php new file mode 100644 index 0000000..7d39e62 --- /dev/null +++ b/src/Excluder/ExecutableLineExcluder.php @@ -0,0 +1,19 @@ + $classNames + */ + public function __construct( + private readonly array $classNames, + ) + { + } + + public function getExcludedLineRange(Node $node): ?ExcludedLineRange + { + if ( + ($node instanceof Throw_ || $node instanceof OldThrow_) + && $node->expr instanceof New_ + && $node->expr->class instanceof Name + && in_array($node->expr->class->toString(), $this->classNames, true) + ) { + return new ExcludedLineRange($node->getStartLine(), $node->getEndLine()); + } + return null; + } + +} diff --git a/src/Hierarchy/LineOfCode.php b/src/Hierarchy/LineOfCode.php index 29f5f5d..3c5e843 100644 --- a/src/Hierarchy/LineOfCode.php +++ b/src/Hierarchy/LineOfCode.php @@ -13,6 +13,7 @@ final class LineOfCode public function __construct( private readonly int $number, private readonly bool $executable, + private readonly bool $excluded, private readonly bool $covered, private readonly bool $changed, private readonly string $contents, @@ -36,6 +37,11 @@ public function isExecutable(): bool return $this->executable; } + public function isExcluded(): bool + { + return $this->excluded; + } + /** * True if this line was executed in tests */ diff --git a/src/Report/ErrorFormatter.php b/src/Report/ErrorFormatter.php index 7bf7f88..311c3d9 100644 --- a/src/Report/ErrorFormatter.php +++ b/src/Report/ErrorFormatter.php @@ -117,6 +117,7 @@ final class ErrorFormatter private const COLOR_NUMBER = "\033[93m"; // Bright yellow private const BG_COVERED = "\033[48;5;22m"; // Dark green background private const BG_UNCOVERED = "\033[48;5;52m"; // Dark red background + private const BG_EXCLUDED = "\033[48;5;236m"; // Gray background for excluded lines public function __construct( private readonly PathHelper $pathHelper, @@ -234,6 +235,7 @@ private function formatBlock( $isChanged = $line->isChanged(); $isCovered = $line->isCovered(); $isExecutable = $line->isExecutable(); + $isExcluded = $line->isExcluded(); // Format line number (right-aligned) $lineNumberFormatted = str_pad((string) $lineNumber, $maxLineNumberWidth, ' ', STR_PAD_LEFT); @@ -245,13 +247,17 @@ private function formatBlock( $bgColor = $isCovered ? self::BG_COVERED : self::BG_UNCOVERED; $resetColor = self::COLOR_RESET; } + if ($isExcluded) { + $bgColor = self::BG_EXCLUDED; + $resetColor = self::COLOR_RESET; + } // Add change indicator $changeIndicator = $patchMode && $isChanged ? '+' : ' '; // Coverage indicator (for plain text mode) $coverageIndicator = ' '; - if ($isExecutable && $this->printer->hasDisabledColors()) { + if (!$isExcluded && $isExecutable && $this->printer->hasDisabledColors()) { $coverageIndicator = $isCovered ? '|' : 'X'; } diff --git a/src/Rule/EnforceCoverageForMethodsRule.php b/src/Rule/EnforceCoverageForMethodsRule.php index 80e7b59..1b7069a 100644 --- a/src/Rule/EnforceCoverageForMethodsRule.php +++ b/src/Rule/EnforceCoverageForMethodsRule.php @@ -35,7 +35,12 @@ public function inspect( ): ?CoverageError { if (!$codeBlock instanceof ClassMethodBlock) { - return null; + return null; // we only care about methods + } + + $methodReflection = $context->getMethodReflection(); + if ($methodReflection === null) { + return null; // e.g. anonymous class methods } if ( diff --git a/tests/CodeBlockAnalyserTest.php b/tests/CodeBlockAnalyserTest.php index 134b914..09c66cf 100644 --- a/tests/CodeBlockAnalyserTest.php +++ b/tests/CodeBlockAnalyserTest.php @@ -3,18 +3,21 @@ namespace ShipMonk\CoverageGuard; use LogicException; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; use PHPUnit\Framework\TestCase; -use RuntimeException; +use ShipMonk\CoverageGuard\Ast\FileTraverser; +use ShipMonk\CoverageGuard\Excluder\ExecutableLineExcluder; +use ShipMonk\CoverageGuard\Excluder\IgnoreThrowNewExceptionLineExcluder; +use ShipMonk\CoverageGuard\Fixtures\MyLogicException; use ShipMonk\CoverageGuard\Hierarchy\ClassMethodBlock; use ShipMonk\CoverageGuard\Hierarchy\CodeBlock; use ShipMonk\CoverageGuard\Rule\CoverageError; use ShipMonk\CoverageGuard\Rule\CoverageRule; use ShipMonk\CoverageGuard\Rule\InspectionContext; +use function array_keys; use function file; -use function file_get_contents; +use function sort; +use function str_contains; use const FILE_IGNORE_NEW_LINES; final class CodeBlockAnalyserTest extends TestCase @@ -55,27 +58,32 @@ public function testAnalysesClassWithAnonymousClass(): void $this->traverseFile($filePath, $analyser); + /** @var list $capturedContexts */ $capturedContexts = $rule->capturedContexts; - self::assertCount(2, $capturedContexts); + self::assertCount(3, $capturedContexts); $methodsByClass = []; foreach ($capturedContexts as $context) { $className = $context->getClassName(); $methodName = $context->getMethodName(); - self::assertNotNull($className); + $classNameKey = $className ?? ''; self::assertNotNull($methodName); - $methodsByClass[$className][] = $methodName; + $methodsByClass[$classNameKey][] = $methodName; self::assertSame($filePath, $context->getFilePath()); self::assertFalse($context->isPatchMode()); } - self::assertCount(1, $methodsByClass, 'Methods of anonymous class should not emitted'); self::assertArrayHasKey('ClassWithAnonymousClass', $methodsByClass); self::assertSame(['methodWithAnonymousClass', 'regularMethod'], $methodsByClass['ClassWithAnonymousClass']); + + self::assertArrayHasKey('', $methodsByClass); + self::assertSame(['methodOfAnonymousClass'], $methodsByClass['']); + + self::assertCount(2, $methodsByClass); } public function testAnalysesTrait(): void @@ -122,13 +130,11 @@ public function testSkipsUnchangedMethodsInPatchMode(): void $rule = $this->createContextCapturingRule(); // In patch mode with no changed lines, methods should be skipped - $analyser = new CodeBlockAnalyser( - patchMode: true, + $analyser = $this->createAnalyser( filePath: $filePath, - linesChanged: [], // No changed lines - linesCoverage: [9 => 1, 13 => 1, 14 => 1, 17 => 1], // Some coverage - linesContents: $this->getFileLines($filePath), rules: [$rule], + patchMode: true, + linesChanged: [], // No changed lines ); $this->traverseFile($filePath, $analyser); @@ -140,17 +146,14 @@ public function testSkipsUnchangedMethodsInPatchMode(): void public function testAnalyzesOnlyChangedMethodsInPatchMode(): void { $filePath = __DIR__ . '/_fixtures/CodeBlockAnalyser/SimpleClass.php'; - $rule = $this->createContextCapturingRule(); // Mark only lines from the first method as changed - $analyser = new CodeBlockAnalyser( - patchMode: true, + $analyser = $this->createAnalyser( filePath: $filePath, - linesChanged: [9 => 9], // Line 9 is in simpleMethod - linesCoverage: [9 => 1, 13 => 1, 14 => 1, 17 => 1], - linesContents: $this->getFileLines($filePath), rules: [$rule], + patchMode: true, + linesChanged: [9 => 9], // Line 9 is in simpleMethod ); $this->traverseFile($filePath, $analyser); @@ -162,22 +165,37 @@ public function testAnalyzesOnlyChangedMethodsInPatchMode(): void self::assertSame('simpleMethod', $capturedContexts[0]->getMethodName()); } + public function testIgnoreThrowNewExceptionLineExcluder(): void + { + $filePath = __DIR__ . '/_fixtures/CodeBlockAnalyser/ClassWithThrowStatements.php'; + $excluder = new IgnoreThrowNewExceptionLineExcluder([MyLogicException::class]); + + $this->assertExcludedLinesMatchFixtureComments($filePath, [$excluder]); + } + /** * @param list $rules + * @param list $excluders + * @param array|null $linesCoverage + * @param array|null $linesChanged */ private function createAnalyser( string $filePath, - array $rules, + array $rules = [], bool $patchMode = false, + array $excluders = [], + ?array $linesCoverage = null, + ?array $linesChanged = null, ): CodeBlockAnalyser { return new CodeBlockAnalyser( patchMode: $patchMode, filePath: $filePath, - linesChanged: $patchMode ? [9 => 9, 13 => 13, 14 => 14] : [], - linesCoverage: [9 => 1, 13 => 1, 14 => 1, 17 => 1], + linesChanged: $linesChanged ?? ($patchMode ? [9 => 9, 13 => 13, 14 => 14] : []), + linesCoverage: $linesCoverage ?? [9 => 1, 13 => 1, 14 => 1, 17 => 1], linesContents: $this->getFileLines($filePath), rules: $rules, + excluders: $excluders, ); } @@ -205,22 +223,8 @@ private function traverseFile( ): void { $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $content = file_get_contents($filePath); - - if ($content === false) { - throw new RuntimeException("Could not read file: $filePath"); - } - - $stmts = $parser->parse($content); - - if ($stmts === null) { - throw new RuntimeException("Could not parse file: $filePath"); - } - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NameResolver()); // Required to resolve namespaced names - $traverser->addVisitor($analyser); - $traverser->traverse($stmts); + $traverser = new FileTraverser($parser); + $traverser->traverse($filePath, $this->getFileLines($filePath), $analyser); } /** @@ -249,4 +253,85 @@ public function inspect( }; } + /** + * @return CoverageRule&object{capturedBlocks: list} + */ + private function createLineCapturingRule(): CoverageRule + { + return new class implements CoverageRule { + + /** + * @var list + */ + public array $capturedBlocks = []; // @phpstan-ignore shipmonk.publicPropertyNotReadonly + + public function inspect( + CodeBlock $codeBlock, + InspectionContext $context, + ): ?CoverageError + { + if ($codeBlock instanceof ClassMethodBlock) { + $this->capturedBlocks[] = $codeBlock; + } + return null; + } + + }; + } + + /** + * Helper method to test excluders by comparing excluded lines with "// excluded" comments in fixture + * + * @param list $excluders + */ + private function assertExcludedLinesMatchFixtureComments( + string $filePath, + array $excluders, + ): void + { + $rule = $this->createLineCapturingRule(); + $linesContents = $this->getFileLines($filePath); + + // Find all lines with "// excluded" comment + $expectedExcludedLines = []; + foreach ($linesContents as $lineNumber => $lineContent) { + if (str_contains($lineContent, '// excluded')) { + $expectedExcludedLines[] = $lineNumber; + } + } + + $linesCoverage = []; + foreach (array_keys($linesContents) as $lineNumber) { + $linesCoverage[$lineNumber] = 1; + } + + $analyser = $this->createAnalyser( + filePath: $filePath, + rules: [$rule], + excluders: $excluders, + linesCoverage: $linesCoverage, + ); + + $this->traverseFile($filePath, $analyser); + + // Collect all excluded lines from all blocks + $actualExcludedLines = []; + foreach ($rule->capturedBlocks as $block) { + foreach ($block->getLines() as $line) { + if ($line->isExcluded()) { + $actualExcludedLines[] = $line->getNumber(); + } + } + } + + sort($expectedExcludedLines); + sort($actualExcludedLines); + + self::assertSame( + $expectedExcludedLines, + $actualExcludedLines, + 'Excluded lines should match "// excluded" comments in fixture file', + ); + } + } diff --git a/tests/Command/CheckCommandTest.php b/tests/Command/CheckCommandTest.php index afa5d94..e5d2917 100644 --- a/tests/Command/CheckCommandTest.php +++ b/tests/Command/CheckCommandTest.php @@ -5,6 +5,7 @@ use PhpParser\ParserFactory; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ShipMonk\CoverageGuard\Ast\FileTraverser; use ShipMonk\CoverageGuard\Coverage\CoverageFormatDetector; use ShipMonk\CoverageGuard\CoverageGuard; use ShipMonk\CoverageGuard\CoverageProvider; @@ -125,7 +126,8 @@ private function createCommand(mixed $stream = null): CheckCommand $patchParser = new PatchParser($cwd, $stderrPrinter); $coverageProvider = new CoverageProvider(new CoverageFormatDetector(), $stderrPrinter); $stopwatch = $this->createStopwatchMock(); - $coverageGuard = new CoverageGuard($stderrPrinter, $phpParser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); + $fileTraverser = new FileTraverser($phpParser); + $coverageGuard = new CoverageGuard($stderrPrinter, $fileTraverser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); $errorFormatter = new ErrorFormatter($pathHelper, $stdoutPrinter); return new CheckCommand($configResolver, $coverageGuard, $errorFormatter); } diff --git a/tests/CoverageGuardTest.php b/tests/CoverageGuardTest.php index 0441f54..65563e3 100644 --- a/tests/CoverageGuardTest.php +++ b/tests/CoverageGuardTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ShipMonk\CoverageGuard\Ast\FileTraverser; use ShipMonk\CoverageGuard\Coverage\CoverageFormatDetector; use ShipMonk\CoverageGuard\Exception\ErrorException; use ShipMonk\CoverageGuard\Hierarchy\CodeBlock; @@ -188,10 +189,11 @@ private function createCoverageGuard( $pathHelper = new PathHelper($cwd); $phpParser = (new ParserFactory())->createForHostVersion(); $patchParser = new PatchParser($cwd, $printer); + $fileTraverser = new FileTraverser($phpParser); $coverageProvider = new CoverageProvider(new CoverageFormatDetector(), $printer); $stopwatch = $this->createStopwatchMock(); - return new CoverageGuard($printer, $phpParser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); + return new CoverageGuard($printer, $fileTraverser, $pathHelper, $patchParser, $coverageProvider, $stopwatch); } private function createStopwatchMock(): MockObject&Stopwatch diff --git a/tests/Hierarchy/CodeBlockTest.php b/tests/Hierarchy/CodeBlockTest.php index 0774bd3..656fc8f 100644 --- a/tests/Hierarchy/CodeBlockTest.php +++ b/tests/Hierarchy/CodeBlockTest.php @@ -12,8 +12,8 @@ final class CodeBlockTest extends TestCase public function testGetLines(): void { $lines = [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: false, covered: false, changed: false, contents: 'comment'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: false, excluded: false, covered: false, changed: false, contents: 'comment'), ]; $block = $this->createBlock(lines: $lines); @@ -24,8 +24,8 @@ public function testGetLines(): void public function testGetStartLineNumber(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 5, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), ]); self::assertSame(5, $block->getStartLineNumber()); @@ -34,10 +34,10 @@ public function testGetStartLineNumber(): void public function testGetExecutableLinesCount(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: false, covered: false, changed: false, contents: 'comment'), - new LineOfCode(number: 3, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 4, executable: false, covered: false, changed: false, contents: 'whitespace'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: false, excluded: false, covered: false, changed: false, contents: 'comment'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 4, executable: false, excluded: false, covered: false, changed: false, contents: 'whitespace'), ]); self::assertSame(2, $block->getExecutableLinesCount()); @@ -46,10 +46,10 @@ public function testGetExecutableLinesCount(): void public function testGetCoveredLinesCount(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 4, executable: false, covered: false, changed: false, contents: 'comment'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 4, executable: false, excluded: false, covered: false, changed: false, contents: 'comment'), ]); self::assertSame(2, $block->getCoveredLinesCount()); @@ -58,8 +58,8 @@ public function testGetCoveredLinesCount(): void public function testGetCoveragePercentageFullyCovered(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), ]); self::assertSame(100, $block->getCoveragePercentage()); @@ -68,10 +68,10 @@ public function testGetCoveragePercentageFullyCovered(): void public function testGetCoveragePercentagePartiallyCovered(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), ]); self::assertSame(50, $block->getCoveragePercentage()); @@ -80,8 +80,8 @@ public function testGetCoveragePercentagePartiallyCovered(): void public function testGetCoveragePercentageNoExecutableLines(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: false, covered: false, changed: false, contents: 'comment'), - new LineOfCode(number: 2, executable: false, covered: false, changed: false, contents: 'whitespace'), + new LineOfCode(number: 1, executable: false, excluded: false, covered: false, changed: false, contents: 'comment'), + new LineOfCode(number: 2, executable: false, excluded: false, covered: false, changed: false, contents: 'whitespace'), ]); self::assertSame(0, $block->getCoveragePercentage()); @@ -90,8 +90,8 @@ public function testGetCoveragePercentageNoExecutableLines(): void public function testGetCoveragePercentageFullyUncovered(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), ]); self::assertSame(0, $block->getCoveragePercentage()); @@ -100,10 +100,10 @@ public function testGetCoveragePercentageFullyUncovered(): void public function testGetChangedLinesCount(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: false, covered: false, changed: true, contents: 'comment'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: false, excluded: false, covered: false, changed: true, contents: 'comment'), ]); self::assertSame(2, $block->getChangedLinesCount()); @@ -112,8 +112,8 @@ public function testGetChangedLinesCount(): void public function testGetChangePercentageFullyChanged(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), ]); self::assertSame(100, $block->getChangePercentage()); @@ -122,8 +122,8 @@ public function testGetChangePercentageFullyChanged(): void public function testGetChangePercentagePartiallyChanged(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), ]); self::assertSame(50, $block->getChangePercentage()); @@ -132,29 +132,29 @@ public function testGetChangePercentagePartiallyChanged(): void public function testGetChangePercentageNoExecutableLines(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: false, covered: false, changed: true, contents: 'comment'), + new LineOfCode(number: 1, executable: false, excluded: false, covered: false, changed: true, contents: 'comment'), ]); self::assertSame(0, $block->getChangePercentage()); } - public function testGetMethodName(): void + public function testGetNode(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), - new LineOfCode(number: 2, executable: false, covered: false, changed: false, contents: 'comment'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), ]); - self::assertSame('testMethod', $block->getMethodName()); + $node = $block->getNode(); + self::assertSame('testMethod', $node->name->toString()); } - public function testGetNode(): void + public function testGetMethodName(): void { $block = $this->createBlock(lines: [ - new LineOfCode(number: 1, executable: true, covered: true, changed: false, contents: 'code'), + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: false, contents: 'code'), ]); - self::assertSame('testMethod', $block->getNode()->name->toString()); + self::assertSame('testMethod', $block->getMethodName()); } /** diff --git a/tests/Report/ErrorFormatterTest.php b/tests/Report/ErrorFormatterTest.php index 6597662..7a9010d 100644 --- a/tests/Report/ErrorFormatterTest.php +++ b/tests/Report/ErrorFormatterTest.php @@ -25,9 +25,9 @@ final class ErrorFormatterTest extends TestCase public function testHighlightWithColors(): void { $lines = [ - new LineOfCode(1, false, false, false, 'formatReport($lines, new Config(), patchMode: true); @@ -47,8 +47,8 @@ public function testClickableFilepathWhenEditorUrlSet(): void $config->setEditorUrl('vscode://file/{file}:{line}'); $lines = [ - new LineOfCode(1, false, false, false, 'formatReport($lines, $config, patchMode: false); @@ -62,8 +62,8 @@ public function testClickableFilepathWhenEditorUrlSet(): void public function testNoClickableFilepathWhenEditorUrlNotSet(): void { $lines = [ - new LineOfCode(1, false, false, false, 'formatReport($lines, new Config(), patchMode: false); @@ -113,10 +113,14 @@ private function createCoverageReport( { $node = new ClassMethod( name: new Identifier('testMethod'), + subNodes: [ + 'stmts' => [], + ], ); + $codeBlock = new ClassMethodBlock( - $node, - $lines, + node: $node, + lines: $lines, ); $reportedError = new ReportedError( '/tmp/test.php', diff --git a/tests/Rule/EnforceCoverageForMethodsRuleTest.php b/tests/Rule/EnforceCoverageForMethodsRuleTest.php index bfe151c..227f207 100644 --- a/tests/Rule/EnforceCoverageForMethodsRuleTest.php +++ b/tests/Rule/EnforceCoverageForMethodsRuleTest.php @@ -17,18 +17,18 @@ public function testReturnsErrorWhenMethodHasNoRequiredCoverage(): void $rule = new EnforceCoverageForMethodsRule(requiredCoveragePercentage: 1, minExecutableLines: 5); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: true, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + ]); - $error = $rule->inspect(codeBlock: $block, context: $this->createContext()); + $error = $rule->inspect(codeBlock: $block, context: $this->createContext(className: self::class, methodName: 'createBlock')); self::assertNotNull($error); - self::assertSame('Method TestClass::testMethod has no coverage, expected at least 1%.', $error->getMessage()); + self::assertSame('Method ' . self::class . '::testMethod has no coverage, expected at least 1%.', $error->getMessage()); } public function testReturnsErrorWhenMethodHasInsufficientCoverage(): void @@ -36,18 +36,18 @@ public function testReturnsErrorWhenMethodHasInsufficientCoverage(): void $rule = new EnforceCoverageForMethodsRule(requiredCoveragePercentage: 50, minExecutableLines: 5); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: true, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: true, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + ]); - $error = $rule->inspect(codeBlock: $block, context: $this->createContext()); + $error = $rule->inspect(codeBlock: $block, context: $this->createContext(className: self::class, methodName: 'createBlock')); self::assertNotNull($error); - self::assertSame('Method TestClass::testMethod has only 17% coverage, expected at least 50%.', $error->getMessage()); + self::assertSame('Method ' . self::class . '::testMethod has only 17% coverage, expected at least 50%.', $error->getMessage()); } public function testReturnsNullWhenMethodHasLessThanMinExecutableLinesDefault(): void @@ -55,11 +55,11 @@ public function testReturnsNullWhenMethodHasLessThanMinExecutableLinesDefault(): $rule = new EnforceCoverageForMethodsRule(minExecutableLines: 5); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + ]); $error = $rule->inspect(codeBlock: $block, context: $this->createContext()); @@ -71,12 +71,12 @@ public function testReturnsNullWhenMethodHasLessThanMinExecutableLines(): void $rule = new EnforceCoverageForMethodsRule(minExecutableLines: 10); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: true, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + ]); $error = $rule->inspect(codeBlock: $block, context: $this->createContext()); @@ -88,13 +88,13 @@ public function testReturnsNullWhenMethodHasSufficientCoverage(): void $rule = new EnforceCoverageForMethodsRule(requiredCoveragePercentage: 50, minExecutableLines: 5); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: true, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: true, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: true, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: true, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: true, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: true, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: true, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + ]); $error = $rule->inspect(codeBlock: $block, context: $this->createContext()); @@ -110,13 +110,13 @@ public function testReturnsNullWhenMethodIsNotChangedEnough(): void ); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: false, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + ]); $error = $rule->inspect(codeBlock: $block, context: $this->createContext(patchMode: true)); @@ -132,18 +132,18 @@ public function testReturnsErrorWhenMethodIsChangedEnough(): void ); $block = $this->createBlock([ - new LineOfCode(number: 1, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 2, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 3, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 4, executable: true, covered: false, changed: true, contents: 'code'), - new LineOfCode(number: 5, executable: true, covered: false, changed: false, contents: 'code'), - new LineOfCode(number: 6, executable: true, covered: false, changed: false, contents: 'code'), - ]); + new LineOfCode(number: 1, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 2, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 3, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 4, executable: true, excluded: false, covered: false, changed: true, contents: 'code'), + new LineOfCode(number: 5, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + new LineOfCode(number: 6, executable: true, excluded: false, covered: false, changed: false, contents: 'code'), + ]); - $error = $rule->inspect(codeBlock: $block, context: $this->createContext(patchMode: true)); + $error = $rule->inspect(codeBlock: $block, context: $this->createContext(className: self::class, methodName: 'createBlock', patchMode: true)); self::assertNotNull($error); - self::assertSame('Method TestClass::testMethod has no coverage, expected at least 50%.', $error->getMessage()); + self::assertSame('Method ' . self::class . '::testMethod has no coverage, expected at least 50%.', $error->getMessage()); } public function testThrowsExceptionWhenRequiredCoveragePercentageIsTooLow(): void @@ -187,8 +187,8 @@ public function testThrowsExceptionWhenMinExecutableLinesIsNegative(): void } private function createContext( - string $className = 'TestClass', - string $methodName = 'testMethod', + ?string $className = null, + ?string $methodName = null, string $filePath = '/path/to/file.php', bool $patchMode = false, ): InspectionContext diff --git a/tests/_fixtures/CodeBlockAnalyser/ClassWithThrowStatements.php b/tests/_fixtures/CodeBlockAnalyser/ClassWithThrowStatements.php new file mode 100644 index 0000000..026c09e --- /dev/null +++ b/tests/_fixtures/CodeBlockAnalyser/ClassWithThrowStatements.php @@ -0,0 +1,35 @@ +