From 9f28db36b8c3534250846c2efbd942bcf0bb01a2 Mon Sep 17 00:00:00 2001 From: Paolo Date: Fri, 4 Mar 2022 19:59:57 +0100 Subject: [PATCH] - Added new sniffs for codesniffer to be compliant with the FHComplete coding style - Commented tests/codesniffer/FHComplete/ruleset.xml - Removed redundant PHPCS checks already performed by PHPMD - Added/removed checks from PHPCS to be compliant with the FHComplete coding style - PHPMD: Removed the ElseExpression rule from the rulesets/cleancode.xml --- .../Classes/FHCPropertyDeclarationSniff.php | 143 ++++++++++++++++ .../Sniffs/Classes/FHCValidClassNameSniff.php | 101 ++++++++++++ .../FHCControlSignatureSniff.php | 154 ++++++++++++++++++ .../Methods/FHCMethodDeclarationSniff.php | 129 +++++++++++++++ tests/codesniffer/FHComplete/ruleset.xml | 75 ++++++--- tests/phpmd/rulesets.xml | 7 +- 6 files changed, 583 insertions(+), 26 deletions(-) create mode 100644 tests/codesniffer/FHComplete/Sniffs/Classes/FHCPropertyDeclarationSniff.php create mode 100644 tests/codesniffer/FHComplete/Sniffs/Classes/FHCValidClassNameSniff.php create mode 100644 tests/codesniffer/FHComplete/Sniffs/ControlStructures/FHCControlSignatureSniff.php create mode 100644 tests/codesniffer/FHComplete/Sniffs/Methods/FHCMethodDeclarationSniff.php diff --git a/tests/codesniffer/FHComplete/Sniffs/Classes/FHCPropertyDeclarationSniff.php b/tests/codesniffer/FHComplete/Sniffs/Classes/FHCPropertyDeclarationSniff.php new file mode 100644 index 000000000..c5ecd1232 --- /dev/null +++ b/tests/codesniffer/FHComplete/Sniffs/Classes/FHCPropertyDeclarationSniff.php @@ -0,0 +1,143 @@ +getTokens(); + + // Detect multiple properties defined at the same time. Throw an error + // for this, but also only process the first property in the list so we don't + // repeat errors. + $find = Tokens::$scopeModifiers; + $find[] = T_VARIABLE; + $find[] = T_VAR; + $find[] = T_SEMICOLON; + $find[] = T_OPEN_CURLY_BRACKET; + + $prev = $phpcsFile->findPrevious($find, ($stackPtr - 1)); + if ($tokens[$prev]['code'] === T_VARIABLE) { + return; + } + + if ($tokens[$prev]['code'] === T_VAR) { + $error = 'The var keyword must not be used to declare a property'; + $phpcsFile->addError($error, $stackPtr, 'VarUsed'); + } + + $next = $phpcsFile->findNext([T_VARIABLE, T_SEMICOLON], ($stackPtr + 1)); + if ($next !== false && $tokens[$next]['code'] === T_VARIABLE) { + $error = 'There must not be more than one property declared per statement'; + $phpcsFile->addError($error, $stackPtr, 'Multiple'); + } + + try { + $propertyInfo = $phpcsFile->getMemberProperties($stackPtr); + if (empty($propertyInfo) === true) { + return; + } + } catch (\Exception $e) { + // Turns out not to be a property after all. + return; + } + + if ($propertyInfo['type'] !== '') { + $typeToken = $propertyInfo['type_end_token']; + $error = 'There must be 1 space after the property type declaration; %s found'; + if ($tokens[($typeToken + 1)]['code'] !== T_WHITESPACE) { + $data = ['0']; + $fix = $phpcsFile->addFixableError($error, $typeToken, 'SpacingAfterType', $data); + if ($fix === true) { + $phpcsFile->fixer->addContent($typeToken, ' '); + } + } else if ($tokens[($typeToken + 1)]['content'] !== ' ') { + $next = $phpcsFile->findNext(T_WHITESPACE, ($typeToken + 1), null, true); + if ($tokens[$next]['line'] !== $tokens[$typeToken]['line']) { + $found = 'newline'; + } else { + $found = $tokens[($typeToken + 1)]['length']; + } + + $data = [$found]; + + $nextNonWs = $phpcsFile->findNext(Tokens::$emptyTokens, ($typeToken + 1), null, true); + if ($nextNonWs !== $next) { + $phpcsFile->addError($error, $typeToken, 'SpacingAfterType', $data); + } else { + $fix = $phpcsFile->addFixableError($error, $typeToken, 'SpacingAfterType', $data); + if ($fix === true) { + if ($found === 'newline') { + $phpcsFile->fixer->beginChangeset(); + for ($x = ($typeToken + 1); $x < $next; $x++) { + $phpcsFile->fixer->replaceToken($x, ''); + } + + $phpcsFile->fixer->addContent($typeToken, ' '); + $phpcsFile->fixer->endChangeset(); + } else { + $phpcsFile->fixer->replaceToken(($typeToken + 1), ' '); + } + } + } + }//end if + }//end if + + if ($propertyInfo['scope_specified'] === false) { + $error = 'Visibility must be declared on property "%s"'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $stackPtr, 'ScopeMissing', $data); + } + + if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_static'] === true) { + $scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1)); + $staticPtr = $phpcsFile->findPrevious(T_STATIC, ($stackPtr - 1)); + if ($scopePtr < $staticPtr) { + return; + } + + $error = 'The static declaration must come after the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($scopePtr, ''); + $phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' '); + + $phpcsFile->fixer->endChangeset(); + } + }//end if + + }//end processMemberVar() +}//end class + diff --git a/tests/codesniffer/FHComplete/Sniffs/Classes/FHCValidClassNameSniff.php b/tests/codesniffer/FHComplete/Sniffs/Classes/FHCValidClassNameSniff.php new file mode 100644 index 000000000..7aaf03816 --- /dev/null +++ b/tests/codesniffer/FHComplete/Sniffs/Classes/FHCValidClassNameSniff.php @@ -0,0 +1,101 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['scope_opener']) === false) { + $error = 'Possible parse error: %s missing opening or closing brace'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addWarning($error, $stackPtr, 'MissingBrace', $data); + return; + } + + // Determine the name of the class or interface. Note that we cannot + // simply look for the first T_STRING because a class name + // starting with the number will be multiple tokens. + $opener = $tokens[$stackPtr]['scope_opener']; + $nameStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), $opener, true); + $nameEnd = $phpcsFile->findNext(T_WHITESPACE, $nameStart, $opener); + if ($nameEnd === false) { + $name = $tokens[$nameStart]['content']; + } else { + $name = trim($phpcsFile->getTokensAsString($nameStart, ($nameEnd - $nameStart))); + } + + // Check for PascalCase format. + $valid = $this->isCamelCaps($name); + if ($valid === false) { + $type = ucfirst($tokens[$stackPtr]['content']); + $error = '%s name "%s" is not in PascalCase format'; + $data = [ + $type, + $name, + ]; + $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $data); + $phpcsFile->recordMetric($stackPtr, 'PascalCase class name', 'no'); + } else { + $phpcsFile->recordMetric($stackPtr, 'PascalCase class name', 'yes'); + } + + }//end process() + + /** + * Returns true if the specified string is in the camel caps format. + * + * NOTE: + * - it does not allow the string to start with an underscore "_" + * - it does allow that the string contains an underscore "_" + * - the string must starts with a capitol letter + * + * @param string $string The string the verify. + * + * @return boolean + */ + private function isCamelCaps($string) + { + $legalFirstChar = '[A-Z]'; + + if (preg_match("/^$legalFirstChar/", $string) === 0) { + return false; + } + + // Check that the name only contains legal characters. + $legalChars = 'a-zA-Z0-9_'; + if (preg_match("|[^$legalChars]|", substr($string, 1)) > 0) { + return false; + } + + return true; + + }//end isCamelCaps() +}//end class + diff --git a/tests/codesniffer/FHComplete/Sniffs/ControlStructures/FHCControlSignatureSniff.php b/tests/codesniffer/FHComplete/Sniffs/ControlStructures/FHCControlSignatureSniff.php new file mode 100644 index 000000000..e53ec67b1 --- /dev/null +++ b/tests/codesniffer/FHComplete/Sniffs/ControlStructures/FHCControlSignatureSniff.php @@ -0,0 +1,154 @@ +getTokens(); + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty === false) { + return; + } + + $isAlternative = false; + if (isset($tokens[$stackPtr]['scope_opener']) === true + && $tokens[$tokens[$stackPtr]['scope_opener']]['code'] === T_COLON + ) { + $isAlternative = true; + } + + // Single newline after opening brace. + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $opener = $tokens[$stackPtr]['scope_opener']; + for ($next = ($opener + 1); $next < $phpcsFile->numTokens; $next++) { + $code = $tokens[$next]['code']; + + if ($code === T_WHITESPACE + || ($code === T_INLINE_HTML + && trim($tokens[$next]['content']) === '') + ) { + continue; + } + + // Skip all empty tokens on the same line as the opener. + if ($tokens[$next]['line'] === $tokens[$opener]['line'] + && (isset(Tokens::$emptyTokens[$code]) === true + || $code === T_CLOSE_TAG) + ) { + continue; + } + + // We found the first bit of a code, or a comment on the + // following line. + break; + }//end for + + if ($tokens[$next]['line'] === $tokens[$opener]['line']) { + $error = 'Newline required after opening brace'; + $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterOpenBrace'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($opener + 1); $i < $next; $i++) { + if (trim($tokens[$i]['content']) !== '') { + break; + } + + // Remove whitespace. + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContent($opener, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + }//end if + } else if ($tokens[$stackPtr]['code'] === T_WHILE) { + // Zero spaces after parenthesis closer, but only if followed by a semicolon. + $closer = $tokens[$stackPtr]['parenthesis_closer']; + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === T_SEMICOLON) { + $found = 0; + if ($tokens[($closer + 1)]['code'] === T_WHITESPACE) { + if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { + $found = 'newline'; + } else { + $found = $tokens[($closer + 1)]['length']; + } + } + + if ($found !== 0) { + $error = 'Expected 0 spaces before semicolon; %s found'; + $data = [$found]; + $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeSemicolon', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($closer + 1), ''); + } + } + } + }//end if + + // Only want to check multi-keyword structures from here on. + if ($tokens[$stackPtr]['code'] === T_WHILE) { + if (isset($tokens[$stackPtr]['scope_closer']) !== false) { + return; + } + + $closer = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($closer === false + || $tokens[$closer]['code'] !== T_CLOSE_CURLY_BRACKET + || $tokens[$tokens[$closer]['scope_condition']]['code'] !== T_DO + ) { + return; + } + } else if ($tokens[$stackPtr]['code'] === T_ELSE + || $tokens[$stackPtr]['code'] === T_ELSEIF + || $tokens[$stackPtr]['code'] === T_CATCH + || $tokens[$stackPtr]['code'] === T_FINALLY + ) { + if (isset($tokens[$stackPtr]['scope_opener']) === true + && $tokens[$tokens[$stackPtr]['scope_opener']]['code'] === T_COLON + ) { + // Special case for alternate syntax, where this token is actually + // the closer for the previous block, so there is no spacing to check. + return; + } + + $closer = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($closer === false || $tokens[$closer]['code'] !== T_CLOSE_CURLY_BRACKET) { + return; + } + } else { + return; + }//end if + }//end process() +}//end class + diff --git a/tests/codesniffer/FHComplete/Sniffs/Methods/FHCMethodDeclarationSniff.php b/tests/codesniffer/FHComplete/Sniffs/Methods/FHCMethodDeclarationSniff.php new file mode 100644 index 000000000..4008b2cf1 --- /dev/null +++ b/tests/codesniffer/FHComplete/Sniffs/Methods/FHCMethodDeclarationSniff.php @@ -0,0 +1,129 @@ +getTokens(); + + // Determine if this is a function which needs to be examined. + $conditions = $tokens[$stackPtr]['conditions']; + end($conditions); + $deepestScope = key($conditions); + if ($deepestScope !== $currScope) { + return; + } + + $methodName = $phpcsFile->getDeclarationName($stackPtr); + if ($methodName === null) { + // Ignore closures. + return; + } + + $visibility = 0; + $static = 0; + $abstract = 0; + $final = 0; + + $find = (Tokens::$methodPrefixes + Tokens::$emptyTokens); + $prev = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); + + $prefix = $stackPtr; + while (($prefix = $phpcsFile->findPrevious(Tokens::$methodPrefixes, ($prefix - 1), $prev)) !== false) { + switch ($tokens[$prefix]['code']) { + case T_STATIC: + $static = $prefix; + break; + case T_ABSTRACT: + $abstract = $prefix; + break; + case T_FINAL: + $final = $prefix; + break; + default: + $visibility = $prefix; + break; + } + } + + $fixes = []; + + if ($visibility !== 0 && $final > $visibility) { + $error = 'The final declaration must precede the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $final, 'FinalAfterVisibility'); + if ($fix === true) { + $fixes[$final] = ''; + $fixes[($final + 1)] = ''; + if (isset($fixes[$visibility]) === true) { + $fixes[$visibility] = 'final '.$fixes[$visibility]; + } else { + $fixes[$visibility] = 'final '.$tokens[$visibility]['content']; + } + } + } + + if ($visibility !== 0 && $abstract > $visibility) { + $error = 'The abstract declaration must precede the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $abstract, 'AbstractAfterVisibility'); + if ($fix === true) { + $fixes[$abstract] = ''; + $fixes[($abstract + 1)] = ''; + if (isset($fixes[$visibility]) === true) { + $fixes[$visibility] = 'abstract '.$fixes[$visibility]; + } else { + $fixes[$visibility] = 'abstract '.$tokens[$visibility]['content']; + } + } + } + + if ($static !== 0 && $static < $visibility) { + $error = 'The static declaration must come after the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $static, 'StaticBeforeVisibility'); + if ($fix === true) { + $fixes[$static] = ''; + $fixes[($static + 1)] = ''; + if (isset($fixes[$visibility]) === true) { + $fixes[$visibility] .= ' static'; + } else { + $fixes[$visibility] = $tokens[$visibility]['content'].' static'; + } + } + } + + // Batch all the fixes together to reduce the possibility of conflicts. + if (empty($fixes) === false) { + $phpcsFile->fixer->beginChangeset(); + foreach ($fixes as $stackPtr => $content) { + $phpcsFile->fixer->replaceToken($stackPtr, $content); + } + + $phpcsFile->fixer->endChangeset(); + } + + }//end processTokenWithinScope() +}//end class + diff --git a/tests/codesniffer/FHComplete/ruleset.xml b/tests/codesniffer/FHComplete/ruleset.xml index 70277e738..9608a5650 100644 --- a/tests/codesniffer/FHComplete/ruleset.xml +++ b/tests/codesniffer/FHComplete/ruleset.xml @@ -10,56 +10,83 @@ application/extensions addons + + - - - - + + + + - + + + + + + + + + + + + - + + + - - - - - - + + - + + + + - - - - - - - - - + - - + + + + + + + + + + + + + + + + diff --git a/tests/phpmd/rulesets.xml b/tests/phpmd/rulesets.xml index 187113acf..d162cd892 100644 --- a/tests/phpmd/rulesets.xml +++ b/tests/phpmd/rulesets.xml @@ -17,8 +17,11 @@ application/extensions addons - - + + + + +