diff --git a/Magento2/Sniffs/Commenting/FunctionPHPDocBlockParser.php b/Magento2/Sniffs/Commenting/FunctionPHPDocBlockParser.php new file mode 100644 index 00000000..c23975e9 --- /dev/null +++ b/Magento2/Sniffs/Commenting/FunctionPHPDocBlockParser.php @@ -0,0 +1,147 @@ + $is_empty, + 'description' => $description, + 'tags' => [], + 'return' => '', + 'parameters' => [], + 'throws' => [], + ]; + + $lastDocLine = 0; + $docType = false; + for ($i = $docStart; $i <= $docEnd; $i++) { + $token = $tokens[$i]; + $code = $token['code']; + $line = $token['line']; + $content = $token['content']; + + + if ($code === T_DOC_COMMENT_TAG) { + // add php tokens also without comment to tag list + if (preg_match('/@[a-z]++/', $content, $output_array)) { + $functionDeclarations['tags'][] = $content; + } + + $lastDocLine = $line; + $docType = $token['content']; + continue; + } + + if ($lastDocLine !== $line) { + $lastDocLine = 0; + continue; + } + + if ($content === ' ' || $content === "\n") { + continue; + } + + preg_match_all('/[A-Za-z0-9$]*/', $content, $docTokens); + $docTokens = array_values(array_filter($docTokens[0])); + + if (count($docTokens) === 0) { + // ignore empty parameter declaration + continue; + } + + switch ($docType) { + case '@param': + $functionDeclarations = $this->addParamTagValue($docTokens, $functionDeclarations); + break; + + case '@return': + $functionDeclarations = $this->addReturnTagValue($docTokens, $functionDeclarations); + break; + + case '@throws': + $functionDeclarations = $this->addThrowsTagValue($docTokens, $functionDeclarations); + break; + } + } + + return $functionDeclarations; + } + + /** + * @param array $tokens + * @param array $functionDeclarations + * @return array + */ + private function addParamTagValue(array $tokens, array $functionDeclarations) + { + $type = false; + $content = false; + + foreach ($tokens as $token) { + if (strpos($token, '$') !== false) { + $content = $token; + continue; + } + $type = $token; + } + + $functionDeclarations['parameters'][] = ['content' => $content, 'type' => $type,]; + $functionDeclarations['is_empty'] = false; + return $functionDeclarations; + } + + /** + * @param array $docTokens + * @param array $functionDeclarations + * @return array + */ + private function addReturnTagValue(array $tokens, array $functionDeclarations) + { + $functionDeclarations['return'] = $tokens[0]; + $functionDeclarations['is_empty'] = false; + return $functionDeclarations; + } + + /** + * @param array $docTokens + * @param array $functionDeclarations + * @return array + */ + private function addThrowsTagValue(array $tokens, array $functionDeclarations) + { + $functionDeclarations['throws'][] = $tokens[0]; + $functionDeclarations['is_empty'] = false; + return $functionDeclarations; + } +} diff --git a/Magento2/Sniffs/Commenting/FunctionsPHPDocFormattingSniff.php b/Magento2/Sniffs/Commenting/FunctionsPHPDocFormattingSniff.php new file mode 100644 index 00000000..099c2e88 --- /dev/null +++ b/Magento2/Sniffs/Commenting/FunctionsPHPDocFormattingSniff.php @@ -0,0 +1,285 @@ +functionPHPDocBlock = new FunctionPHPDocBlockParser(); + } + + /** + * @inheritDoc + */ + public function register() + { + return [T_FUNCTION]; + } + + /** + * @inheritDoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $funcReturnType = $this->analysePhp7ReturnDeclaration($phpcsFile, $stackPtr); + $funcParamTypeList = $this->analysePhp7ParamDeclarations($phpcsFile, $stackPtr); + $phpDocTokens = $this->getPhpDocTokens($phpcsFile, $stackPtr); + $hasPhp7TypeDeclarations = $funcReturnType !== false && count($funcParamTypeList['missing_type']) === 0; + + if ($phpDocTokens === false && $hasPhp7TypeDeclarations === true) { + // NO check it use all php 7 type declarations and no php doc docblock + return; + } + + if ($phpDocTokens === false && $hasPhp7TypeDeclarations === false) { + $phpcsFile->addWarning('Use php 7 type declarations or an php doc block', $stackPtr, $this->warningCode); + + return; + } + $tokens = $phpcsFile->getTokens(); + $phpDocTokensList = $this->functionPHPDocBlock->execute( + $tokens, + $phpDocTokens[0], + $phpDocTokens[1] + ); + + if ($phpDocTokensList['is_empty']) { + $phpcsFile->addWarning('Empty Docblock SHOULD NOT be used', $stackPtr, $this->warningCode); + return; + } + + $description = $phpDocTokensList['description']; + + if ($description !== false) { + $functionNameToken = $phpcsFile->findNext(T_STRING, $stackPtr, $tokens[$stackPtr]['parenthesis_opener']); + $functionName = str_replace(['_', ' ', '.', ','], '', strtolower($tokens[$functionNameToken]['content'])); + $description = str_replace(['_', ' ', '.', ','], '', strtolower($description)); + + if ($functionName === $description) { + $phpcsFile->addWarning( + sprintf( + '%s description should contain additional information beyond the name already supplies.', + ucfirst($phpDocTokensList['description']) + ), + $stackPtr, + 'InvalidDescription' + ); + } + } + + if (array_key_exists('@inheritdoc', array_flip($phpDocTokensList['tags']))) { + $phpcsFile->addWarning('The @inheritdoc tag SHOULD NOT be used', $stackPtr, $this->warningCode); + return; + } + + if (count($phpDocTokensList['parameters']) > 0) { + $this->comparePhp7WithDocBlock( + $funcParamTypeList, + $phpDocTokensList['parameters'], + $phpcsFile, + $stackPtr + ); + } + + + } + + /** + * Search for php7 return type declarations like func() : string {} + * @param File $phpcsFile + * @param $stackPtr + * @return bool|string + */ + private function analysePhp7ReturnDeclaration(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $functionNameToken = $phpcsFile->findNext(T_STRING, $stackPtr, $tokens[$stackPtr]['parenthesis_opener']); + if (strpos($tokens[$functionNameToken]['content'], '__construct') === 0) { + // magic functions start with __construct dont have php7 return type + return 'void'; + } + + $funcParamCloser = $tokens[$stackPtr]['parenthesis_closer']; + $funcReturnTypePos = $phpcsFile->findNext([T_STRING], $funcParamCloser, $tokens[$stackPtr]['scope_opener']); + $funcReturnType = false; + if ($funcReturnTypePos !== false) { + $funcReturnType = $tokens[$funcReturnTypePos]['content']; + } + + return $funcReturnType; + } + + /** + * Search for php7 return type declarations like func(bool $arg1) {} + * @param File $phpcsFile + * @param $stackPtr + * @return array + */ + private function analysePhp7ParamDeclarations(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + $funcParamStart = $tokens[$stackPtr]['parenthesis_opener']; + $funcParamCloser = $tokens[$stackPtr]['parenthesis_closer']; + + $functionParameterTokens = array_slice( + $tokens, + $funcParamStart + 1, + $funcParamCloser - ($funcParamStart + 1) + ); + + return $this->parseFunctionTokens($functionParameterTokens); + } + + /** + * Returns all parameter as list with there php 7 type declarations like + * func(string $arg1, int $arg2) + * + * @param array $tokens + * @return array + */ + private function parseFunctionTokens(array $tokens) + { + $paramType = null; + $functionParameterList = [ + 'count' => 0, + 'missing_type' => [], + 'has_type' => [], + ]; + + foreach ($tokens as $token) { + $type = $token['code']; + $content = $token['content']; + + if ($type === T_COMMA) { + $paramType = null; + continue; + } + + if ($type === T_STRING) { + $paramType = $content; + continue; + } + if ($content === ' ') { + continue; + } + + $functionParameterList['count']++; + $key = $paramType !== null ? 'has_type' : 'missing_type'; + $functionParameterList[$key][$content] = + [ + 'content' => $content, + 'type' => $paramType, + ]; + } + + return $functionParameterList; + } + + /** + * Parse the doc block for type and return declarations + * @param File $phpcsFile + * @param $stackPtr + * @return array|bool + */ + private function getPhpDocTokens(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // search for php doc block + $find = Tokens::$methodPrefixes; + $find[] = T_WHITESPACE; + + $commentEnd = $phpcsFile->findPrevious($find, $stackPtr - 1, null, true); + $commentStart = false; + + if ($commentEnd !== false) { + $endToken = $tokens[$commentEnd]; + if ($endToken['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $commentStart = $tokens[$commentEnd]['comment_opener']; + } + } + + if ($commentStart === false) { + return false; + } + + return [$commentStart + 1, $commentEnd - 1]; + } + + private function comparePhp7WithDocBlock(array $php7Tokens, array $docBlockTokens, File $phpcsFile, $stackPtr) + { + + $parsedDocToken = []; + foreach ($docBlockTokens as $token) { + $parameterName = $token['content']; + if (isset($parsedDocToken[$parameterName])) { + $phpcsFile->addWarning( + sprintf('Parameter %s is definitely multiple', $parameterName), + $stackPtr, + $this->warningCode + ); + return; + } + + $parsedDocToken[$parameterName] = $token['type']; + } + + if (count($parsedDocToken) > $php7Tokens['count']) { + $phpcsFile->addWarning( + 'More documented parameter than real function parameter', + $stackPtr, + $this->warningCode + ); + return; + } + + $hasMissingTypes = count($php7Tokens['missing_type']) > 0; + if ($hasMissingTypes === false) { + return; + } + + $php7ParamKey = array_keys($php7Tokens['missing_type']); + $parsedDocTokenKeys = array_keys($parsedDocToken); + if ($php7ParamKey !== $parsedDocTokenKeys) { + $phpcsFile->addWarning( + 'Documented parameter and real function parameter dont match', + $stackPtr, + $this->warningCode + ); + } + + foreach ($php7ParamKey as $parameter) { + if (!isset($parsedDocToken[$parameter]) || $parsedDocToken[$parameter] === false) { + $phpcsFile->addWarning( + sprintf('Type for parameter %s is missing', $parameter), + $stackPtr, + $this->warningCode + ); + } + } + } +} diff --git a/Magento2/Tests/Commenting/FunctionsPHPDocFormattingUnitTest.1.inc b/Magento2/Tests/Commenting/FunctionsPHPDocFormattingUnitTest.1.inc new file mode 100644 index 00000000..f3f34273 --- /dev/null +++ b/Magento2/Tests/Commenting/FunctionsPHPDocFormattingUnitTest.1.inc @@ -0,0 +1,61 @@ + 1, + 19 => 1, + 26 => 2, + 30 => 1, + 40 => 1, + 47 => 1, + 55 => 2, + 62 => 1, + 69 => 1, + ]; + } +} diff --git a/Magento2/ruleset.xml b/Magento2/ruleset.xml index 9d65e166..9b23bcdd 100644 --- a/Magento2/ruleset.xml +++ b/Magento2/ruleset.xml @@ -556,6 +556,10 @@ 5 warning + + 5 + warning + 5 warning