diff --git a/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php index c1e6cc6f..fce47012 100644 --- a/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/rules/ArcanistInvalidModifiersXHPASTLinterRule.php @@ -1,103 +1,135 @@ selectDescendantsOfTypes(array( 'n_CLASS_MEMBER_MODIFIER_LIST', 'n_METHOD_MODIFIER_LIST', )); foreach ($methods as $method) { $modifiers = $method->getChildren(); $is_abstract = false; $is_final = false; $is_static = false; $visibility = null; + $is_property = ($method->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST'); + $is_method = !$is_property; + + $final_modifier = null; + $visibility_modifier = null; + $abstract_modifier = null; foreach ($modifiers as $modifier) { switch ($modifier->getConcreteString()) { case 'abstract': - if ($method->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { + if ($is_property) { $this->raiseLintAtNode( $modifier, pht( - 'Properties cannot be declared `%s`.', - 'abstract')); + 'Properties cannot be declared "abstract".')); } if ($is_abstract) { $this->raiseLintAtNode( $modifier, pht( - 'Multiple `%s` modifiers are not allowed.', - 'abstract')); - } - - if ($is_final) { - $this->raiseLintAtNode( - $modifier, - pht( - 'Cannot use the `%s` modifier on an `%s` class member', - 'final', - 'abstract')); + 'Multiple "abstract" modifiers are not allowed.')); } + $abstract_modifier = $modifier; $is_abstract = true; break; case 'final': - if ($is_abstract) { + if ($is_property) { $this->raiseLintAtNode( $modifier, pht( - 'Cannot use the `%s` modifier on an `%s` class member', - 'final', - 'abstract')); + 'Properties can not be declared "final".')); } if ($is_final) { $this->raiseLintAtNode( $modifier, pht( - 'Multiple `%s` modifiers are not allowed.', - 'final')); + 'Multiple "final" modifiers are not allowed.')); } + $final_modifier = $modifier; $is_final = true; break; case 'public': case 'protected': case 'private': - if ($visibility) { + if ($visibility !== null) { $this->raiseLintAtNode( $modifier, pht('Multiple access type modifiers are not allowed.')); } + $visibility_modifier = $modifier; + $visibility = $modifier->getConcreteString(); + $visibility = phutil_utf8_strtolower($visibility); break; case 'static': if ($is_static) { $this->raiseLintAtNode( $modifier, pht( - 'Multiple `%s` modifiers are not allowed.', - 'static')); + 'Multiple "static" modifiers are not allowed.')); } break; } } + + $is_private = ($visibility === 'private'); + + if ($is_final && $is_abstract) { + if ($is_method) { + $this->raiseLintAtNode( + $final_modifier, + pht('Methods may not be both "abstract" and "final".')); + } else { + // Properties can't be "abstract" and "final" either, but they can't + // ever be "abstract" at all, and we've already raise a message about + // that earlier. + } + } + + if ($is_private && $is_final) { + if ($is_method) { + $final_tokens = $final_modifier->getTokens(); + $space_tokens = last($final_tokens)->getWhitespaceTokensAfter(); + + $final_offset = head($final_tokens)->getOffset(); + + $final_string = array_merge($final_tokens, $space_tokens); + $final_string = mpull($final_string, 'getValue'); + $final_string = implode('', $final_string); + + $this->raiseLintAtOffset( + $final_offset, + pht('Methods may not be both "private" and "final".'), + $final_string, + ''); + } else { + // Properties can't be "final" at all, and we already raised a + // message about this. + } + } } } } diff --git a/src/lint/linter/xhpast/rules/__tests__/invalid-modifiers/invalid-modifiers.lint-test b/src/lint/linter/xhpast/rules/__tests__/invalid-modifiers/invalid-modifiers.lint-test index 1490d748..ae74d7fd 100644 --- a/src/lint/linter/xhpast/rules/__tests__/invalid-modifiers/invalid-modifiers.lint-test +++ b/src/lint/linter/xhpast/rules/__tests__/invalid-modifiers/invalid-modifiers.lint-test @@ -1,18 +1,40 @@ id = $id; $this->typeID = $type; $this->value = $value; $this->offset = $offset; $this->tree = $tree; } final public function getTokenID() { return $this->id; } final public function getTypeID() { return $this->typeID; } public function getTypeName() { if (empty($this->typeName)) { $this->typeName = $this->tree->getTokenTypeNameFromTypeID($this->typeID); } return $this->typeName; } final public function getValue() { return $this->value; } final public function overwriteValue($value) { $this->value = $value; return $this; } final public function getOffset() { return $this->offset; } abstract public function isComment(); abstract public function isAnyWhitespace(); public function isSemantic() { return !($this->isComment() || $this->isAnyWhitespace()); } public function getPrevToken() { $tokens = $this->tree->getRawTokenStream(); return idx($tokens, $this->id - 1); } public function getNextToken() { $tokens = $this->tree->getRawTokenStream(); return idx($tokens, $this->id + 1); } public function getNonsemanticTokensBefore() { $tokens = $this->tree->getRawTokenStream(); $result = array(); $ii = $this->id - 1; while ($ii >= 0 && !$tokens[$ii]->isSemantic()) { $result[$ii] = $tokens[$ii]; --$ii; } return array_reverse($result); } public function getNonsemanticTokensAfter() { $tokens = $this->tree->getRawTokenStream(); $result = array(); $ii = $this->id + 1; while ($ii < count($tokens) && !$tokens[$ii]->isSemantic()) { $result[$ii] = $tokens[$ii]; ++$ii; } return $result; } + public function getWhitespaceTokensAfter() { + $tokens = $this->tree->getRawTokenStream(); + $result = array(); + $ii = $this->id + 1; + while ($ii < count($tokens) && $tokens[$ii]->isAnyWhitespace()) { + $result[$ii] = $tokens[$ii]; + ++$ii; + } + return $result; + } + final public function getLineNumber() { return idx($this->tree->getOffsetToLineNumberMap(), $this->getOffset()); } }