diff --git a/.github/workflows/php-unit-tests.yml b/.github/workflows/php-unit-tests.yml new file mode 100644 index 0000000..ec5c6a1 --- /dev/null +++ b/.github/workflows/php-unit-tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + name: PHPUnit Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + + strategy: + matrix: + php: ["8.2", "8.3"] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPUnit tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 9eee532..140133e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock vendor/ content/ .phpunit.result.cache +.phpunit.cache diff --git a/composer.json b/composer.json index 9066264..f920262 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,9 @@ "symfony/console": "^7", "symfony/filesystem": "^7" }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, "autoload": { "psr-4": { "Teak\\": "lib", diff --git a/lib/Compiler/ClassLinkList.php b/lib/Compiler/ClassLinkList.php index 7bef342..712e0d4 100644 --- a/lib/Compiler/ClassLinkList.php +++ b/lib/Compiler/ClassLinkList.php @@ -64,6 +64,20 @@ public function generate($project, $filePrefix) 'filename' => $filePrefix . mb_strtolower(str_replace("\\", '-', ltrim($class->getFqsen(), "\\"))), ]; } + + // Process traits + foreach ($file->getTraits() as $trait) { + $classReflection = new ClassReflection($trait); + + if ($classReflection->shouldIgnore()) { + continue; + } + + $classes[ltrim($trait->getFqsen()->__toString(), "\\")] = [ + 'path' => $file->getPath(), + 'filename' => $filePrefix . mb_strtolower(str_replace("\\", '-', ltrim($trait->getFqsen(), "\\"))), + ]; + } } $this->set($classes); diff --git a/lib/Compiler/Class_/ApiTable.php b/lib/Compiler/Class_/ApiTable.php index 49ec28b..fad4703 100644 --- a/lib/Compiler/Class_/ApiTable.php +++ b/lib/Compiler/Class_/ApiTable.php @@ -45,13 +45,16 @@ public function compile() $properties = $this->class->getProperties(); $methods = $this->class->getMethods(); - if (!empty($properties) || !empty($methods) || $this->class->getParent() || $this->class->getInterfaces()) { + $hasUsedTraits = !empty($this->class->getUsedTraits()); + + if (!empty($properties) || !empty($methods) || $this->class->getParent() || $this->class->getInterfaces() || $hasUsedTraits) { $contents .= (new Heading('Overview', 2))->compile(); } - if ($this->class->getParent() || $this->class->getInterfaces()) { + if ($this->class->getParent() || $this->class->getInterfaces() || $hasUsedTraits) { $contents .= (new ParentList($this->class))->compile(); $contents .= (new InterfaceList($this->class))->compile(); + $contents .= (new TraitList($this->class))->compile(); $contents .= self::NEWLINE; } diff --git a/lib/Compiler/Class_/TraitList.php b/lib/Compiler/Class_/TraitList.php new file mode 100644 index 0000000..276fe0d --- /dev/null +++ b/lib/Compiler/Class_/TraitList.php @@ -0,0 +1,53 @@ +class = $class; + } + + /** + * Compile. + * + * @return string + */ + public function compile() + { + $contents = ''; + + $usedTraits = $this->class->getUsedTraits(); + + if (!empty($usedTraits)) { + $traitNames = array_map(function ($trait) { + return '`' . ltrim((string) $trait, '\\') . '`'; + }, $usedTraits); + + if (count($traitNames) === 1) { + $contents .= '*This class uses the trait ' . reset($traitNames) . '*'; + } else { + $contents .= '*This class uses the traits ' . implode(', ', $traitNames) . '*'; + } + } + + return $contents . self::BREAK; + } +} diff --git a/lib/Console/ClassReferenceHandler.php b/lib/Console/ClassReferenceHandler.php index c67485b..4e6bcae 100644 --- a/lib/Console/ClassReferenceHandler.php +++ b/lib/Console/ClassReferenceHandler.php @@ -3,6 +3,7 @@ namespace Teak\Console; use phpDocumentor\Reflection\Php\Class_; +use phpDocumentor\Reflection\Php\Trait_; use phpDocumentor\Reflection\Php\ProjectFactory; use phpDocumentor\Reflection\Project; use Teak\Compiler\ClassLinkList; @@ -72,10 +73,14 @@ protected function generateClassList($project) $files = $project->getFiles(); $classMap = []; + $traitMap = []; foreach ($files as $file) { foreach ($file->getClasses() as $class) { - $classMap[$class->getFqsen()->__toString() ] = $class; + $classMap[$class->getFqsen()->__toString()] = $class; + } + foreach ($file->getTraits() as $trait) { + $traitMap[$trait->getFqsen()->__toString()] = $trait; } } @@ -88,6 +93,27 @@ protected function generateClassList($project) $classReflection->addParentInformation($parent); } + // Add trait information + if (method_exists($class, 'getUsedTraits')) { + foreach ($class->getUsedTraits() as $usedTrait) { + $trait = $traitMap[$usedTrait->__toString()] ?? null; + if ($trait) { + $classReflection->addTraitInformation($trait); + } + } + } + + if ($classReflection->shouldIgnore()) { + continue; + } + + $classReflections[] = $classReflection; + } + + // Process traits + foreach ($file->getTraits() as $trait) { + $classReflection = new ClassReflection($trait); + if ($classReflection->shouldIgnore()) { continue; } @@ -117,7 +143,7 @@ protected function getParentsRecursive($class, $classMap) return $parents; } - public function getProject() : Project + public function getProject(): Project { return $this->project; } diff --git a/lib/Reflection/ClassReflection.php b/lib/Reflection/ClassReflection.php index a84ae77..456cae7 100644 --- a/lib/Reflection/ClassReflection.php +++ b/lib/Reflection/ClassReflection.php @@ -9,11 +9,13 @@ class ClassReflection extends Reflection { private array $parentMethods = []; private array $parentProperties = []; + private array $traitMethods = []; + private array $traitProperties = []; /** * ClassReflection constructor. * - * @param \phpDocumentor\Reflection\Php\Class_ $reflection + * @param \phpDocumentor\Reflection\Php\Class_|\phpDocumentor\Reflection\Php\Trait_ $reflection */ public function __construct($reflection) { @@ -33,6 +35,19 @@ public function addParentInformation($parentClass) $this->parentProperties = $parentClass->getProperties(); } + public function addTraitInformation($trait) + { + $methods = array_map(function ($method) { + return $method->getName(); + }, $this->traitMethods); + $trait_methods = array_filter($trait->getMethods(), function ($trait_method) use ($methods) { + return !in_array($trait_method->getName(), $methods); + }); + + $this->traitMethods = array_merge($this->traitMethods, $trait_methods); + $this->traitProperties = array_merge($this->traitProperties, $trait->getProperties()); + } + public function hasMethods() { if (method_exists($this, 'getMethods')) { @@ -46,7 +61,19 @@ public function getMethods() { $methods = $this->reflection->getMethods(); - if ($this->reflection->getParent() && !empty($this->parentMethods)) { + // Add trait methods first (so they can be overridden by class/parent methods) + if (!empty($this->traitMethods)) { + $method_names = array_map(function ($method) { + return $method->getName(); + }, $methods); + $trait_methods = array_filter($this->traitMethods, function ($trait_method) use ($method_names) { + return !in_array($trait_method->getName(), $method_names); + }); + + $methods = array_merge($methods, $trait_methods); + } + + if (method_exists($this->reflection, 'getParent') && $this->reflection->getParent() && !empty($this->parentMethods)) { // Filter out parent methods that are already defined in the child class. $method_names = array_map(function ($method) { return $method->getName(); @@ -74,7 +101,12 @@ public function getProperties() { $properties = $this->reflection->getProperties(); - if ($this->reflection->getParent() && !empty($this->parentProperties)) { + // Add trait properties + if (!empty($this->traitProperties)) { + $properties = array_merge($properties, $this->traitProperties); + } + + if (method_exists($this->reflection, 'getParent') && $this->reflection->getParent() && !empty($this->parentProperties)) { $properties = array_merge($properties, $this->parentProperties); } @@ -92,21 +124,35 @@ public function getProperties() public function getParent() { - return $this->reflection->getParent(); + if (method_exists($this->reflection, 'getParent')) { + return $this->reflection->getParent(); + } + return null; } public function getInterfaces() { - return $this->reflection->getInterfaces(); + if (method_exists($this->reflection, 'getInterfaces')) { + return $this->reflection->getInterfaces(); + } + return []; } - public function getParentMethods() : array + public function getParentMethods(): array { return $this->parentMethods; } - public function getParentProperties() : array + public function getParentProperties(): array { return $this->parentProperties; } + + public function getUsedTraits(): array + { + if (method_exists($this->reflection, 'getUsedTraits')) { + return $this->reflection->getUsedTraits(); + } + return []; + } } diff --git a/lib/Reflection/Reflection.php b/lib/Reflection/Reflection.php index fa5e629..c6b394d 100644 --- a/lib/Reflection/Reflection.php +++ b/lib/Reflection/Reflection.php @@ -3,6 +3,7 @@ namespace Teak\Reflection; use phpDocumentor\Reflection\Php\Class_; +use phpDocumentor\Reflection\Php\Trait_; use phpDocumentor\Reflection\Php\Method; use phpDocumentor\Reflection\Php\Property; @@ -18,7 +19,7 @@ class Reflection /** * Reflection constructor. * - * @param Class_|Method|Property $reflection + * @param Class_|Trait_|Method|Property $reflection */ public function __construct($reflection) { @@ -29,23 +30,28 @@ public function __construct($reflection) } } - public function getName() { + public function getName() + { return $this->reflection->getName(); } - public function hasDocBlock() { + public function hasDocBlock() + { return !empty($this->getDocBlock()); } - public function getDocBlock() { + public function getDocBlock() + { return $this->docBlock; } - public function hasSummary() { + public function hasSummary() + { return !empty($this->getSummary()); } - public function getSummary() { + public function getSummary() + { if (!$this->docBlock) { return ''; } @@ -53,11 +59,13 @@ public function getSummary() { return $this->docBlock->getSummary(); } - public function hasDescription() { + public function hasDescription() + { return !empty($this->getDescription()); } - public function getDescription() { + public function getDescription() + { if (!$this->docBlock) { return ''; } @@ -113,7 +121,8 @@ public function isDeprecated() return $this->hasTag('deprecated'); } - public function getDeprecationMessage() { + public function getDeprecationMessage() + { if ($this->hasTag('deprecated')) { return $this->getTag('deprecated')->getDescription(); } @@ -121,7 +130,8 @@ public function getDeprecationMessage() { return null; } - public function getTitle() { + public function getTitle() + { $title = $this->reflection->getFqsen(); $title = ltrim('\\', $title); @@ -133,13 +143,14 @@ public function getTitle() { * * @return bool */ - public function shouldIgnore() { + public function shouldIgnore() + { return !$this->docBlock - || !$this->hasTag('api') - || $this->hasTag('ignore') - || $this->hasTag('internal') - || (method_exists($this->reflection, 'getVisibility') - && $this->reflection->getVisibility() === 'private' - ); + || !$this->hasTag('api') + || $this->hasTag('ignore') + || $this->hasTag('internal') + || (method_exists($this->reflection, 'getVisibility') + && $this->reflection->getVisibility() === 'private' + ); } } diff --git a/phpunit.xml b/phpunit.xml index 8afa0ff..3a3cdb2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,22 @@ - + + tests + + + lib + + + diff --git a/tests/ClassCompilerTest.php b/tests/ClassCompilerTest.php index 1cc1960..d0e0dcb 100644 --- a/tests/ClassCompilerTest.php +++ b/tests/ClassCompilerTest.php @@ -21,7 +21,7 @@ class ClassCompilerTest extends TestCase */ public $application; - public function setUp() : void + public function setUp(): void { parent::setUp(); @@ -56,4 +56,70 @@ public function testDefault() $fs->dumpFile(__DIR__ . '/output/TestClass.md', $output);*/ // $this->assertContains('Username: Wouter', $output); } + + public function testTraitDocumentation() + { + $command = $this->application->find('generate:class-reference'); + + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'files' => ABSPATH . '/testclasses/TestTrait.php', + '--output' => './../temp', + ]); + + $files = $command->getFiles($commandTester->getInput()); + + $classReferenceHandler = new ClassReferenceHandler($files); + + $classList = $classReferenceHandler->getClassList(); + + // Assert that traits are being documented + $this->assertNotEmpty($classList, 'Traits should be included in the class list'); + + foreach ($classList as $class) { + $contents = $classReferenceHandler->compileClass($class); + + // Verify the compiled output contains trait documentation + $this->assertNotEmpty($contents, 'Trait documentation should not be empty'); + $this->assertStringContainsString('TestTrait', $contents, 'Trait name should appear in documentation'); + } + } + + public function testClassWithTraitShowsTraitMethods() + { + $command = $this->application->find('generate:class-reference'); + + $commandTester = new CommandTester($command); + $commandTester->execute([ + 'command' => $command->getName(), + 'files' => ABSPATH . '/testclasses/', + '--output' => './../temp', + ]); + + $files = $command->getFiles($commandTester->getInput()); + + $classReferenceHandler = new ClassReferenceHandler($files); + + $classList = $classReferenceHandler->getClassList(); + + $this->assertNotEmpty($classList, 'Class list should not be empty'); + + foreach ($classList as $class) { + if ($class->getName() === 'TestClass') { + $contents = $classReferenceHandler->compileClass($class); + + // Verify the class shows it uses the trait + $this->assertStringContainsString('This class uses the trait', $contents, 'Class should show it uses a trait'); + $this->assertStringContainsString('TestTrait', $contents, 'Trait name should appear in trait usage statement'); + + // Verify the class documentation contains trait methods + $this->assertStringContainsString('trait_method', $contents, 'Trait method should appear in class documentation'); + $this->assertStringContainsString('Trait method summary', $contents, 'Trait method summary should appear in class documentation'); + + // Verify trait properties are included + $this->assertStringContainsString('trait_property', $contents, 'Trait property should appear in class documentation'); + } + } + } } diff --git a/tests/testclasses/TestClass.php b/tests/testclasses/TestClass.php index e6c9822..5f19ad6 100644 --- a/tests/testclasses/TestClass.php +++ b/tests/testclasses/TestClass.php @@ -22,20 +22,20 @@ */ class TestClass { + use TestTrait; + /** * Public property summary. * - * @api - * @var string Public property description. - */ - public $public_property; + * @api + * @var string Public property description. + */ + public $public_property; /** * TestClass constructor. */ - public function __construct() { - - } + public function __construct() {} /** * Summary. @@ -54,7 +54,8 @@ public function __construct() { * * @return string Description. */ - public function default_method( $string_var, $int_var ) { + public function default_method($string_var, $int_var) + { return 'Description'; } @@ -79,7 +80,7 @@ public function no_param_return_empty_void() {} * * @param string $string_var Parameter description. */ - public function string_empty( $string_var ) {} + public function string_empty($string_var) {} /** * Function summary @@ -87,7 +88,8 @@ public function string_empty( $string_var ) {} * @param string $string_var Parameter description. * @return string $string_return Return description. */ - public function doc_param_string_return_string( $string_var ) { + public function doc_param_string_return_string($string_var) + { $string_return = ''; return $string_return; diff --git a/tests/testclasses/TestTrait.php b/tests/testclasses/TestTrait.php new file mode 100644 index 0000000..9fa3463 --- /dev/null +++ b/tests/testclasses/TestTrait.php @@ -0,0 +1,50 @@ +