Skip to content

Commit 369abb4

Browse files
Refactor (and cache potentially expensive Reflection API operations)
1 parent cc3149b commit 369abb4

File tree

3 files changed

+68
-40
lines changed

3 files changed

+68
-40
lines changed

.psalm/baseline.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@
449449
</RedundantCondition>
450450
</file>
451451
<file src="src/Metadata/Api/CodeCoverage.php">
452+
<LessSpecificReturnStatement>
453+
<code><![CDATA[$names]]></code>
454+
</LessSpecificReturnStatement>
455+
<MoreSpecificReturnType>
456+
<code><![CDATA[non-empty-list<non-empty-string>]]></code>
457+
</MoreSpecificReturnType>
452458
<RedundantCondition>
453459
<code><![CDATA[$metadata instanceof Covers]]></code>
454460
<code><![CDATA[$metadata instanceof Uses]]></code>

src/Metadata/Api/CodeCoverage.php

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace PHPUnit\Metadata\Api;
1111

1212
use function assert;
13+
use function class_exists;
1314
use function count;
1415
use function interface_exists;
1516
use function sprintf;
@@ -28,7 +29,6 @@
2829
use PHPUnit\Metadata\UsesFunction;
2930
use PHPUnit\Metadata\UsesMethod;
3031
use ReflectionClass;
31-
use ReflectionException;
3232
use SebastianBergmann\CodeUnit\CodeUnitCollection;
3333
use SebastianBergmann\CodeUnit\Exception as CodeUnitException;
3434
use SebastianBergmann\CodeUnit\InvalidCodeUnitException;
@@ -37,8 +37,13 @@
3737
/**
3838
* @internal This class is not covered by the backward compatibility promise for PHPUnit
3939
*/
40-
final readonly class CodeCoverage
40+
final class CodeCoverage
4141
{
42+
/**
43+
* @psalm-var array<class-string, non-empty-list<class-string>>
44+
*/
45+
private array $withParents = [];
46+
4247
/**
4348
* @psalm-param class-string $className
4449
* @psalm-param non-empty-string $methodName
@@ -222,30 +227,7 @@ public function shouldCodeCoverageBeCollectedFor(string $className, string $meth
222227
private function mapToCodeUnits(CoversClass|CoversFunction|CoversMethod|UsesClass|UsesFunction|UsesMethod $metadata): CodeUnitCollection
223228
{
224229
$mapper = new Mapper;
225-
$names = [$metadata->asStringForCodeUnitMapper()];
226-
227-
if ($metadata->isCoversClass() || $metadata->isUsesClass()) {
228-
try {
229-
$reflector = new ReflectionClass($metadata->asStringForCodeUnitMapper());
230-
} catch (ReflectionException $e) {
231-
throw new InvalidCoversTargetException(
232-
sprintf(
233-
'Class "%s" is not a valid target for code coverage',
234-
$metadata->asStringForCodeUnitMapper(),
235-
),
236-
$e->getCode(),
237-
$e,
238-
);
239-
}
240-
241-
while ($reflector = $reflector->getParentClass()) {
242-
if (!$reflector->isUserDefined()) {
243-
break;
244-
}
245-
246-
$names[] = $reflector->getName();
247-
}
248-
}
230+
$names = $this->names($metadata);
249231

250232
try {
251233
if (count($names) === 1) {
@@ -262,25 +244,65 @@ private function mapToCodeUnits(CoversClass|CoversFunction|CoversMethod|UsesClas
262244

263245
return $codeUnits;
264246
} catch (CodeUnitException $e) {
265-
if ($metadata->isCoversClass() || $metadata->isUsesClass()) {
266-
if (interface_exists($metadata->className())) {
267-
$type = 'Interface';
268-
} else {
269-
$type = 'Class';
270-
}
271-
} else {
272-
$type = 'Function';
273-
}
274-
275247
throw new InvalidCoversTargetException(
276248
sprintf(
277-
'%s "%s" is not a valid target for code coverage',
278-
$type,
249+
'%s is not a valid target for code coverage',
279250
$metadata->asStringForCodeUnitMapper(),
280251
),
281252
$e->getCode(),
282253
$e,
283254
);
284255
}
285256
}
257+
258+
/**
259+
* @psalm-return non-empty-list<non-empty-string>
260+
*
261+
* @throws InvalidCoversTargetException
262+
*/
263+
private function names(CoversClass|CoversFunction|CoversMethod|UsesClass|UsesFunction|UsesMethod $metadata): array
264+
{
265+
$name = $metadata->asStringForCodeUnitMapper();
266+
$names = [$name];
267+
268+
if ($metadata->isCoversClass() || $metadata->isUsesClass()) {
269+
if (isset($this->withParents[$name])) {
270+
return $this->withParents[$name];
271+
}
272+
273+
if (interface_exists($name)) {
274+
throw new InvalidCoversTargetException(
275+
sprintf(
276+
'Interface "%s" is not a valid target for code coverage',
277+
$name,
278+
),
279+
);
280+
}
281+
282+
if (!class_exists($name)) {
283+
throw new InvalidCoversTargetException(
284+
sprintf(
285+
'Class "%s" is not a valid target for code coverage',
286+
$name,
287+
),
288+
);
289+
}
290+
291+
assert(class_exists($names[0]));
292+
293+
$reflector = new ReflectionClass($name);
294+
295+
while ($reflector = $reflector->getParentClass()) {
296+
if (!$reflector->isUserDefined()) {
297+
break;
298+
}
299+
300+
$names[] = $reflector->getName();
301+
}
302+
303+
$this->withParents[$name] = $names;
304+
}
305+
306+
return $names;
307+
}
286308
}

tests/unit/Metadata/Api/CodeCoverageTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,15 +455,15 @@ public function testRejectsInvalidUsesClassTargetWithAnnotation(): void
455455
public function testRejectsInvalidCoversFunctionTarget(): void
456456
{
457457
$this->expectException(CodeCoverageException::class);
458-
$this->expectExceptionMessage('Function "::invalid_function" is not a valid target for code coverage');
458+
$this->expectExceptionMessage('::invalid_function is not a valid target for code coverage');
459459

460460
(new CodeCoverage)->linesToBeCovered(InvalidFunctionTargetTest::class, 'testOne');
461461
}
462462

463463
public function testRejectsInvalidUsesFunctionTarget(): void
464464
{
465465
$this->expectException(CodeCoverageException::class);
466-
$this->expectExceptionMessage('Function "::invalid_function" is not a valid target for code coverage');
466+
$this->expectExceptionMessage('::invalid_function is not a valid target for code coverage');
467467

468468
(new CodeCoverage)->linesToBeUsed(InvalidFunctionTargetTest::class, 'testOne');
469469
}

0 commit comments

Comments
 (0)