diff --git a/extension.neon b/extension.neon index 3e98e0c0..87e455c3 100644 --- a/extension.neon +++ b/extension.neon @@ -19,6 +19,8 @@ parameters: drupal_root: '%currentWorkingDirectory%' bleedingEdge: checkDeprecatedHooksInApiFiles: false + rules: + testClassSuffixNameRule: false entityMapping: aggregator_feed: class: Drupal\aggregator\Entity\Feed @@ -238,6 +240,9 @@ parametersSchema: bleedingEdge: structure([ checkDeprecatedHooksInApiFiles: boolean() ]) + rules: structure([ + testClassSuffixNameRule: boolean() + ]) entityMapping: arrayOf(anyOf( structure([ class: string() diff --git a/rules.neon b/rules.neon index fd3a77f8..07720b8b 100644 --- a/rules.neon +++ b/rules.neon @@ -1,21 +1,29 @@ -rules: - - mglaman\PHPStanDrupal\Rules\Drupal\Coder\DiscouragedFunctionsRule - - mglaman\PHPStanDrupal\Rules\Drupal\GlobalDrupalDependencyInjectionRule - - mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule - - mglaman\PHPStanDrupal\Rules\Deprecations\AccessDeprecatedConstant - - mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule - - mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule - - mglaman\PHPStanDrupal\Rules\Deprecations\ConditionManagerCreateInstanceContextConfigurationRule - - mglaman\PHPStanDrupal\Rules\Drupal\RenderCallbackRule - - mglaman\PHPStanDrupal\Rules\Deprecations\StaticServiceDeprecatedServiceRule - - mglaman\PHPStanDrupal\Rules\Deprecations\GetDeprecatedServiceRule - - mglaman\PHPStanDrupal\Rules\Drupal\Tests\BrowserTestBaseDefaultThemeRule - - mglaman\PHPStanDrupal\Rules\Deprecations\ConfigEntityConfigExportRule - - mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule - - mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude - - mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes - - mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule - - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRouteObjectInterfaceConstantsRule - - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRoutingInClassMethodSignatureRule - - mglaman\PHPStanDrupal\Rules\Drupal\RequestStackGetMainRequestRule - - mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule +rules: + - mglaman\PHPStanDrupal\Rules\Drupal\Coder\DiscouragedFunctionsRule + - mglaman\PHPStanDrupal\Rules\Drupal\GlobalDrupalDependencyInjectionRule + - mglaman\PHPStanDrupal\Rules\Drupal\PluginManager\PluginManagerSetsCacheBackendRule + - mglaman\PHPStanDrupal\Rules\Deprecations\AccessDeprecatedConstant + - mglaman\PHPStanDrupal\Rules\Classes\ClassExtendsInternalClassRule + - mglaman\PHPStanDrupal\Rules\Classes\PluginManagerInspectionRule + - mglaman\PHPStanDrupal\Rules\Deprecations\ConditionManagerCreateInstanceContextConfigurationRule + - mglaman\PHPStanDrupal\Rules\Drupal\RenderCallbackRule + - mglaman\PHPStanDrupal\Rules\Deprecations\StaticServiceDeprecatedServiceRule + - mglaman\PHPStanDrupal\Rules\Deprecations\GetDeprecatedServiceRule + - mglaman\PHPStanDrupal\Rules\Drupal\Tests\BrowserTestBaseDefaultThemeRule + - mglaman\PHPStanDrupal\Rules\Deprecations\ConfigEntityConfigExportRule + - mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule + - mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude + - mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes + - mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule + - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRouteObjectInterfaceConstantsRule + - mglaman\PHPStanDrupal\Rules\Deprecations\SymfonyCmfRoutingInClassMethodSignatureRule + - mglaman\PHPStanDrupal\Rules\Drupal\RequestStackGetMainRequestRule + - mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule + +conditionalTags: + mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule: + phpstan.rules.rule: %drupal.rules.testClassSuffixNameRule% + +services: + - + class: mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule diff --git a/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php b/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php new file mode 100644 index 00000000..6f8b83ea --- /dev/null +++ b/src/Rules/Drupal/Tests/TestClassSuffixNameRule.php @@ -0,0 +1,69 @@ + + */ +final class TestClassSuffixNameRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + // We're not interested in non-extending classes. + if ($node->extends === null) { + return []; + } + + // We're not interested in abstract classes. + if ($node->isAbstract()) { + return []; + } + + // We need a namespaced class name. + if ($node->namespacedName === null) { + return []; + } + + // We're only interested in \PHPUnit\Framework\TestCase subtype classes. + $classType = $scope->resolveTypeByName($node->namespacedName); + $phpUnitFrameworkTestCaseType = new ObjectType(TestCase::class); + if (!$phpUnitFrameworkTestCaseType->isSuperTypeOf($classType)->yes()) { + return []; + } + + // Check class name has suffix "Test". + // @todo replace this str_ends_with() when php 8 is required. + if (substr_compare($node->namespacedName->getLast(), 'Test', -4) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Non-abstract test classes names should always have the suffix "Test", found incorrect class name "%s".', + $node->name, + ) + ) + ->line($node->getStartLine()) + ->tip('See https://www.drupal.org/docs/develop/standards/php/object-oriented-code#naming') + ->build() + ]; + } +} diff --git a/tests/src/Rules/TestClassSuffixNameRuleTest.php b/tests/src/Rules/TestClassSuffixNameRuleTest.php new file mode 100644 index 00000000..e1f9126a --- /dev/null +++ b/tests/src/Rules/TestClassSuffixNameRuleTest.php @@ -0,0 +1,49 @@ +analyse( + [$path], + $errorMessages + ); + } + + public function fileData(): \Generator + { + yield [ + __DIR__ . '/data/test-cases-TestClassSuffixNameRule.php', + [ + [ + 'Non-abstract test classes names should always have the suffix "Test", found incorrect class name "IncorrectlyNamedDirectlyExtendingPHPUnitFrameworkTestCaseClass".', + 29, + 'See https://www.drupal.org/docs/develop/standards/php/object-oriented-code#naming' + ], + [ + 'Non-abstract test classes names should always have the suffix "Test", found incorrect class name "IncorrectlyNamedIndirectlyExtendingPHPUnitFrameworkTestCaseClass".', + 39, + 'See https://www.drupal.org/docs/develop/standards/php/object-oriented-code#naming' + ], + ] + ]; + } + +} diff --git a/tests/src/Rules/data/test-cases-TestClassSuffixNameRule.php b/tests/src/Rules/data/test-cases-TestClassSuffixNameRule.php new file mode 100644 index 00000000..ef71aef8 --- /dev/null +++ b/tests/src/Rules/data/test-cases-TestClassSuffixNameRule.php @@ -0,0 +1,42 @@ +