Skip to content

Commit

Permalink
Merge pull request #3701 from smaddock/issue-2717
Browse files Browse the repository at this point in the history
Inherit method documentation through multiple generations
  • Loading branch information
jaapio committed May 8, 2024
2 parents 1dd4b2a + eb4234c commit 59f5d11
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 37 deletions.
108 changes: 71 additions & 37 deletions src/phpDocumentor/Descriptor/MethodDescriptor.php
Expand Up @@ -210,47 +210,23 @@ public function getInheritedElement(): MethodInterface|null
return $this->inheritedElement;
}

/** @var ClassInterface|InterfaceInterface|null $associatedClass */
$associatedClass = $this->getParent();
if (! $associatedClass instanceof ClassInterface && ! $associatedClass instanceof InterfaceInterface) {
return null;
}

$parentClass = $associatedClass->getParent();
if ($parentClass instanceof ClassInterface || $parentClass instanceof Collection) {
// the parent of a class is always a class, but the parent of an interface is a collection of interfaces.
$parents = $parentClass instanceof ClassInterface ? [$parentClass] :
$parentClass->filter(InterfaceInterface::class);
foreach ($parents as $parent) {
/** @var MethodInterface|null $parentMethod */
$parentMethod = $parent->getMethods()->fetch($this->getName());
if ($parentMethod instanceof self) {
$this->inheritedElement = $parentMethod;

return $this->inheritedElement;
}
/** @var ClassInterface|InterfaceInterface|null $methodParent */
$methodParent = $this->getParent();
if ($methodParent instanceof ClassInterface) {
/** @var MethodInterface|null $parentClassMethod */
$parentClassMethod = $this->recurseClassInheritance($methodParent);
if ($parentClassMethod instanceof self) {
$this->inheritedElement = $parentClassMethod;
}
}

// also check all implemented interfaces next if the parent is a class and not an interface
if ($associatedClass instanceof ClassInterface) {
/** @var InterfaceInterface|Fqsen $interface */
foreach ($associatedClass->getInterfaces() as $interface) {
if ($interface instanceof Fqsen) {
continue;
}

/** @var ?MethodInterface $parentMethod */
$parentMethod = $interface->getMethods()->fetch($this->getName());
if ($parentMethod instanceof self) {
$this->inheritedElement = $parentMethod;

return $this->inheritedElement;
}
} elseif ($methodParent instanceof InterfaceInterface) {
/** @var MethodInterface|null $parentInterfaceMethod */
$parentInterfaceMethod = $this->recurseInterfaceInheritance($methodParent);
if ($parentInterfaceMethod instanceof self) {
$this->inheritedElement = $parentInterfaceMethod;
}
}

return null;
return $this->inheritedElement;
}

/**
Expand All @@ -270,4 +246,62 @@ public function getHasReturnByReference(): bool
{
return $this->hasReturnByReference;
}

private function recurseClassInheritance(ClassInterface $currentClass): MethodInterface|null
{
/** @var ClassInterface|null $parentClass */
$parentClass = $currentClass->getParent();
if ($parentClass instanceof ClassInterface) {
/** @var MethodInterface|null $parentClassMethod */
$parentClassMethod = $parentClass->getMethods()->fetch($this->getName());
if ($parentClassMethod instanceof self) {
return $parentClassMethod;
}

/** @var MethodInterface|null $ancestorMethod */
$ancestorMethod = $this->recurseClassInheritance($parentClass);
if ($ancestorMethod instanceof self) {
return $ancestorMethod;
}
}

/** @var Collection<InterfaceInterface>|null $parentInterfaces */
$parentInterfaces = $currentClass->getInterfaces()->filter(InterfaceInterface::class);
foreach ($parentInterfaces as $parentInterface) {
/** @var MethodInterface|null $parentInterfaceMethod */
$parentInterfaceMethod = $parentInterface->getMethods()->fetch($this->getName());
if ($parentInterfaceMethod instanceof self) {
return $parentInterfaceMethod;
}

/** @var MethodInterface|null $ancestorMethod */
$ancestorMethod = $this->recurseInterfaceInheritance($parentInterface);
if ($ancestorMethod instanceof self) {
return $ancestorMethod;
}
}

return null;
}

private function recurseInterfaceInheritance(InterfaceInterface $currentInterface): MethodInterface|null
{
/** @var Collection<InterfaceInterface>|null $parentInterfaces */
$parentInterfaces = $currentInterface->getParent()->filter(InterfaceInterface::class);
foreach ($parentInterfaces as $parentInterface) {
/** @var MethodInterface|null $parentInterfaceMethod */
$parentInterfaceMethod = $parentInterface->getMethods()->fetch($this->getName());
if ($parentInterfaceMethod instanceof self) {
return $parentInterfaceMethod;
}

/** @var MethodInterface|null $ancestorMethod */
$ancestorMethod = $this->recurseInterfaceInheritance($parentInterface);
if ($ancestorMethod instanceof self) {
return $ancestorMethod;
}
}

return null;
}
}
150 changes: 150 additions & 0 deletions tests/unit/phpDocumentor/Descriptor/MethodDescriptorTest.php
Expand Up @@ -196,6 +196,156 @@ public function testParamTagsInheritWhenNoneArePresent(): void
$this->assertSame($paramCollection, $result);
}

public function testElementDoesNotInheritWhenNoParents(): void
{
$this->assertNull($this->fixture->getInheritedElement());

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);

$this->assertNull($this->fixture->getInheritedElement());
}

public function testElementInheritanceCaches(): void
{
$parentClass = new ClassDescriptor();
$parentClass->setAbstract(true);
$parentClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\AbstractClass'));
$parentMethod = new MethodDescriptor();
$parentMethod->setName('myMethod');
$parentMethod->setParent($parentClass);
$parentClass->getMethods()->set($parentMethod->getName(), $parentMethod);

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setParent($parentClass);
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->fixture->getInheritedElement();
$this->assertSame($parentMethod, $this->fixture->getInheritedElement());
}

public function testElementInheritsWhenExtending(): void
{
$parentClass = new ClassDescriptor();
$parentClass->setAbstract(true);
$parentClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\AbstractClass'));
$parentMethod = new MethodDescriptor();
$parentMethod->setName('myMethod');
$parentMethod->setParent($parentClass);
$parentClass->getMethods()->set($parentMethod->getName(), $parentMethod);

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setParent($parentClass);
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->assertSame($parentMethod, $this->fixture->getInheritedElement());
}

public function testElementInheritsRecursivelyWhenExtending(): void
{
$parentClass1 = new ClassDescriptor();
$parentClass1->setAbstract(true);
$parentClass1->setFullyQualifiedStructuralElementName(new Fqsen('\My\AbstractClass1'));
$parentMethod1 = new MethodDescriptor();
$parentMethod1->setName('myMethod');
$parentMethod1->setParent($parentClass1);
$parentClass1->getMethods()->set($parentMethod1->getName(), $parentMethod1);

$parentClass2 = new ClassDescriptor();
$parentClass2->setAbstract(true);
$parentClass2->setFullyQualifiedStructuralElementName(new Fqsen('\My\AbstractClass2'));
$parentClass2->setParent($parentClass1);

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setParent($parentClass2);
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->assertSame($parentMethod1, $this->fixture->getInheritedElement());
}

public function testElementInheritsWhenImplementing(): void
{
$interface1 = new InterfaceDescriptor();
$interface1->setFullyQualifiedStructuralElementName(new Fqsen('\My\Interface1'));

$interface2 = new InterfaceDescriptor();
$interface2->setFullyQualifiedStructuralElementName(new Fqsen('\My\Interface2'));
$interface2->setParent(new Collection([$interface1]));
$interfaceMethod2 = new MethodDescriptor();
$interfaceMethod2->setName('myMethod');
$interface2->setMethods(new Collection([$interfaceMethod2]));
$interface2->getMethods()->set($interfaceMethod2->getName(), $interfaceMethod2);

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setInterfaces(new Collection([$interface1, $interface2]));
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->assertSame($interfaceMethod2, $this->fixture->getInheritedElement());
}

public function testElementInheritsRecursivelyWhenImplementing(): void
{
$interface1 = new InterfaceDescriptor();
$interface1->setFullyQualifiedStructuralElementName(new Fqsen('\My\Interface1'));
$interfaceMethod1 = new MethodDescriptor();
$interfaceMethod1->setName('myMethod');
$interfaceMethod1->setParent($interface1);
$interface1->setMethods(new Collection([$interfaceMethod1]));
$interface1->getMethods()->set($interfaceMethod1->getName(), $interfaceMethod1);

$interface2 = new InterfaceDescriptor();
$interface2->setFullyQualifiedStructuralElementName(new Fqsen('\My\Interface2'));
$interface2->setParent(new Collection([$interface1]));

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setInterfaces(new Collection([$interface2]));
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->assertSame($interfaceMethod1, $this->fixture->getInheritedElement());
}

public function testElementInheritsRecursivelyWhenExtendingAndImplementing(): void
{
$interface = new InterfaceDescriptor();
$interface->setFullyQualifiedStructuralElementName(new Fqsen('\My\Interface'));
$interfaceMethod = new MethodDescriptor();
$interfaceMethod->setName('myMethod');
$interfaceMethod->setParent($interface);
$interface->setMethods(new Collection([$interfaceMethod]));
$interface->getMethods()->set($interfaceMethod->getName(), $interfaceMethod);

$parentClass = new ClassDescriptor();
$parentClass->setAbstract(true);
$parentClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\AbstractClass'));
$parentClass->setInterfaces(new Collection([$interface]));

$associatedClass = new ClassDescriptor();
$associatedClass->setFullyQualifiedStructuralElementName(new Fqsen('\My\Class'));
$associatedClass->setParent($parentClass);
$this->fixture->setName('myMethod');
$this->fixture->setParent($associatedClass);
$associatedClass->getMethods()->set($this->fixture->getName(), $this->fixture);

$this->assertSame($interfaceMethod, $this->fixture->getInheritedElement());
}

/** @covers \phpDocumentor\Descriptor\DescriptorAbstract::getAuthor */
public function testAuthorTagsInheritWhenNoneArePresent(): void
{
Expand Down

0 comments on commit 59f5d11

Please sign in to comment.