diff --git a/src/Analyser/ClassNode.php b/src/Analyser/ClassNode.php index d81ea041..5513064a 100644 --- a/src/Analyser/ClassNode.php +++ b/src/Analyser/ClassNode.php @@ -135,20 +135,40 @@ public function dependsOnNamespace(string $namespace): bool public function implementsInterface(string $interface): bool { - return in_array($interface, $this->implements, true) - || in_array($interface, $this->parentInterfaces, true); + return $this->matchesAnyClassLike($interface, $this->implements) + || $this->matchesAnyClassLike($interface, $this->parentInterfaces); } public function extendsClass(string $class): bool { - return $this->extends === $class - || in_array($class, $this->parentClasses, true); + if ($this->extends !== null && strcasecmp($this->extends, $class) === 0) { + return true; + } + + return $this->matchesAnyClassLike($class, $this->parentClasses); } public function extendsInterface(string $interface): bool { - return in_array($interface, $this->interfaceExtends, true) - || in_array($interface, $this->parentInterfaces, true); + return $this->matchesAnyClassLike($interface, $this->interfaceExtends) + || $this->matchesAnyClassLike($interface, $this->parentInterfaces); + } + + /** + * Class-like names are case-insensitive in PHP, unlike function and + * constant dependencies, so matching must not be case-sensitive. + * + * @param string[] $classLikes + */ + private function matchesAnyClassLike(string $needle, array $classLikes): bool + { + foreach ($classLikes as $classLike) { + if (strcasecmp($classLike, $needle) === 0) { + return true; + } + } + + return false; } public function callsFunction(string $function): bool diff --git a/tests/Analyser/ClassNodeTest.php b/tests/Analyser/ClassNodeTest.php index c39b19fd..5a1bd6aa 100644 --- a/tests/Analyser/ClassNodeTest.php +++ b/tests/Analyser/ClassNodeTest.php @@ -161,6 +161,68 @@ className: 'App\\Domain\\DieService', $this->assertFalse($usesExit->usesLanguageConstruct('include')); } + public function testImplementsInterfaceIsCaseInsensitive(): void + { + $classNode = new ClassNode( + className: Foo::class, + file: '/foo.php', + line: 1, + layer: 'Domain', + extends: null, + isAbstract: false, + isFinal: true, + isInterface: false, + isReadonly: false, + implements: ['App\\Contracts\\foointerface'], + parentInterfaces: ['App\\Contracts\\rootinterface'], + ); + + $this->assertTrue($classNode->implementsInterface('App\\Contracts\\FooInterface')); + $this->assertTrue($classNode->implementsInterface('App\\Contracts\\RootInterface')); + $this->assertFalse($classNode->implementsInterface('App\\Contracts\\OtherInterface')); + } + + public function testExtendsClassIsCaseInsensitive(): void + { + $classNode = new ClassNode( + className: Foo::class, + file: '/foo.php', + line: 1, + layer: 'Domain', + extends: 'App\\Support\\baseclass', + isAbstract: false, + isFinal: true, + isInterface: false, + isReadonly: false, + parentClasses: ['App\\Support\\rootclass'], + ); + + $this->assertTrue($classNode->extendsClass('App\\Support\\BaseClass')); + $this->assertTrue($classNode->extendsClass('App\\Support\\RootClass')); + $this->assertFalse($classNode->extendsClass('App\\Support\\OtherClass')); + } + + public function testExtendsInterfaceIsCaseInsensitive(): void + { + $classNode = new ClassNode( + className: 'App\\Contracts\\FooInterface', + file: '/FooInterface.php', + line: 1, + layer: 'Contracts', + extends: null, + isAbstract: false, + isFinal: false, + isInterface: true, + isReadonly: false, + interfaceExtends: ['App\\Contracts\\baseinterface'], + parentInterfaces: ['App\\Contracts\\rootinterface'], + ); + + $this->assertTrue($classNode->extendsInterface('App\\Contracts\\BaseInterface')); + $this->assertTrue($classNode->extendsInterface('App\\Contracts\\RootInterface')); + $this->assertFalse($classNode->extendsInterface('App\\Contracts\\OtherInterface')); + } + public function testSetRecursiveParents(): void { $classNode = new ClassNode(