diff --git a/CHANGELOG.md b/CHANGELOG.md index d72efb7a..30a6ba7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,13 @@ This project adheres to [Semantic Versioning](https://semver.org/). The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com). ## [Unreleased] +### Fixed +- The `moodle.NamingConventions.ValidFunctionName` sniff will now ignore errors on methods employing the `#[\Override]` attribute. + ## [v3.4.9] - 2024-06-19 ### Fixed - Fixed a recent regression by allowing to the `moodle.Files.BoilerplateComment` sniff to contain "extra" consecutive comment lines immediately after the official boilerplate ends. +- The `moodle.NamingConventions.ValidFunctionName` sniff will now ignore errors on methods employing the `#[\Override]` attribute. ## [v3.4.8] - 2024-06-14 ### Added diff --git a/moodle/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/moodle/Sniffs/NamingConventions/ValidFunctionNameSniff.php index a0b58f7a..9c1350b6 100644 --- a/moodle/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/moodle/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -27,6 +27,7 @@ namespace MoodleHQ\MoodleCS\moodle\Sniffs\NamingConventions; +use MoodleHQ\MoodleCS\moodle\Util\Attributes; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\AbstractScopeSniff; use PHP_CodeSniffer\Util\Tokens; @@ -108,6 +109,11 @@ protected function processTokenWithinScope(File $phpcsfile, $stackptr, $currscop $scope = $methodprops['scope']; $scopespecified = $methodprops['scope_specified']; + if (Attributes::hasOverrideAttribute($phpcsfile, $stackptr)) { + // This method has an `#[\Override]` attribute, so it is allowed to have a different name. + return; + } + // Only lower-case accepted. if ( preg_match('/[A-Z]+/', $methodname) && diff --git a/moodle/Tests/Util/AttributesTest.php b/moodle/Tests/Util/AttributesTest.php index 5351db82..68c90388 100644 --- a/moodle/Tests/Util/AttributesTest.php +++ b/moodle/Tests/Util/AttributesTest.php @@ -184,4 +184,99 @@ function foo() {} $this->assertNull(Attributes::getAttributeProperties($phpcsFile, $searchPtr)); } + +/** + * @dataProvider hasOverrideAttributeProvider + */ + public function testHasOverrideAttribute( + string $content, + $stackPtrSearch, + bool $expected + ): void { + $config = new Config([]); + $ruleset = new Ruleset($config); + + $phpcsFile = new DummyFile($content, $ruleset, $config); + $phpcsFile->process(); + + $searchPtr = $phpcsFile->findNext($stackPtrSearch, 0); + + $this->assertEquals($expected, Attributes::hasOverrideAttribute($phpcsFile, $searchPtr)); + } + + public static function hasOverrideAttributeProvider(): array { + return [ + 'Not in a method' => [ + '<?php + protected $example; + function exampleFunction(string $param): void {}', + T_PROPERTY, + false, + ], + 'Not in a class' => [ + '<?php + function exampleFunction(string $param): void {}', + T_FUNCTION, + false, + ], + 'Not in a class, has Override' => [ + '<?php + #[\Override] + function exampleFunction(string $param): void {}', + T_FUNCTION, + false + ], + 'In a class, no Override' => [ + '<?php + class Example { + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + false, + ], + 'In a class, does not extend/implement, has Override' => [ + '<?php + class Example { + #[\Override] + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + false, + ], + 'In a class, extends, no Override' => [ + '<?php + class Example extends OtherExample { + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + false, + ], + 'In a class, implements, no Override' => [ + '<?php + class Example implements OtherExample { + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + false, + ], + 'In a class, extends, has Override' => [ + '<?php + class Example extends OtherExample { + #[\Override] + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + true, + ], + 'In a class, implements, has Override' => [ + '<?php + class Example implements OtherExample { + #[\Override] + function exampleFunction(string $param): void {} + }', + T_FUNCTION, + true, + ], + ]; + } } diff --git a/moodle/Tests/fixtures/namingconventions/validfunctionname_correct.php b/moodle/Tests/fixtures/namingconventions/validfunctionname_correct.php index 218ed54e..3171e7b0 100644 --- a/moodle/Tests/fixtures/namingconventions/validfunctionname_correct.php +++ b/moodle/Tests/fixtures/namingconventions/validfunctionname_correct.php @@ -64,3 +64,10 @@ public function __call() { echo 'hi'; } }; + +class example extends class_with_correct_function_names { + #[\Override] + public function childMethod(): void { + echo 'hi'; + } +} diff --git a/moodle/Util/Attributes.php b/moodle/Util/Attributes.php index ef6ad71d..644aa3bf 100644 --- a/moodle/Util/Attributes.php +++ b/moodle/Util/Attributes.php @@ -20,6 +20,7 @@ use PHP_CodeSniffer\Files\File; use PHPCSUtils\Utils\Context; use PHPCSUtils\Utils\Namespaces; +use PHPCSUtils\Utils\ObjectDeclarations; /** * Utilities related to PHP Attributes. @@ -108,4 +109,65 @@ public static function getAttributeProperties( return $properties; } + + /** + * Check if a function has an \Override Attribute. + * + * Note: Override attributes can only be valid on methods of classes which extend or implement another class. + * + * @param File $phpcsFile + * @param int $stackPtr + * @return bool + */ + public static function hasOverrideAttribute( + File $phpcsFile, + int $stackPtr + ): bool { + $tokens = $phpcsFile->getTokens(); + $token = $tokens[$stackPtr]; + if ($token['code'] !== T_FUNCTION) { + // Not a function so can't have an Override Attribute. + return false; + } + + if (empty($token['conditions'])) { + // Not in a class or interface. + return false; + } + + $extendsOrImplements = false; + foreach ($token['conditions'] as $condition => $conditionCode) { + $extendsOrImplements = $extendsOrImplements || ObjectDeclarations::findExtendedClassName( + $phpcsFile, + $condition + ); + $extendsOrImplements = $extendsOrImplements || ObjectDeclarations::findImplementedInterfaceNames( + $phpcsFile, + $condition + ); + $extendsOrImplements = $extendsOrImplements || ObjectDeclarations::findExtendedInterfaceNames( + $phpcsFile, + $condition + ); + + if ($extendsOrImplements) { + break; + } + } + + if (!$extendsOrImplements) { + // The OVerride attrinbute can only apply to a class which has a parent. + return false; + } + + $attributes = self::getAttributePointers($phpcsFile, $stackPtr); + foreach ($attributes as $attributePtr) { + $attribute = self::getAttributeProperties($phpcsFile, $attributePtr); + if ($attribute['attribute_name'] === '\Override') { + return true; + } + } + + return false; + } }