diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 65f00f2c..ae832c38 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -57,6 +57,8 @@ phutil_register_library_map(array( 'ArcanistClassExtendsObjectXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassExtendsObjectXHPASTLinterRule.php', 'ArcanistClassExtendsObjectXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassExtendsObjectXHPASTLinterRuleTestCase.php', 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassFilenameMismatchXHPASTLinterRule.php', + 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule.php', + 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase.php', 'ArcanistClassNameLiteralXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistClassNameLiteralXHPASTLinterRule.php', 'ArcanistClassNameLiteralXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistClassNameLiteralXHPASTLinterRuleTestCase.php', 'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php', @@ -441,6 +443,8 @@ phutil_register_library_map(array( 'ArcanistClassExtendsObjectXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassExtendsObjectXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistClassFilenameMismatchXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', + 'ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistClassNameLiteralXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistClassNameLiteralXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistCloseRevisionWorkflow' => 'ArcanistWorkflow', diff --git a/src/lint/linter/xhpast/rules/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule.php new file mode 100644 index 00000000..1d3b4f6e --- /dev/null +++ b/src/lint/linter/xhpast/rules/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRule.php @@ -0,0 +1,74 @@ +selectDescendantsOfType('n_CLASS_DECLARATION'); + + foreach ($classes as $class) { + $class_modifiers = $this->getModifiers($class); + + $abstract_methods = array(); + $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); + + foreach ($methods as $method) { + $method_modifiers = $this->getModifiers($method); + + if (idx($method_modifiers, 'abstract')) { + $abstract_methods[] = $method; + } + } + + if (!idx($class_modifiers, 'abstract') && $abstract_methods) { + $this->raiseLintAtNode( + $class, + pht( + 'Class contains %s %s method(s) and must therefore '. + 'be declared `%s`.', + phutil_count($abstract_methods), + 'abstract', + 'abstract')); + } + } + } + + /** + * Get class/method modifiers. + * + * @param XHPASTNode A node of type `n_CLASS_DECLARATION` or + * `n_METHOD_DECLARATION`. + * @return map Class/method modifiers. + */ + private function getModifiers(XHPASTNode $node) { + $modifier_list = $node->getChildByIndex(0); + + switch ($modifier_list->getTypeName()) { + case 'n_CLASS_ATTRIBUTES': + case 'n_METHOD_MODIFIER_LIST': + break; + + default: + return array(); + } + + $modifiers = array(); + + foreach ($modifier_list->selectDescendantsOfType('n_STRING') as $modifier) { + $modifiers[strtolower($modifier->getConcreteString())] = true; + } + + return $modifiers; + } + +} diff --git a/src/lint/linter/xhpast/rules/__tests__/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase.php b/src/lint/linter/xhpast/rules/__tests__/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase.php new file mode 100644 index 00000000..ae34b95f --- /dev/null +++ b/src/lint/linter/xhpast/rules/__tests__/ArcanistClassMustBeDeclaredAbstractXHPASTLinterRuleTestCase.php @@ -0,0 +1,11 @@ +executeTestsInDirectory( + dirname(__FILE__).'/class-must-be-declared-abstract/'); + } + +} diff --git a/src/lint/linter/xhpast/rules/__tests__/class-must-be-declared-abstract/is-abstract.lint-test b/src/lint/linter/xhpast/rules/__tests__/class-must-be-declared-abstract/is-abstract.lint-test new file mode 100644 index 00000000..c2307f9c --- /dev/null +++ b/src/lint/linter/xhpast/rules/__tests__/class-must-be-declared-abstract/is-abstract.lint-test @@ -0,0 +1,5 @@ +