diff --git a/.phplint.yml b/.phplint.yml index 35dda97..fd69190 100644 --- a/.phplint.yml +++ b/.phplint.yml @@ -2,3 +2,4 @@ path: ./ exclude: - vendor - moodle/Tests/fixtures + - moodle/Tests/Sniffs/Namespaces/fixtures diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e66c72..83a64c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ The format of this change log follows the advice given at [Keep a CHANGELOG](htt ## [Unreleased] +## [v3.3.6] - 2023-09-15 +### Added +- A new `moodle-extra` coding standard which moves towards a more PSR-12 compliant coding style. +- Enforce the use of the short array syntax (`[]`), warning about the long alternative (`array()`): `Generic.Arrays.DisallowLongArraySyntax`. This will be raised from `warning` to `error` in 1 year. + +## [v3.3.5] - 2023-08-28 +### Changed +- Update composer dependencies to current versions, notably PHPCompatibility (0a17f9ed). +- Enforce the use of `&&` and `||` logical operators, **now erroring** (after a grace period of 1 year) with `and` and `or` uses: `Squiz.Operators.ValidLogicalOperators` + ## [v3.3.4] - 2023-05-28 ### Changed - Update composer dependencies to current versions, notably PHPCompatibility (70e4ca24). @@ -51,7 +61,9 @@ All features are maintained and no new features have been introduced to either t All the details about [previous releases] can be found in [local_codechecker](https://github.com/moodlehq/moodle-local_codechecker) own change log. -[Unreleased]: https://github.com/moodlehq/moodle-cs/compare/v3.3.4...main +[Unreleased]: https://github.com/moodlehq/moodle-cs/compare/v3.3.6...main +[v3.3.6]: https://github.com/moodlehq/moodle-cs/compare/v3.3.5...v3.3.6 +[v3.3.5]: https://github.com/moodlehq/moodle-cs/compare/v3.3.4...v3.3.5 [v3.3.4]: https://github.com/moodlehq/moodle-cs/compare/v3.3.3...v3.3.4 [v3.3.3]: https://github.com/moodlehq/moodle-cs/compare/v3.3.2...v3.3.3 [v3.3.2]: https://github.com/moodlehq/moodle-cs/compare/v3.3.1...v3.3.2 diff --git a/README.md b/README.md index 0b17747..84ec567 100644 --- a/README.md +++ b/README.md @@ -16,81 +16,72 @@ ## Information -This repository contains the Moodle Coding Style configuration. +This repository contains the Moodle Coding Style configurations, written as [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) rulesets. -Currently this only includes the configuration for PHP Coding style, but this -may be extended to include custom rules for JavaScript, and any other supported -languages or syntaxes. +Two coding styles are included: +- `moodle` - the main ruleset for the [Moodle Coding Style](https://moodledev.io/general/development/policies/codingstyle) +- `moodle-extra` - extended ruleset which includes recommended best practices + - extends the main `moodle` ruleset + +Currently this only includes the configuration for PHP Coding style, but this may be extended to include custom rules for JavaScript, and any other supported languages or syntaxes. ## Installation -### Using Composer +### Using Composer (recommended) -You can include these coding style rules using Composer to make them available -globally across your system. +You can install these coding style rules using Composer to make them available globally across your system. -This will install the correct version of phpcs, with the Moodle rules, and their -dependencies. +This will install the correct version of phpcs, with the Moodle rules, and their dependencies. -``` +```shell composer global require moodlehq/moodle-cs ``` -### As a part of moodle-local_codechecker - -This plugin is included as part of the [moodle-local_codechecker -plugin](https://github.com/moodlehq/moodle-local_codechecker). - - ## Configuration -You can set the Moodle standard as the system default: -``` -phpcs --config-set default_standard moodle -``` - -This will inform most IDEs automatically. -Alternatively you can configuration your IDE to use phpcs with the Moodle -ruleset as required. - +Typically configuration is not required. Recent versions of Moodle (3.11 onwards) include a configuration file for the PHP CodeSniffer, which will set the standard when run within a Moodle directory. -### IDE Integration +Additional configuration can be generated automatically to have PHP CodeSniffer ignore any third-party library code. This can be generated by running: -#### PhpStorm - -1. Open PhpStorm preferences -2. Go to Inspections > PHP > PHP Code Sniffer Validation -3. In the 'coding standard' dropdown, select 'moodle' +```shell +npx grunt ignorefiles +``` -#### Sublime Text +### Using the `moodle-extra` coding style -Find documentation [here](https://docs.moodle.org/dev/Setting_up_Sublime2#Sublime_PHP_CS). +The recommended way of configuring PHP CodeSniffer to use the `moodle-extra` coding style is to provide an additional configuration file. -1. Go in your Sublime Text to Preferences -> Package Control -> Package Control: Install Package -2. Write 'phpcs' in the search field, if you see Phpcs and SublimeLinter-phpcs, click on them to install them. -3. If not, check if they are already installed Preferences -> Package Control -> Package Control: Remove Package. -4. To set your codecheck to moodle standards go to Preferences -> Package Settings -> PHP Code Sniffer -> Settings-User and write: +For Moodle 3.11 onwards you can create a file named `.phpcs.xml` with the following contents: - { "phpcs_additional_args": { - "--standard": "moodle", - "-n": " - }, - } +```xml + + + + + +``` -5. If you don’t have the auto-save plugin turned on, YOU’RE DONE! -6. If you have the auto-save plugin turned on, because the codecheck gets triggered on save, the quick panel will keep popping making it impossible to type. - To stop quick panel from showing go to Settings-User file and add: +This will load the `phpcs.xml` file (generated by `npx grunt ignorefiles`), and apply the `moodle-extra` configuration on top. - "phpcs_show_quick_panel": false, +### Moodle 3.10 and earlier - The line with the error will still get marked and if you’ll click on it you’ll see the error text in the status bar. +The easiset way to have PHP CodeSniffer pick up your preferred style, you can create a file named `phpcs.xml` with the following contents: -#### VSCode +```xml + + + + +``` -Find documentation [here](https://docs.moodle.org/dev/Setting_up_VSCode#PHP_CS). +If you wish to use the `moodle-extra` coding style, then you can use the following content: -1. Install [PHPSniffer](https://marketplace.visualstudio.com/items?itemName=wongjn.php-sniffer). -2. Open VSCode settings.json and add the following setting to define standard PHP CS (if you haven't set it as default in your system): +```xml + + + + +``` - "phpSniffer.standard": "moodle", +Note: Third-party library code will not be ignored with these versions of Moodle. diff --git a/composer.json b/composer.json index a488ec3..3ec2e09 100644 --- a/composer.json +++ b/composer.json @@ -12,12 +12,17 @@ { "name": "Andrew Lyons", "email": "andrew@nicols.co.uk" + }, + { + "name": "Eloy Lafuente", + "email": "stronk7@moodle.com" } ], "require": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", "squizlabs/php_codesniffer": "^3.7.2", - "phpcompatibility/php-compatibility": "dev-develop#70e4ca24" + "phpcsstandards/phpcsextra": "^1.1.0", + "phpcompatibility/php-compatibility": "dev-develop#0a17f9ed" }, "config": { "allow-plugins": { diff --git a/moodle-extra/ruleset.xml b/moodle-extra/ruleset.xml new file mode 100644 index 0000000..5a894bb --- /dev/null +++ b/moodle-extra/ruleset.xml @@ -0,0 +1,99 @@ + + + + Best Practices for Moodle development beyond the core Coding Standards + + + + + + + error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/moodle/Sniffs/Namespaces/NamespaceStatementSniff.php b/moodle/Sniffs/Namespaces/NamespaceStatementSniff.php new file mode 100644 index 0000000..19923b3 --- /dev/null +++ b/moodle/Sniffs/Namespaces/NamespaceStatementSniff.php @@ -0,0 +1,71 @@ +. + +/** + * Checks that each file contains the standard GPL comment. + * + * @package moodle-cs + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace MoodleHQ\MoodleCS\moodle\Sniffs\Namespaces; + +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; + +// phpcs:disable moodle.NamingConventions + +class NamespaceStatementSniff implements Sniff { + public function register() + { + return [ + T_NAMESPACE, + ]; + } + + public function process(File $file, $stackPtr) + { + $tokens = $file->getTokens(); + // Format should be: + // - T_NAMESPACE + // - T_WHITESPACE + // - T_STRING + + $checkPtr = $stackPtr + 2; + $token = $tokens[$checkPtr]; + if ($token['code'] === T_NS_SEPARATOR) { + $fqdn = ''; + $stop = $file->findNext(Tokens::$emptyTokens, ($stackPtr + 2)); + for ($i = $stackPtr + 2; $i < $stop; $i++) { + $fqdn .= $tokens[$i]['content']; + } + $fix = $file->addFixableError( + 'Namespace should not start with a slash: %s', + $checkPtr, + 'LeadingSlash', + [$fqdn] + ); + + if ($fix) { + $file->fixer->beginChangeset(); + $file->fixer->replaceToken($checkPtr, ''); + $file->fixer->endChangeset(); + } + } + } +} diff --git a/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php b/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php index 78c7094..37b3367 100644 --- a/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php +++ b/moodle/Sniffs/PHPUnit/TestCaseCoversSniff.php @@ -49,11 +49,16 @@ public function process(File $file, $pointer) { // Before starting any check, let's look for various things. - // Get the moodle branch being analysed. - $moodleBranch = MoodleUtil::getMoodleBranch($file); + // If we aren't checking Moodle 4.0dev (400) and up, nothing to check. + // Make and exception for codechecker phpunit tests, so they are run always. + if (!MoodleUtil::meetsMinimumMoodleVersion($file, 400) && !MoodleUtil::isUnitTestRunning()) { + return; // @codeCoverageIgnore + } - // Detect if we are running PHPUnit. - $runningPHPUnit = defined('PHPUNIT_TEST') && PHPUNIT_TEST; + // If the file is not a unit test file, nothing to check. + if (!MoodleUtil::isUnitTest($file) && !MoodleUtil::isUnitTestRunning()) { + return; // @codeCoverageIgnore + } // We have all we need from core, let's start processing the file. @@ -70,24 +75,6 @@ public function process(File $file, $pointer) { return; // @codeCoverageIgnore } - // If we aren't checking Moodle 4.0dev (400) and up, nothing to check. - // Make and exception for codechecker phpunit tests, so they are run always. - if (isset($moodleBranch) && $moodleBranch < 400 && !$runningPHPUnit) { - return; // @codeCoverageIgnore - } - - // If the file isn't under tests directory, nothing to check. - if (stripos($file->getFilename(), '/tests/') === false) { - return; // @codeCoverageIgnore - } - - // If the file isn't called, _test.php, nothing to check. - // Make an exception for codechecker own phpunit fixtures here, allowing any name for them. - $fileName = basename($file->getFilename()); - if (substr($fileName, -9) !== '_test.php' && !$runningPHPUnit) { - return; // @codeCoverageIgnore - } - // Iterate over all the classes (hopefully only one, but that's not this sniff problem). $cStart = $pointer; while ($cStart = $file->findNext(T_CLASS, $cStart + 1)) { diff --git a/moodle/Sniffs/PHPUnit/TestCaseNamesSniff.php b/moodle/Sniffs/PHPUnit/TestCaseNamesSniff.php index c15da81..7b57de3 100644 --- a/moodle/Sniffs/PHPUnit/TestCaseNamesSniff.php +++ b/moodle/Sniffs/PHPUnit/TestCaseNamesSniff.php @@ -68,7 +68,7 @@ public function process(File $file, $pointer) { $moodleComponent = MoodleUtil::getMoodleComponent($file); // Detect if we are running PHPUnit. - $runningPHPUnit = defined('PHPUNIT_TEST') && PHPUNIT_TEST; + $runningPHPUnit = MoodleUtil::isUnitTestRunning(); // We have all we need from core, let's start processing the file. @@ -81,17 +81,11 @@ public function process(File $file, $pointer) { return; // @codeCoverageIgnore } - // If the file isn't under tests directory, nothing to check. - if (stripos($file->getFilename(), '/tests/') === false) { + // If the file is not a unit test file, nothing to check. + if (!MoodleUtil::isUnitTest($file) && !$runningPHPUnit) { return; // @codeCoverageIgnore } - - // If the file isn't called, _test.php, nothing to check. - // Make an exception for codechecker own phpunit fixtures here, allowing any name for them. $fileName = basename($file->getFilename()); - if (substr($fileName, -9) !== '_test.php' && !$runningPHPUnit) { - return; // @codeCoverageIgnore - } // In order to cover the duplicates detection, we need to set some // properties (caches) here. It's extremely hard to do diff --git a/moodle/Sniffs/PHPUnit/TestCaseProviderSniff.php b/moodle/Sniffs/PHPUnit/TestCaseProviderSniff.php new file mode 100644 index 0000000..b396072 --- /dev/null +++ b/moodle/Sniffs/PHPUnit/TestCaseProviderSniff.php @@ -0,0 +1,366 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit; + +// phpcs:disable moodle.NamingConventions + +use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\FunctionDeclarations; + +/** + * Checks that a test file has the @coversxxx annotations properly defined. + * + * @package local_codechecker + * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class TestCaseProviderSniff implements Sniff { + + /** + * Whether to autofix static providers. + * + * @var bool + */ + public $autofixStaticProviders = false; + + /** + * Register for open tag (only process once per file). + */ + public function register(): array + { + return [ + T_OPEN_TAG, + ]; + } + + /** + * Processes php files and perform various checks with file. + * + * @param File $file The file being scanned. + * @param int $pointer The position in the stack. + */ + public function process(File $file, $pointer): void + { + // Before starting any check, let's look for various things. + + // If we aren't checking Moodle 4.0dev (400) and up, nothing to check. + // Make and exception for codechecker phpunit tests, so they are run always. + if (!MoodleUtil::meetsMinimumMoodleVersion($file, 400) && !MoodleUtil::isUnitTestRunning()) { + return; // @codeCoverageIgnore + } + + // If the file is not a unit test file, nothing to check. + if (!MoodleUtil::isUnitTest($file) && !MoodleUtil::isUnitTestRunning()) { + return; // @codeCoverageIgnore + } + + // We have all we need from core, let's start processing the file. + + // Get the file tokens, for ease of use. + $tokens = $file->getTokens(); + + // In various places we are going to ignore class/method prefixes (private, abstract...) + // and whitespace, create an array for all them. + $skipTokens = Tokens::$methodPrefixes + [T_WHITESPACE => T_WHITESPACE]; + + // Iterate over all the classes (hopefully only one, but that's not this sniff problem). + $cStart = $pointer; + while ($cStart = $file->findNext(T_CLASS, $cStart + 1)) { + $class = $file->getDeclarationName($cStart); + + // Only if the class is extending something. + // TODO: We could add a list of valid classes once we have a class-map available. + if (!$file->findNext(T_EXTENDS, $cStart + 1, $tokens[$cStart]['scope_opener'])) { + continue; + } + + // Ignore any classname which does not end in "_test". + if (substr($class, -5) !== '_test') { + continue; + } + + // Iterate over all the methods in the class. + $mStart = $cStart; + while ($mStart = $file->findNext(T_FUNCTION, $mStart + 1, $tokens[$cStart]['scope_closer'])) { + $method = $file->getDeclarationName($mStart); + + // Ignore non test_xxxx() methods. + if (strpos($method, 'test_') !== 0) { + continue; + } + + // Let's see if the method has any phpdoc block (first non skip token must be end of phpdoc comment). + $docPointer = $file->findPrevious($skipTokens, $mStart - 1, null, true); + + // Found a phpdoc block, let's look for @dataProvider tag. + if ($tokens[$docPointer]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $docStart = $tokens[$docPointer]['comment_opener']; + while ($docPointer) { // Let's look upwards, until the beginning of the phpdoc block. + $docPointer = $file->findPrevious(T_DOC_COMMENT_TAG, $docPointer - 1, $docStart); + if ($docPointer) { + $docTag = trim($tokens[$docPointer]['content']); + $docTagLC = strtolower($docTag); + switch ($docTagLC) { + case '@dataprovider': + // Validate basic syntax (FQCN or ::). + $this->checkDataProvider($file, $docPointer); + break; + } + } + } + } + + // Advance until the end of the method, if possible, to find the next one quicker. + $mStart = $tokens[$mStart]['scope_closer'] ?? $pointer + 1; + } + } + } + + /** + * Perform a basic syntax cheking of the values of the @dataProvider tag. + * + * @param File $file The file being scanned + * @param int $pointer pointer to the token that contains the tag. Calculations are based on that. + * @return void + */ + protected function checkDataProvider( + File $file, + int $pointer + ) { + // Get the file tokens, for ease of use. + $tokens = $file->getTokens(); + $tag = $tokens[$pointer]['content']; + $methodName = $tokens[$pointer + 2]['content']; + $testPointer = $file->findNext(T_FUNCTION, $pointer + 2); + $testName = FunctionDeclarations::getName($file, $testPointer); + + if ($tag !== '@dataProvider') { + $fix = $file->addFixableError( + 'Wrong @dataProvider tag: %s provided, @dataProvider expected', + $pointer, + 'dataProviderNaming', + [$tag] + ); + + if ($fix) { + $file->fixer->beginChangeset(); + $file->fixer->replaceToken($pointer, '@dataProvider'); + $file->fixer->endChangeset(); + } + } + + if ($tokens[$pointer + 2]['code'] !== T_DOC_COMMENT_STRING) { + $file->addError( + 'Wrong @dataProvider tag specified for test %s, it must be followed by a space and a method name.', + $pointer + 2, + 'dataProviderSyntaxMethodnameMissing', + [ + $testName, + ] + ); + + // The remaining checks all relate to the method name, so we can't continue. + return; + } + + // Check that the method name is valid. + // It must _not_ start with `test_`. + if (substr($methodName, 0, 5) === 'test_') { + $file->addError( + 'Data provider must not start with "test_". "%s" provided.', + $pointer + 2, + 'dataProviderSyntaxMethodnameInvalid', + [ + $methodName, + ] + ); + } + + // Find the method itself. + $classPointer = $file->findPrevious(T_CLASS, $pointer - 1); + $providerPointer = MoodleUtil::findClassMethodPointer($file, $classPointer, $methodName); + if ($providerPointer === null) { + $file->addError( + 'Data provider method "%s" not found.', + $pointer + 2, + 'dataProviderSyntaxMethodNotFound', + [ + $methodName, + ] + ); + + return; + } + + // https://docs.phpunit.de/en/9.6/writing-tests-for-phpunit.html#data-providers + // A data provider method must be public and either return an array of arrays + // or an object that implements the Iterator interface and yields an array for + // each iteration step. For each array that is part of the collection the test + // method will be called with the contents of the array as its arguments. + + // Check that the method is public. + $methodProps = $file->getMethodProperties($providerPointer); + if (!$methodProps['scope_specified']) { + $fix = $file->addFixableError( + 'Data provider method "%s" visibility should be specified.', + $providerPointer, + 'dataProviderSyntaxMethodVisibilityNotSpecified', + [ + $methodName, + ] + ); + + if ($fix) { + $file->fixer->beginChangeset(); + if ($methodProps['is_static']) { + $staticPointer = $file->findPrevious(T_STATIC, $providerPointer - 1); + $file->fixer->addContentBefore($staticPointer, 'public '); + } else { + $file->fixer->addContentBefore($providerPointer, 'public '); + } + $file->fixer->endChangeset(); + } + } else if ($methodProps['scope'] !== 'public') { + $scopePointer = $file->findPrevious(Tokens::$scopeModifiers, $providerPointer - 1); + $fix = $file->addFixableError( + 'Data provider method "%s" must be public.', + $scopePointer, + 'dataProviderSyntaxMethodNotPublic', + [ + $methodName, + ] + ); + + if ($fix) { + $file->fixer->beginChangeset(); + $file->fixer->replaceToken($scopePointer, 'public'); + $file->fixer->endChangeset(); + } + } + + // Check the return type. + switch ($methodProps['return_type']) { + case 'array': + case 'Generator': + case 'Iterable': + // All valid + break; + default: + $file->addError( + 'Data provider method "%s" must return an array, a Generator or an Iterable.', + $pointer + 2, + 'dataProviderSyntaxMethodInvalidReturnType', + [ + $methodName, + ] + ); + } + + // In preparation for PHPUnit 10, we want to recommend that data providers are statically defined. + if (!$methodProps['is_static']) { + $supportAutomatedFix = true; + if (!$this->autofixStaticProviders) { + $supportAutomatedFix = false; + } else { + // We can make this fixable if the method does not contain any `$this`. + // Search the body. + $currentPointer = $tokens[$providerPointer]['scope_opener'] + 1; + $bodyEnd = $tokens[$providerPointer]['scope_closer'] - 1; + while ($token = $file->findNext(T_VARIABLE, $currentPointer, $bodyEnd)) { + if ($tokens[$token]['content'] === '$this') { + $supportAutomatedFix = false; + break; + } + $currentPointer = $token + 1; + } + } + + if (!$supportAutomatedFix) { + $file->addWarning( + 'Data provider method "%s" will need to be converted to static in future.', + $pointer + 2, + 'dataProviderNotStatic', + [ + $methodName, + ] + ); + } else { + $fix = $file->addFixableWarning( + 'Data provider method "%s" will need to be converted to static in future.', + $pointer + 2, + 'dataProviderNotStatic', + [ + $methodName, + ] + ); + + if ($fix) { + $file->fixer->beginChangeset(); + $file->fixer->addContentBefore($providerPointer, "static "); + $uses = self::findMethodCalls($file, $classPointer, $methodName); + foreach ($uses as $use) { + $file->fixer->replaceToken($use['start'], 'self::' . $methodName); + $file->fixer->replaceToken($use['start'] + 1, ''); + $file->fixer->replaceToken($use['start'] + 2, ''); + } + $file->fixer->endChangeset(); + } + } + } + } + + + /** + * Find all calls to a method. + * @param File $phpcsFile + * @param int $classPtr + * @param string $methodName + * @return array + */ + protected static function findMethodCalls( + File $phpcsFile, + int $classPtr, + string $methodName + ): array + { + $data = []; + + $mStart = $classPtr; + $tokens = $phpcsFile->getTokens(); + while ($mStart = $phpcsFile->findNext(T_VARIABLE, $mStart + 1, $tokens[$classPtr]['scope_closer'])) { + if ($tokens[$mStart]['content'] !== '$this') { + continue; + } + if ($tokens[$mStart + 1]['code'] !== T_OBJECT_OPERATOR) { + continue; + } + if ($tokens[$mStart + 2]['content'] !== $methodName) { + continue; + } + + $data[] = [ + 'start' => $mStart, + 'end' => $mStart + 2, + ]; + } + + return $data; + } +} diff --git a/moodle/Tests/FilesBoilerPlateCommentTest.php b/moodle/Tests/FilesBoilerPlateCommentTest.php index 7c33068..1c4c981 100644 --- a/moodle/Tests/FilesBoilerPlateCommentTest.php +++ b/moodle/Tests/FilesBoilerPlateCommentTest.php @@ -28,7 +28,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Files\BoilerplateCommentSniff */ -class FilesBoilerPlateCommentTest extends MoodleCSBaseTest { +class FilesBoilerPlateCommentTest extends MoodleCSBaseTestCase { public function test_moodle_files_boilerplatecomment_ok() { $this->set_standard('moodle'); diff --git a/moodle/Tests/FilesMoodleInternalTest.php b/moodle/Tests/FilesMoodleInternalTest.php index d062213..561540a 100644 --- a/moodle/Tests/FilesMoodleInternalTest.php +++ b/moodle/Tests/FilesMoodleInternalTest.php @@ -28,7 +28,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Files\MoodleInternalSniff */ -class FilesMoodleInternalTest extends MoodleCSBaseTest { +class FilesMoodleInternalTest extends MoodleCSBaseTestCase { public function test_moodle_files_moodleinternal_problem() { $this->set_standard('moodle'); diff --git a/moodle/Tests/MoodleCSBaseTest.php b/moodle/Tests/MoodleCSBaseTestCase.php similarity index 84% rename from moodle/Tests/MoodleCSBaseTest.php rename to moodle/Tests/MoodleCSBaseTestCase.php index d39e158..f052a56 100644 --- a/moodle/Tests/MoodleCSBaseTest.php +++ b/moodle/Tests/MoodleCSBaseTestCase.php @@ -39,7 +39,7 @@ * @copyright 2013 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -abstract class MoodleCSBaseTest extends \PHPUnit\Framework\TestCase { +abstract class MoodleCSBaseTestCase extends \PHPUnit\Framework\TestCase { /** * @var string name of the standard to be tested. @@ -80,53 +80,18 @@ public function set_component_mapping(array $mapping): void { * * @param string $standard name of the standard to be tested. */ - protected function set_standard($standard) { - $installedStandards = \PHP_CodeSniffer\Util\Standards::getInstalledStandardDetails(); - - foreach (array_keys($installedStandards) as $standard) { - if (\PHP_CodeSniffer\Util\Standards::isInstalledStandard($standard) === false) { - // They didn't select a valid coding standard, so help them - // out by letting them know which standards are installed. - $error = 'ERROR: the "'.$standard.'" coding standard is not installed. '; - ob_start(); - \PHP_CodeSniffer\Util\Standards::printInstalledStandards(); - $error .= ob_get_contents(); - ob_end_clean(); - throw new \PHP_CodeSniffer\Exceptions\DeepExitException($error, 3); - } + protected function set_standard(string $standard) { + if (\PHP_CodeSniffer\Util\Standards::isInstalledStandard($standard) === false) { + // They didn't select a valid coding standard, so help them + // out by letting them know which standards are installed. + $error = "ERROR: the '{$standard}' coding standard is not installed.\n"; + ob_start(); + \PHP_CodeSniffer\Util\Standards::printInstalledStandards(); + $error .= ob_get_contents(); + ob_end_clean(); + throw new \PHP_CodeSniffer\Exceptions\DeepExitException($error, 3); } $this->standard = $standard; - return; - // Since 2.9 arbitrary standard directories are not allowed by default, - // only those under the CodeSniffer/Standards dir are detected. Other base - // dirs containing standards can be added using CodeSniffer.conf or the - // PHP_CODESNIFFER_CONFIG_DATA global (installed_paths setting). - // We are using the global way here to avoid changes in the phpcs import. - // phpcs:disable - if (!isset($GLOBALS['PHP_CODESNIFFER_CONFIG_DATA']['installed_paths'])) { - $localcodecheckerpath = realpath(__DIR__ . '/../'); - $GLOBALS['PHP_CODESNIFFER_CONFIG_DATA'] = ['installed_paths' => $localcodecheckerpath]; - } - // phpcs:enable - - // Basic search of standards in the allowed directories. - $stdsearch = array( - __DIR__ . '/../phpcs/src/Standards', // PHPCS standards dir. - __DIR__ . '/..', // Plugin local_codechecker dir, allowed above via global. - ); - - foreach ($stdsearch as $stdpath) { - $stdpath = realpath($stdpath . '/' . $standard); - $stdfile = $stdpath . '/ruleset.xml'; - if (file_exists($stdfile)) { - $this->standard = $stdpath; // Need to pass the path here. - break; - } - } - // Standard not found, fail. - if ($this->standard === null) { - $this->fail('Standard "' . $standard . '" not found.'); - } } /** diff --git a/moodle/Tests/MoodleStandardTest.php b/moodle/Tests/MoodleStandardTest.php index 8042d41..3f7489e 100644 --- a/moodle/Tests/MoodleStandardTest.php +++ b/moodle/Tests/MoodleStandardTest.php @@ -30,7 +30,7 @@ * * @todo Complete coverage of all Sniffs. */ -class MoodleCsStandardTest extends MoodleCSBaseTest { +class MoodleStandardTest extends MoodleCSBaseTestCase { /** * Test the PSR2.Methods.MethodDeclaration sniff. @@ -223,6 +223,33 @@ public function test_moodle_files_linelength() { $this->verify_cs_results(); } + /** + * Test the Generic.Arrays.DisallowLongArraySyntax sniff. + * + * @covers \PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays\DisallowLongArraySyntaxSniff + */ + public function test_generic_array_disallowlongarraysyntax(): void { + // Define the standard, sniff and fixture to use. + $this->set_standard('moodle'); + $this->set_sniff('Generic.Arrays.DisallowLongArraySyntax'); + $this->set_fixture(__DIR__ . '/fixtures/generic_array_longarraysyntax.php'); + + // Define expected results (errors and warnings). Format, array of: + // - line => number of problems, or + // - line => array of contents for message / source problem matching. + // - line => string of contents for message / source problem matching (only 1). + $this->set_errors([ + 3 => 'Short array syntax must be used to define arrays @Source: Generic.Arrays.DisallowLongArraySyntax.Found', + 5 => 'Short array syntax must be used to define arrays @Source: Generic.Arrays.DisallowLongArraySyntax.Found', + 9 => 'Short array syntax must be used to define arrays @Source: Generic.Arrays.DisallowLongArraySyntax.Found', + ]); + + $this->set_warnings(array()); + + // Let's do all the hard work! + $this->verify_cs_results(); + } + /** * Test the Generic.Files.LineEndings sniff. * diff --git a/moodle/Tests/MoodleUtilTest.php b/moodle/Tests/MoodleUtilTest.php index 5f19303..651d587 100644 --- a/moodle/Tests/MoodleUtilTest.php +++ b/moodle/Tests/MoodleUtilTest.php @@ -35,7 +35,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Util\MoodleUtil */ -class MoodleUtilTest extends MoodleCSBaseTest { +class MoodleUtilTest extends MoodleCSBaseTestCase { /** * Unit test for calculateAllComponents. @@ -466,4 +466,200 @@ protected function cleanMoodleUtilCaches() { $moodleComponents->setAccessible(true); $moodleComponents->setValue([]); } + + /** + * Data provider for testIsUnitTest. + * + * @return array + */ + public static function isUnitTestProvider(): array + { + return [ + 'Not in tests directory' => [ + 'value' => '/path/to/standard/file.php', + 'return' => false, + ], + 'In tests directory' => [ + 'value' => '/path/to/standard/tests/file.php', + 'return' => true, + ], + 'In test sub-directory' => [ + 'value' => '/path/to/standard/tests/sub/file.php', + 'return' => true, + ], + 'Generator' => [ + 'value' => '/path/to/standard/tests/generator/file.php', + 'return' => false, + ], + 'Fixture' => [ + 'value' => '/path/to/standard/tests/fixtures/file.php', + 'return' => false, + ], + 'Behat' => [ + 'value' => '/path/to/standard/tests/behat/behat_test_file.php', + 'return' => false, + ], + ]; + } + + /** + * @dataProvider isUnitTestProvider + */ + public function testIsUnitTest( + string $filepath, + bool $expected + ): void + { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File($filepath, $phpcsRuleset, $phpcsConfig); + + $this->assertEquals($expected, MoodleUtil::isUnitTest($file)); + } + + /** + * Data provider for testMeetsMinimumMoodleVersion. + * + * @return array + */ + public static function meetsMinimumMoodleVersionProvider(): array + { + return [ + // Setting up moodleBranch config/runtime option. + 'moodleBranch_not_integer' => [ + 'moodleVersion' => 'noint', + 'minVersion' => 311, + 'return' => ['exception' => DeepExitException::class, 'message' => 'Value in not an integer'], + ], + 'moodleBranch_big' => [ + 'moodleVersion' => '10000', + 'minVersion' => 311, + 'return' => ['exception' => DeepExitException::class, 'message' => 'Value must be 4 digit max'], + ], + 'moodleBranch_valid_meets_minimum' => [ + 'moodleVersion' => 999, + 'minVersion' => 311, + 'return' => ['value' => true], + ], + 'moodleBranch_valid_equals_minimum' => [ + 'moodleVersion' => 311, + 'minVersion' => 311, + 'return' => ['value' => true], + ], + 'moodleBranch_valid_does_not_meet_minimum' => [ + 'moodleVersion' => 311, + 'minVersion' => 402, + 'return' => ['value' => false], + ], + 'moodleBranch_valid_but_empty' => [ + 'moodleVersion' => 0, + 'minVersion' => 311, + 'return' => ['value' => null], + ], + ]; + } + + /** + * @dataProvider meetsMinimumMoodleVersionProvider + * @param string|int $moodleVersion + * @param int $minversion + * @param array $return + */ + public function testMeetsMinimumMoodleVersion( + $moodleVersion, + int $minVersion, + array $return + ): void + { + Config::setConfigData('moodleBranch', $moodleVersion, true); + + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $file = new File('/path/to/tests/file.php', $phpcsRuleset, $phpcsConfig); + + // Exception is coming, let's verify it happens. + if (isset($return['exception'])) { + try { + MoodleUtil::getMoodleBranch($file); + } catch (\Exception $e) { + $this->assertInstanceOf($return['exception'], $e); + $this->assertStringContainsString($return['message'], $e->getMessage()); + } + } else if (array_key_exists('value', $return)) { + // Normal asserting result. + $this->assertSame($return['value'], MoodleUtil::meetsMinimumMoodleVersion($file, $minVersion)); + } + + // Do we want to reset any information cached (by default we do). + $this->cleanMoodleUtilCaches(); + + // We need to unset all config options when passed. + Config::setConfigData('moodleBranch', null, true); + } + + public static function findClassMethodPointerProvider(): array + { + return [ + [ + 'instance_method', + true, + ], + [ + 'protected_method', + true, + ], + [ + 'private_method', + true, + ], + [ + 'static_method', + true, + ], + [ + 'protected_static_method', + true, + ], + [ + 'private_static_method', + true, + ], + [ + 'not_found_method', + false, + ], + ]; + } + + /** + * @dataProvider findClassMethodPointerProvider + */ + public function testFindClassMethodPointer( + string $methodName, + bool $found + ): void + { + $phpcsConfig = new Config(); + $phpcsRuleset = new Ruleset($phpcsConfig); + $phpcsFile = new \PHP_CodeSniffer\Files\LocalFile( + __DIR__ . '/fixtures/moodleutil/test_with_methods_to_find.php', + $phpcsRuleset, + $phpcsConfig + ); + + $phpcsFile->process(); + $classPointer = $phpcsFile->findNext(T_CLASS, 0); + + $pointer = MoodleUtil::findClassMethodPointer( + $phpcsFile, + $classPointer, + $methodName + ); + + if ($found) { + $this->assertGreaterThan(0, $pointer); + } else { + $this->assertNull($pointer); + } + } } diff --git a/moodle/Tests/NamingConventionsValidFunctionNameTest.php b/moodle/Tests/NamingConventionsValidFunctionNameTest.php index 4425c93..501830c 100644 --- a/moodle/Tests/NamingConventionsValidFunctionNameTest.php +++ b/moodle/Tests/NamingConventionsValidFunctionNameTest.php @@ -30,7 +30,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\NamingConventions\ValidFunctionNameSniff */ -class NamingConventionsValidFunctionNameTest extends MoodleCSBaseTest { +class NamingConventionsValidFunctionNameTest extends MoodleCSBaseTestCase { /** * Data provider for self::test_namingconventions_validfunctionname diff --git a/moodle/Tests/PHPIncludingFileTest.php b/moodle/Tests/PHPIncludingFileTest.php index b23cd5e..641f1a9 100644 --- a/moodle/Tests/PHPIncludingFileTest.php +++ b/moodle/Tests/PHPIncludingFileTest.php @@ -28,7 +28,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHP\IncludingFileSniff */ -class PHPIncludingFileTest extends MoodleCSBaseTest { +class PHPIncludingFileTest extends MoodleCSBaseTestCase { public function test_php_includingfile() { // Define the standard, sniff and fixture to use. diff --git a/moodle/Tests/PHPMemberVarScopeTest.php b/moodle/Tests/PHPMemberVarScopeTest.php index 37727fd..9becd59 100644 --- a/moodle/Tests/PHPMemberVarScopeTest.php +++ b/moodle/Tests/PHPMemberVarScopeTest.php @@ -28,7 +28,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHP\MemberVarScopeSniff */ -class PHPMemberVarScopeTest extends MoodleCSBaseTest { +class PHPMemberVarScopeTest extends MoodleCSBaseTestCase { public function test_php_membervarscope() { // Define the standard, sniff and fixture to use. diff --git a/moodle/Tests/PHPUnitTestCaseCoversTest.php b/moodle/Tests/PHPUnitTestCaseCoversTest.php index 55649c5..67434b1 100644 --- a/moodle/Tests/PHPUnitTestCaseCoversTest.php +++ b/moodle/Tests/PHPUnitTestCaseCoversTest.php @@ -30,7 +30,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit\TestCaseCoversSniff */ -class PHPUnitTestCaseCoversTest extends MoodleCSBaseTest { +class PHPUnitTestCaseCoversTest extends MoodleCSBaseTestCase { /** * Data provider for self::test_phpunit_testcasecovers diff --git a/moodle/Tests/PHPUnitTestCaseNamesTest.php b/moodle/Tests/PHPUnitTestCaseNamesTest.php index fae5e4a..ca64fe8 100644 --- a/moodle/Tests/PHPUnitTestCaseNamesTest.php +++ b/moodle/Tests/PHPUnitTestCaseNamesTest.php @@ -30,7 +30,7 @@ * * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit\TestCaseNamesSniff */ -class PHPUnitTestCaseNamesTest extends MoodleCSBaseTest { +class PHPUnitTestCaseNamesTest extends MoodleCSBaseTestCase { /** * Data provider for self::test_phpunit_testcasenames diff --git a/moodle/Tests/PHPUnitTestCaseProviderTest.php b/moodle/Tests/PHPUnitTestCaseProviderTest.php new file mode 100644 index 0000000..12e5e67 --- /dev/null +++ b/moodle/Tests/PHPUnitTestCaseProviderTest.php @@ -0,0 +1,152 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests; + +use MoodleHQ\MoodleCS\moodle\Util\MoodleUtil; + +// phpcs:disable moodle.NamingConventions + +/** + * Test the TestCaseCoversSniff sniff. + * + * @package local_codechecker + * @category test + * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\PHPUnit\TestCaseProviderSniff + */ +class PHPUnitTestCaseProviderTest extends MoodleCSBaseTestCase { + + /** + * Data provider for self::test_phpunit_test_providers + */ + public function provider_phpunit_data_providers() { + return [ + 'Correct' => [ + 'fixture' => 'fixtures/phpunit/provider/correct_test.php', + 'errors' => [], + 'warnings' => [], + ], + 'Provider Casing' => [ + 'fixture' => 'fixtures/phpunit/provider/provider_casing_test.php', + 'errors' => [ + 6 => 'Wrong @dataProvider tag: @dataprovider provided, @dataProvider expected', + ], + 'warnings' => [ + ], + ], + 'Provider Visibility' => [ + 'fixture' => 'fixtures/phpunit/provider/provider_visibility_test.php', + 'errors' => [ + 12 => 'Data provider method "provider" must be public.', + 23 => 'Data provider method "provider_without_visibility" visibility should be specified.', + 34 => 'Data provider method "static_provider_without_visibility" visibility should be specified.', + ], + 'warnings' => [ + 17 => 'Data provider method "provider_without_visibility" will need to be converted to static in future.', + ], + ], + 'Provider Naming conflicts with test names' => [ + 'fixture' => 'fixtures/phpunit/provider/provider_prefix_test.php', + 'errors' => [ + 6 => 'Data provider must not start with "test_". "test_provider" provided.', + ], + 'warnings' => [ + ], + ], + 'Static Providers' => [ + 'fixture' => 'fixtures/phpunit/provider/static_providers_test.php', + 'errors' => [ + ], + 'warnings' => [ + 12 => 'Data provider method "fixable_provider" will need to be converted to static in future.', + 23 => 'Data provider method "unfixable_provider" will need to be converted to static in future.', + 34 => 'Data provider method "partially_fixable_provider" will need to be converted to static in future.', + ], + ], + 'Static Providers Applying fixes' => [ + 'fixture' => 'fixtures/phpunit/provider/static_providers_fix_test.php', + 'errors' => [ + ], + 'warnings' => [ + 13 => 'Data provider method "fixable_provider" will need to be converted to static in future.', + 24 => 'Data provider method "unfixable_provider" will need to be converted to static in future.', + 35 => 'Data provider method "partially_fixable_provider" will need to be converted to static in future.', + ], + ], + 'Provider Return Type checks' => [ + 'fixture' => 'fixtures/phpunit/provider/provider_returntype_test.php', + 'errors' => [ + 6 => 'Data provider method "provider_no_return" must return an array, a Generator or an Iterable.', + 17 => 'Data provider method "provider_wrong_return" must return an array, a Generator or an Iterable.', + 28 => 'Data provider method "provider_returns_generator" must return an array, a Generator or an Iterable.', + 41 => 'Data provider method "provider_returns_iterator" must return an array, a Generator or an Iterable.', + ], + 'warnings' => [ + ], + ], + 'Provider not found' => [ + 'fixture' => 'fixtures/phpunit/provider/provider_not_found_test.php', + 'errors' => [ + 6 => 'Data provider method "provider" not found.', + 14 => 'Wrong @dataProvider tag specified for test test_two, it must be followed by a space and a method name.', + ], + 'warnings' => [ + ], + ], + 'Complex test with multiple classes' => [ + 'fixture' => 'fixtures/phpunit/provider/complex_provider_test.php', + 'errors' => [ + 7 => 'Data provider method "provider" not found.', + ], + 'warnings' => [ + 14 => 'Data provider method "second_provider" will need to be converted to static in future.', + ], + ], + ]; + } + + /** + * Test the moodle.PHPUnit.TestCaseCovers sniff + * + * @param string $fixture relative path to fixture to use. + * @param array $errors array of errors expected. + * @param array $warnings array of warnings expected. + * @dataProvider provider_phpunit_data_providers + */ + public function test_phpunit_test_providers( + string $fixture, + array $errors, + array $warnings + ): void { + // Define the standard, sniff and fixture to use. + $this->set_standard('moodle'); + $this->set_sniff('moodle.PHPUnit.TestCaseProvider'); + $this->set_fixture(__DIR__ . '/' . $fixture); + + // Define expected results (errors and warnings). Format, array of: + // - line => number of problems, or + // - line => array of contents for message / source problem matching. + // - line => string of contents for message / source problem matching (only 1). + $this->set_errors($errors); + $this->set_warnings($warnings); + + // Let's do all the hard work! + $this->verify_cs_results(); + } +} diff --git a/moodle/Tests/Sniffs/Namespaces/NamespaceStatementSniffTest.php b/moodle/Tests/Sniffs/Namespaces/NamespaceStatementSniffTest.php new file mode 100644 index 0000000..d2a0d98 --- /dev/null +++ b/moodle/Tests/Sniffs/Namespaces/NamespaceStatementSniffTest.php @@ -0,0 +1,76 @@ +. + +namespace MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Namespaces; + +use MoodleHQ\MoodleCS\moodle\Tests\MoodleCSBaseTestCase; + +// phpcs:disable moodle.NamingConventions + +/** + * Test the NoLeadingSlash sniff. + * + * @package moodle-cs + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \MoodleHQ\MoodleCS\moodle\Sniffs\Namespaces\NoLeadingSlashSniff + */ +class NamespaceStatementSniffTest extends MoodleCSBaseTestCase +{ + public static function leading_slash_provider(): array + { + return [ + [ + 'fixture' => 'correct_namespace', + 'warnings' => [], + 'errors' => [], + ], + [ + 'fixture' => 'leading_backslash', + 'warnings' => [], + 'errors' => [ + 3 => 'Namespace should not start with a slash: \MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Namespaces', + ], + ], + [ + 'fixture' => 'curly_namespace', + 'warnings' => [], + 'errors' => [ + 3 => 'Namespace should not start with a slash: \MoodleHQ\MoodleCS\moodle\Tests\Sniffs\Namespaces', + ], + ], + ]; + } + /** + * @dataProvider leading_slash_provider + */ + public function test_leading_slash( + string $fixture, + array $warnings, + array $errors + ): void + { + $this->set_standard('moodle'); + $this->set_sniff('moodle.Namespaces.NamespaceStatement'); + $this->set_fixture(sprintf("%s/fixtures/%s.php", __DIR__, $fixture)); + $this->set_warnings($warnings); + $this->set_errors($errors); + + $this->verify_cs_results(); + } +} diff --git a/moodle/Tests/Sniffs/Namespaces/fixtures/correct_namespace.php b/moodle/Tests/Sniffs/Namespaces/fixtures/correct_namespace.php new file mode 100644 index 0000000..26c4931 --- /dev/null +++ b/moodle/Tests/Sniffs/Namespaces/fixtures/correct_namespace.php @@ -0,0 +1,8 @@ +second_provider(); + } +} diff --git a/moodle/Tests/fixtures/phpunit/provider/complex_provider_test.php.fixed b/moodle/Tests/fixtures/phpunit/provider/complex_provider_test.php.fixed new file mode 100644 index 0000000..890f08d --- /dev/null +++ b/moodle/Tests/fixtures/phpunit/provider/complex_provider_test.php.fixed @@ -0,0 +1,43 @@ +// phpcs:set moodle.PHPUnit.TestCaseProvider autofixStaticProviders true +second_provider(); + } +} diff --git a/moodle/Tests/fixtures/phpunit/provider/correct_test.php b/moodle/Tests/fixtures/phpunit/provider/correct_test.php new file mode 100644 index 0000000..99de326 --- /dev/null +++ b/moodle/Tests/fixtures/phpunit/provider/correct_test.php @@ -0,0 +1,22 @@ +provider(); + } + + /** + * @dataProvider partially_fixable_provider + */ + public function test_partially_fixable(): void { + // Nothing to test. + } + + public function partially_fixable_provider(): array { + $foo = $this->call_something(); + $foo->bar(); + $foo(); + $this(); + $this; + return $this->fixable_provider(); + } +} diff --git a/moodle/Tests/fixtures/phpunit/provider/static_providers_fix_test.php.fixed b/moodle/Tests/fixtures/phpunit/provider/static_providers_fix_test.php.fixed new file mode 100644 index 0000000..d8cfbf1 --- /dev/null +++ b/moodle/Tests/fixtures/phpunit/provider/static_providers_fix_test.php.fixed @@ -0,0 +1,49 @@ +// phpcs:set moodle.PHPUnit.TestCaseProvider autofixStaticProviders true +provider(); + } + + /** + * @dataProvider partially_fixable_provider + */ + public function test_partially_fixable(): void { + // Nothing to test. + } + + public function partially_fixable_provider(): array { + $foo = $this->call_something(); + $foo->bar(); + $foo(); + $this(); + $this; + return self::fixable_provider(); + } +} diff --git a/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php b/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php new file mode 100644 index 0000000..f9ba938 --- /dev/null +++ b/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php @@ -0,0 +1,46 @@ +provider(); + } + + /** + * @dataProvider partially_fixable_provider + */ + public function test_partially_fixable(): void { + // Nothing to test. + } + + public function partially_fixable_provider(): array { + $foo = $this->call_something(); + $foo->bar(); + $foo(); + return $this->fixable_provider(); + } +} diff --git a/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php.fixed b/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php.fixed new file mode 100644 index 0000000..f9ba938 --- /dev/null +++ b/moodle/Tests/fixtures/phpunit/provider/static_providers_test.php.fixed @@ -0,0 +1,46 @@ +provider(); + } + + /** + * @dataProvider partially_fixable_provider + */ + public function test_partially_fixable(): void { + // Nothing to test. + } + + public function partially_fixable_provider(): array { + $foo = $this->call_something(); + $foo->bar(); + $foo(); + return $this->fixable_provider(); + } +} diff --git a/moodle/Util/MoodleUtil.php b/moodle/Util/MoodleUtil.php index 2f10b92..8befc45 100644 --- a/moodle/Util/MoodleUtil.php +++ b/moodle/Util/MoodleUtil.php @@ -18,6 +18,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Exceptions\DeepExitException; +use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\DummyFile; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Ruleset; @@ -370,4 +371,100 @@ public static function getMoodleRoot(File $file = null, bool $selfPath = true) { self::$moodleRoot = null; return self::$moodleRoot; } + + /** + * Whether this file is a unit test file. + * + * This does not include test fixtures, generators, or behat files. + * + * Any file which is not correctly named will be ignored. + * + * @param File $phpcsFile + * @return bool + */ + public static function isUnitTest(File $phpcsFile): bool + { + // If the file isn't under tests directory, nothing to check. + if (stripos($phpcsFile->getFilename(), '/tests/') === false) { + return false; + } + + // If the file is in a fixture directory, ignore it. + if (stripos($phpcsFile->getFilename(), '/tests/fixtures/') !== false) { + return false; + } + + // If the file is in a generator directory, ignore it. + if (stripos($phpcsFile->getFilename(), '/tests/generator/') !== false) { + return false; + } + + // If the file is in a behat directory, ignore it. + if (stripos($phpcsFile->getFilename(), '/tests/behat/') !== false) { + return false; + } + + return true; + } + + /** + * Whether we are running PHPUnit. + * + * @return bool + * @codeCoverageIgnore + */ + public static function isUnitTestRunning(): bool + { + // Detect if we are running PHPUnit. + return defined('PHPUNIT_TEST') && PHPUNIT_TEST; + } + + /** + * Whether the file belongs to a version of Moodle meeting the specifeid minimum version. + * + * If a version could not be determined, null is returned. + * + * @param File $phpcsFile The file to check + * @param int The minimum version to check against as a 2, or 3 digit number. + * @return null|bool + */ + public static function meetsMinimumMoodleVersion( + File $phpcsFile, + int $version + ): ?bool + { + $moodleBranch = self::getMoodleBranch($phpcsFile); + if (!isset($moodleBranch)) { + // We cannot determine the moodle branch, so we cannot determine if the version is met. + return null; + } + + return ($moodleBranch >= $version); + } + + /** + * Find the pointer to a method in a class. + * + * @param File $phpcsFile + * @param int $classPtr + * @param string $methodName + * @return null|int + */ + public static function findClassMethodPointer( + File $phpcsFile, + int $classPtr, + string $methodName + ): ?int + { + $mStart = $classPtr; + $tokens = $phpcsFile->getTokens(); + while ($mStart = $phpcsFile->findNext(T_FUNCTION, $mStart + 1, $tokens[$classPtr]['scope_closer'])) { + $method = $phpcsFile->getDeclarationName($mStart); + if ($method === $methodName) { + return $mStart; + } + } + + return null; + } } diff --git a/moodle/ruleset.xml b/moodle/ruleset.xml index 059922c..983f893 100644 --- a/moodle/ruleset.xml +++ b/moodle/ruleset.xml @@ -9,6 +9,20 @@ + + warning + + + + + @@ -56,9 +70,7 @@ - - warning - + @@ -88,11 +100,27 @@ + + + 0 + + + +