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
+
+
+
+