Skip to content

Commit 3f039d1

Browse files
committed
2 parents 6659305 + d986663 commit 3f039d1

File tree

9 files changed

+310
-16
lines changed

9 files changed

+310
-16
lines changed

docs/documentation/selectors.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,45 +46,48 @@ The first selector will select all classes defined in the `src/App/User/Domain/U
4646

4747
The second one will select all classes who have a filepath matching the regular expression.
4848

49+
## Selector::isStandardClass()
50+
Selects Predefined PHP classes (stdClass, ArrayAccess, Exception, Enum...)
51+
4952
## Selector::isError()
50-
Select classes that extend the `\Error` class.
53+
Selects classes that extend the `\Error` class.
5154

5255
## Selector::isException()
53-
Select classes that extend the `\Exception` class.
56+
Selects classes that extend the `\Exception` class.
5457

5558
## Selector::isThrowable()
56-
Select classes that implement the `\Throwable` interface.
59+
Selects classes that implement the `\Throwable` interface.
5760

5861
## Selector::implements(string)
59-
Select classes that implement the given interface.
62+
Selects classes that implement the given interface.
6063

6164
## Selector::extends(string)
62-
Select classes that extend the given class.
65+
Selects classes that extend the given class.
6366

6467
## Selector::isInterface()
65-
Select all interfaces.
68+
Selects all interfaces.
6669

6770
## Selector::appliesAttribute(string)
68-
Select classes that applies the given attribute.
71+
Selects classes that applies the given attribute.
6972
You can pass one or more arguments with their values that the attribute should apply to.
7073

7174
## Selector::isAbstract()
72-
Select all abstract classes.
75+
Selects all abstract classes.
7376

7477
## Selector::isAttribute()
75-
Select all attribute classes.
78+
Selects all attribute classes.
7679

7780
## Selector::isEnum()
78-
Select all enums.
81+
Selects all enums.
7982

8083
## Selector::isFinal()
81-
Select all final classes.
84+
Selects all final classes.
8285

8386
## Selector::isReadonly()
84-
Select all readonly classes.
87+
Selects all readonly classes.
8588

8689
## Selector::isTrait()
87-
Select all traits.
90+
Selects all traits.
8891

8992
<br />
9093

src/Rule/Extractor/Declaration/ClassnameExtractor.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ public function getNodeType(): string
1818
*/
1919
protected function meetsDeclaration(Node $node, Scope $scope, array $params = []): bool
2020
{
21-
if (!isset($params['isRegex'], $params['fqcn'])) {
21+
if (!isset($params['isRegex'], $params['classname'])) {
2222
return false;
2323
}
2424

2525
$namespacedName = $node->getClassReflection()->getName();
2626

2727
if ($params['isRegex'] === true) {
28-
return preg_match($params['fqcn'], $namespacedName) === 1;
28+
return preg_match($params['classname'], $namespacedName) === 1;
2929
}
3030

31-
return $namespacedName === trimSeparators($params['fqcn']);
31+
return $namespacedName === trimSeparators($params['classname']);
3232
}
3333
}

src/Selector/IsStandardClass.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPat\Selector;
4+
5+
use PHPat\Parser\BuiltInClasses;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
final class IsStandardClass implements SelectorInterface
9+
{
10+
public function getName(): string
11+
{
12+
return '-standard classes-';
13+
}
14+
15+
public function matches(ClassReflection $classReflection): bool
16+
{
17+
return in_array($classReflection->getName(), BuiltInClasses::PHP_BUILT_IN_CLASSES, true);
18+
}
19+
}

src/Selector/SelectorPrimitive.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public static function isAttribute(): IsAttribute
5959
return new IsAttribute();
6060
}
6161

62+
public static function isStandardClass(): IsStandardClass
63+
{
64+
return new IsStandardClass();
65+
}
66+
6267
/**
6368
* @param class-string|non-empty-string $namespace
6469
*/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\PHPat\fixtures\Special;
4+
5+
class ClassExtendingException extends \Exception
6+
{
7+
public function customMethod(): string
8+
{
9+
return 'custom';
10+
}
11+
}
12+
13+
class ClassExtendingError extends \Error
14+
{
15+
public function customMethod(): string
16+
{
17+
return 'custom';
18+
}
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\PHPat\fixtures\Special;
4+
5+
class ClassWithStandardClasses
6+
{
7+
public function createException(): \Exception
8+
{
9+
return new \Exception('test');
10+
}
11+
12+
public function createStdClass(): \stdClass
13+
{
14+
return new \stdClass();
15+
}
16+
17+
public function createError(): \Error
18+
{
19+
return new \Error('test');
20+
}
21+
22+
public function createClosure(): \Closure
23+
{
24+
return function() { return 'test'; };
25+
}
26+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\PHPat\unit\rules\ShouldBeNamed;
4+
5+
use PHPat\Configuration;
6+
use PHPat\Rule\Assertion\Declaration\ShouldBeNamed\ClassnameRule;
7+
use PHPat\Rule\Assertion\Declaration\ShouldBeNamed\ShouldBeNamed;
8+
use PHPat\Selector\Classname;
9+
use PHPat\Statement\Builder\StatementBuilderFactory;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Testing\RuleTestCase;
12+
use PHPStan\Type\FileTypeMapper;
13+
use Tests\PHPat\fixtures\FixtureClass;
14+
use Tests\PHPat\unit\FakeTestParser;
15+
16+
/**
17+
* @extends RuleTestCase<ClassnameRule>
18+
* @internal
19+
* @coversNothing
20+
*/
21+
class ClassnameRegexTest extends RuleTestCase
22+
{
23+
public const RULE_NAME = 'testFixtureClassShouldEndWithClass';
24+
25+
public function testRule(): void
26+
{
27+
$this->analyse(['tests/fixtures/FixtureClass.php'], []);
28+
}
29+
30+
protected function getRule(): Rule
31+
{
32+
$testParser = FakeTestParser::create(
33+
self::RULE_NAME,
34+
ShouldBeNamed::class,
35+
[new Classname(FixtureClass::class, false)],
36+
[],
37+
[],
38+
['isRegex' => true, 'classname' => '/.*Class$/']
39+
);
40+
41+
return new ClassnameRule(
42+
new StatementBuilderFactory($testParser),
43+
new Configuration(false, true, false),
44+
$this->createReflectionProvider(),
45+
self::getContainer()->getByType(FileTypeMapper::class)
46+
);
47+
}
48+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\PHPat\unit\rules\ShouldNotDepend;
4+
5+
use PHPat\Configuration;
6+
use PHPat\Rule\Assertion\Relation\ShouldNotDepend\NewRule;
7+
use PHPat\Rule\Assertion\Relation\ShouldNotDepend\ShouldNotDepend;
8+
use PHPat\Selector\Classname;
9+
use PHPat\Selector\IsStandardClass;
10+
use PHPat\Statement\Builder\StatementBuilderFactory;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Testing\RuleTestCase;
13+
use PHPStan\Type\FileTypeMapper;
14+
use Tests\PHPat\fixtures\Special\ClassWithStandardClasses;
15+
use Tests\PHPat\unit\FakeTestParser;
16+
17+
/**
18+
* @extends RuleTestCase<NewRule>
19+
* @internal
20+
* @coversNothing
21+
*/
22+
class IsStandardClassTest extends RuleTestCase
23+
{
24+
public const RULE_NAME = 'testClassWithStandardClassInstanceofShouldNotDependOnStandardClasses';
25+
26+
public function testRule(): void
27+
{
28+
// This test verifies that the IsStandardClass selector works with relation assertions
29+
// ClassWithStandardClassInstanceof uses new with standard PHP classes
30+
// With ignore_built_in_classes = false, these should be detected as violations
31+
$this->analyse([__DIR__.'/../../../fixtures/Special/ClassWithStandardClasses.php'], [
32+
[sprintf('%s should not depend on %s', ClassWithStandardClasses::class, 'Exception'), 9],
33+
[sprintf('%s should not depend on %s', ClassWithStandardClasses::class, 'stdClass'), 14],
34+
[sprintf('%s should not depend on %s', ClassWithStandardClasses::class, 'Error'), 19],
35+
]);
36+
}
37+
38+
protected function getRule(): Rule
39+
{
40+
$testParser = FakeTestParser::create(
41+
self::RULE_NAME,
42+
ShouldNotDepend::class,
43+
[new Classname(ClassWithStandardClasses::class, false)],
44+
[new IsStandardClass()]
45+
);
46+
47+
return new NewRule(
48+
new StatementBuilderFactory($testParser),
49+
new Configuration(false, false, false), // ignore_built_in_classes = false
50+
$this->createReflectionProvider(),
51+
self::getContainer()->getByType(FileTypeMapper::class)
52+
);
53+
}
54+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Tests\PHPat\unit\selectors;
4+
5+
use PHPat\Selector\IsStandardClass;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPUnit\Framework\TestCase;
8+
use Tests\PHPat\fixtures\Simple\SimpleClass;
9+
10+
/**
11+
* @internal
12+
*
13+
* @covers \PHPat\Selector\IsStandardClass
14+
*/
15+
class IsStandardClassTest extends TestCase
16+
{
17+
public function testGetName(): void
18+
{
19+
$selector = new IsStandardClass();
20+
21+
$this->assertEquals('-standard classes-', $selector->getName());
22+
}
23+
24+
/**
25+
* @dataProvider getBuiltInClassCases
26+
*/
27+
public function testMatchesBuiltInClasses(string $className): void
28+
{
29+
$selector = new IsStandardClass();
30+
$classReflection = $this->createClassReflection($className);
31+
32+
self::assertTrue($selector->matches($classReflection));
33+
}
34+
35+
public static function getBuiltInClassCases(): array
36+
{
37+
return [
38+
['stdClass'],
39+
['Exception'],
40+
['Iterator'],
41+
['Throwable'],
42+
['Generator'],
43+
['Countable'],
44+
['ArrayAccess'],
45+
['Closure'],
46+
['Error'],
47+
['TypeError'],
48+
['ValueError'],
49+
];
50+
}
51+
52+
/**
53+
* @dataProvider getUserDefinedClassCases
54+
*/
55+
public function testDoesNotMatchUserDefinedClasses(string $className): void
56+
{
57+
$selector = new IsStandardClass();
58+
$classReflection = $this->createClassReflection($className);
59+
60+
self::assertFalse($selector->matches($classReflection));
61+
}
62+
63+
public static function getUserDefinedClassCases(): array
64+
{
65+
return [
66+
[SimpleClass::class],
67+
['App\User'],
68+
['MyCustomClass'],
69+
['Vendor\Package\SomeClass'],
70+
['Tests\PHPat\fixtures\Simple\SimpleClass'],
71+
];
72+
}
73+
74+
public function testHandlesNonExistentClass(): void
75+
{
76+
$selector = new IsStandardClass();
77+
$classReflection = $this->createClassReflection('NonExistentClass');
78+
79+
self::assertFalse($selector->matches($classReflection));
80+
}
81+
82+
public function testHandlesEmptyClassName(): void
83+
{
84+
$selector = new IsStandardClass();
85+
$classReflection = $this->createClassReflection('');
86+
87+
self::assertFalse($selector->matches($classReflection));
88+
}
89+
90+
public function testCaseSensitiveMatching(): void
91+
{
92+
$selector = new IsStandardClass();
93+
94+
// Correct case should match
95+
$correctCaseReflection = $this->createClassReflection('stdClass');
96+
self::assertTrue($selector->matches($correctCaseReflection));
97+
98+
// Incorrect case should not match
99+
$incorrectCaseReflection = $this->createClassReflection('stdclass');
100+
self::assertFalse($selector->matches($incorrectCaseReflection));
101+
102+
$upperCaseReflection = $this->createClassReflection('STDCLASS');
103+
self::assertFalse($selector->matches($upperCaseReflection));
104+
}
105+
106+
private function createClassReflection(string $className): ClassReflection
107+
{
108+
$ref = new \ReflectionClass(ClassReflection::class);
109+
$instance = $ref->newInstanceWithoutConstructor();
110+
111+
$mockReflection = $this->createMock(\ReflectionClass::class);
112+
$mockReflection->method('getName')->willReturn($className);
113+
114+
$reflectionProperty = $ref->getProperty('reflection');
115+
$reflectionProperty->setAccessible(true);
116+
$reflectionProperty->setValue($instance, $mockReflection);
117+
118+
return $instance;
119+
}
120+
}

0 commit comments

Comments
 (0)