diff --git a/Magento2/Sniffs/Annotation/AnnotationFormatValidator.php b/Magento2/Sniffs/Annotation/AnnotationFormatValidator.php new file mode 100644 index 00000000..220d1a19 --- /dev/null +++ b/Magento2/Sniffs/Annotation/AnnotationFormatValidator.php @@ -0,0 +1,374 @@ +getTokens(); + $shortPtrEnd = $shortPtr; + for ($i = ($shortPtr + 1); $i < $commentEndPtr; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] === $tokens[$shortPtrEnd]['line'] + 1) { + $shortPtrEnd = $i; + } else { + break; + } + } + } + return $shortPtrEnd; + } + + /** + * Validates whether the short description has multi lines in description + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $commentEndPtr + */ + private function validateMultiLinesInShortDescription( + File $phpcsFile, + int $shortPtr, + int $commentEndPtr + ): void { + $tokens = $phpcsFile->getTokens(); + $shortPtrEnd = $this->getShortDescriptionEndPosition( + $phpcsFile, + (int)$shortPtr, + $commentEndPtr + ); + $shortPtrEndContent = $tokens[$shortPtrEnd]['content']; + if (preg_match('/^[a-z]/', $shortPtrEndContent) + && $shortPtrEnd != $shortPtr + && !preg_match('/\bSee\b/', $shortPtrEndContent) + && $tokens[$shortPtr]['line'] + 1 === $tokens[$shortPtrEnd]['line'] + && $tokens[$shortPtrEnd]['code'] !== T_DOC_COMMENT_TAG + ) { + $error = 'Short description should not be in multi lines'; + $phpcsFile->addFixableError($error, $shortPtrEnd + 1, 'MethodAnnotation'); + } + } + + /** + * Validates whether the spacing between short and long descriptions + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateSpacingBetweenShortAndLongDescriptions( + File $phpcsFile, + int $shortPtr, + int $commentEndPtr, + array $emptyTypeTokens + ): void { + $tokens = $phpcsFile->getTokens(); + $shortPtrEnd = $this->getShortDescriptionEndPosition( + $phpcsFile, + (int)$shortPtr, + $commentEndPtr + ); + $shortPtrEndContent = $tokens[$shortPtrEnd]['content']; + if (preg_match('/^[A-Z]/', $shortPtrEndContent) + && !preg_match('/\bSee\b/', $shortPtrEndContent) + && $tokens[$shortPtr]['line'] + 1 === $tokens[$shortPtrEnd]['line'] + && $tokens[$shortPtrEnd]['code'] !== T_DOC_COMMENT_TAG + ) { + $error = 'There must be exactly one blank line between lines short and long descriptions'; + $phpcsFile->addFixableError($error, $shortPtrEnd + 1, 'MethodAnnotation'); + } + if ($shortPtrEnd != $shortPtr) { + $this->validateLongDescriptionFormat($phpcsFile, $shortPtrEnd, $commentEndPtr, $emptyTypeTokens); + } else { + $this->validateLongDescriptionFormat($phpcsFile, $shortPtr, $commentEndPtr, $emptyTypeTokens); + } + } + + /** + * Validates short description format + * + * @param File $phpcsFile + * @param int $shortPtr + * @param int $stackPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateShortDescriptionFormat( + File $phpcsFile, + int $shortPtr, + int $stackPtr, + int $commentEndPtr, + array $emptyTypeTokens + ): void { + $tokens = $phpcsFile->getTokens(); + if ($tokens[$shortPtr]['line'] !== $tokens[$stackPtr]['line'] + 1) { + $error = 'No blank lines are allowed before short description'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + if (strtolower($tokens[$shortPtr]['content']) === '{@inheritdoc}') { + $error = 'If the @inheritdoc not inline it shouldn’t have braces'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + $shortPtrContent = $tokens[$shortPtr]['content']; + if (preg_match('/^\p{Ll}/u', $shortPtrContent) === 1) { + $error = 'Short description must start with a capital letter'; + $phpcsFile->addFixableError($error, $shortPtr, 'MethodAnnotation'); + } + $this->validateNoExtraNewLineBeforeShortDescription( + $phpcsFile, + $stackPtr, + $commentEndPtr, + $emptyTypeTokens + ); + $this->validateSpacingBetweenShortAndLongDescriptions( + $phpcsFile, + $shortPtr, + $commentEndPtr, + $emptyTypeTokens + ); + $this->validateMultiLinesInShortDescription( + $phpcsFile, + $shortPtr, + $commentEndPtr + ); + } + + /** + * Validates long description format + * + * @param File $phpcsFile + * @param int $shortPtrEnd + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateLongDescriptionFormat( + File $phpcsFile, + int $shortPtrEnd, + int $commentEndPtr, + array $emptyTypeTokens + ): void { + $tokens = $phpcsFile->getTokens(); + $longPtr = $phpcsFile->findNext($emptyTypeTokens, $shortPtrEnd + 1, $commentEndPtr - 1, true); + if (strtolower($tokens[$longPtr]['content']) === '@inheritdoc') { + $error = '@inheritdoc imports only short description, annotation must have long description'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + if ($longPtr !== false && $tokens[$longPtr]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$longPtr]['line'] !== $tokens[$shortPtrEnd]['line'] + 2) { + $error = 'There must be exactly one blank line between descriptions'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + if (preg_match('/^\p{Ll}/u', $tokens[$longPtr]['content']) === 1) { + $error = 'Long description must start with a capital letter'; + $phpcsFile->addFixableError($error, $longPtr, 'MethodAnnotation'); + } + } + } + + /** + * Validates tags spacing format + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param array $emptyTypeTokens + */ + public function validateTagsSpacingFormat(File $phpcsFile, int $commentStartPtr, array $emptyTypeTokens): void + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$commentStartPtr]['comment_tags'][0])) { + $firstTagPtr = $tokens[$commentStartPtr]['comment_tags'][0]; + $commentTagPtrContent = $tokens[$firstTagPtr]['content']; + $prevPtr = $phpcsFile->findPrevious($emptyTypeTokens, $firstTagPtr - 1, $commentStartPtr, true); + if ($tokens[$firstTagPtr]['line'] !== $tokens[$prevPtr]['line'] + 2 + && strtolower($commentTagPtrContent) !== '@inheritdoc' + ) { + $error = 'There must be exactly one blank line before tags'; + $phpcsFile->addFixableError($error, $firstTagPtr, 'MethodAnnotation'); + } + } + } + + /** + * Validates tag grouping format + * + * @param File $phpcsFile + * @param int $commentStartPtr + */ + public function validateTagGroupingFormat(File $phpcsFile, int $commentStartPtr): void + { + $tokens = $phpcsFile->getTokens(); + $tagGroups = []; + $groupId = 0; + $paramGroupId = null; + foreach ($tokens[$commentStartPtr]['comment_tags'] as $position => $tag) { + if ($position > 0) { + $prevPtr = $phpcsFile->findPrevious( + T_DOC_COMMENT_STRING, + $tag - 1, + $tokens[$commentStartPtr]['comment_tags'][$position - 1] + ); + if ($prevPtr === false) { + $prevPtr = $tokens[$commentStartPtr]['comment_tags'][$position - 1]; + } + + if ($tokens[$prevPtr]['line'] !== $tokens[$tag]['line'] - 1) { + $groupId++; + } + } + + if (strtolower($tokens[$tag]['content']) === '@param') { + if ($paramGroupId !== null + && $paramGroupId !== $groupId) { + $error = 'Parameter tags must be grouped together'; + $phpcsFile->addFixableError($error, $tag, 'MethodAnnotation'); + } + if ($paramGroupId === null) { + $paramGroupId = $groupId; + } + } + $tagGroups[$groupId][] = $tag; + } + } + + /** + * Validates tag aligning format + * + * @param File $phpcsFile + * @param int $commentStartPtr + */ + public function validateTagAligningFormat(File $phpcsFile, int $commentStartPtr): void + { + $tokens = $phpcsFile->getTokens(); + $noAlignmentPositions = []; + $actualPositions = []; + $stackPtr = null; + foreach ($tokens[$commentStartPtr]['comment_tags'] as $tag) { + $content = $tokens[$tag]['content']; + if (preg_match('/^@/', $content) && ($tokens[$tag]['line'] === $tokens[$tag + 2]['line'])) { + $noAlignmentPositions[] = $tokens[$tag + 1]['column'] + 1; + $actualPositions[] = $tokens[$tag + 2]['column']; + $stackPtr = $stackPtr ?? $tag; + } + } + + if (!$this->allTagsAligned($actualPositions) + && !$this->noneTagsAligned($actualPositions, $noAlignmentPositions)) { + $phpcsFile->addFixableError( + 'Tags visual alignment must be consistent', + $stackPtr, + 'MethodArguments' + ); + } + } + + /** + * Check whether all docblock params are aligned. + * + * @param array $actualPositions + * @return bool + */ + private function allTagsAligned(array $actualPositions): bool + { + return count(array_unique($actualPositions)) === 1; + } + + /** + * Check whether all docblock params are not aligned. + * + * @param array $actualPositions + * @param array $noAlignmentPositions + * @return bool + */ + private function noneTagsAligned(array $actualPositions, array $noAlignmentPositions): bool + { + return $actualPositions === $noAlignmentPositions; + } + + /** + * Validates extra newline before short description + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + private function validateNoExtraNewLineBeforeShortDescription( + File $phpcsFile, + int $commentStartPtr, + int $commentEndPtr, + array $emptyTypeTokens + ): void { + $tokens = $phpcsFile->getTokens(); + $prevPtr = $phpcsFile->findPrevious($emptyTypeTokens, $commentEndPtr - 1, $commentStartPtr, true); + if ($tokens[$prevPtr]['line'] < ($tokens[$commentEndPtr]['line'] - 1)) { + $error = 'Additional blank lines found at end of the annotation block'; + $phpcsFile->addFixableError($error, $commentEndPtr, 'MethodAnnotation'); + } + } + + /** + * Validates structure description format + * + * @param File $phpcsFile + * @param int $commentStartPtr + * @param int $shortPtr + * @param int $commentEndPtr + * @param array $emptyTypeTokens + */ + public function validateDescriptionFormatStructure( + File $phpcsFile, + int $commentStartPtr, + int $shortPtr, + int $commentEndPtr, + array $emptyTypeTokens + ): void { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$commentStartPtr]['comment_tags'][0]) + ) { + $commentTagPtr = $tokens[$commentStartPtr]['comment_tags'][0]; + $commentTagPtrContent = $tokens[$commentTagPtr]['content']; + if ($tokens[$shortPtr]['code'] !== T_DOC_COMMENT_STRING + && strtolower($commentTagPtrContent) !== '@inheritdoc' + ) { + $error = 'Missing short description'; + $phpcsFile->addFixableError($error, $commentStartPtr, 'MethodAnnotation'); + } else { + $this->validateShortDescriptionFormat( + $phpcsFile, + (int)$shortPtr, + (int)$commentStartPtr, + (int)$commentEndPtr, + $emptyTypeTokens + ); + } + } else { + $this->validateShortDescriptionFormat( + $phpcsFile, + (int)$shortPtr, + $commentStartPtr, + $commentEndPtr, + $emptyTypeTokens + ); + } + } +} diff --git a/Magento2/Sniffs/Annotation/MethodAnnotationStructureSniff.php b/Magento2/Sniffs/Annotation/MethodAnnotationStructureSniff.php new file mode 100644 index 00000000..fed274a9 --- /dev/null +++ b/Magento2/Sniffs/Annotation/MethodAnnotationStructureSniff.php @@ -0,0 +1,86 @@ +annotationFormatValidator = new AnnotationFormatValidator(); + } + + /** + * @inheritdoc + */ + public function register() + { + return [ + T_FUNCTION + ]; + } + + /** + * @inheritdoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $commentStartPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, ($stackPtr), 0); + $commentEndPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr), 0); + if (!$commentStartPtr) { + $phpcsFile->addError('Comment block is missing', $stackPtr, 'MethodArguments'); + return; + } + $commentCloserPtr = $tokens[$commentStartPtr]['comment_closer']; + $functionPtrContent = $tokens[$stackPtr + 2]['content']; + if (preg_match('/(?i)__construct/', $functionPtrContent)) { + return; + } + $emptyTypeTokens = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR + ]; + $shortPtr = $phpcsFile->findNext($emptyTypeTokens, $commentStartPtr + 1, $commentCloserPtr, true); + if ($shortPtr === false) { + $error = 'Annotation block is empty'; + $phpcsFile->addError($error, $commentStartPtr, 'MethodAnnotation'); + } else { + $this->annotationFormatValidator->validateDescriptionFormatStructure( + $phpcsFile, + $commentStartPtr, + (int)$shortPtr, + $commentEndPtr, + $emptyTypeTokens + ); + if (empty($tokens[$commentStartPtr]['comment_tags'])) { + return; + } + $this->annotationFormatValidator->validateTagsSpacingFormat( + $phpcsFile, + $commentStartPtr, + $emptyTypeTokens + ); + $this->annotationFormatValidator->validateTagGroupingFormat($phpcsFile, $commentStartPtr); + $this->annotationFormatValidator->validateTagAligningFormat($phpcsFile, $commentStartPtr); + } + } +} diff --git a/Magento2/Sniffs/Annotation/MethodArgumentsSniff.php b/Magento2/Sniffs/Annotation/MethodArgumentsSniff.php new file mode 100644 index 00000000..2ebb12d4 --- /dev/null +++ b/Magento2/Sniffs/Annotation/MethodArgumentsSniff.php @@ -0,0 +1,684 @@ +validTokensBeforeClosingCommentTag); + } + + /** + * Validates whether comment block exists + * + * @param File $phpcsFile + * @param int $previousCommentClosePtr + * @param int $stackPtr + * @return bool + */ + private function validateCommentBlockExists(File $phpcsFile, int $previousCommentClosePtr, int $stackPtr): bool + { + $tokens = $phpcsFile->getTokens(); + for ($tempPtr = $previousCommentClosePtr + 1; $tempPtr < $stackPtr; $tempPtr++) { + if (!$this->isTokenBeforeClosingCommentTagValid($tokens[$tempPtr]['type'])) { + return false; + } + } + return true; + } + + /** + * Checks whether the parameter type is invalid + * + * @param string $type + * @return bool + */ + private function isInvalidType(string $type): bool + { + return in_array(strtolower($type), $this->invalidTypes); + } + + /** + * Get arguments from method signature + * + * @param File $phpcsFile + * @param int $openParenthesisPtr + * @param int $closedParenthesisPtr + * @return array + */ + private function getMethodArguments(File $phpcsFile, int $openParenthesisPtr, int $closedParenthesisPtr): array + { + $tokens = $phpcsFile->getTokens(); + $methodArguments = []; + for ($i = $openParenthesisPtr; $i < $closedParenthesisPtr; $i++) { + $argumentsPtr = $phpcsFile->findNext(T_VARIABLE, $i + 1, $closedParenthesisPtr); + if ($argumentsPtr === false) { + break; + } elseif ($argumentsPtr < $closedParenthesisPtr) { + $arguments = $tokens[$argumentsPtr]['content']; + $methodArguments[] = $arguments; + $i = $argumentsPtr - 1; + } + } + return $methodArguments; + } + + /** + * Get parameters from method annotation + * + * @param array $paramDefinitions + * @return array + */ + private function getMethodParameters(array $paramDefinitions): array + { + $paramName = []; + foreach ($paramDefinitions as $paramDefinition) { + if (isset($paramDefinition['paramName'])) { + $paramName[] = $paramDefinition['paramName']; + } + } + return $paramName; + } + + /** + * Validates whether @inheritdoc without braces [@inheritdoc] exists or not + * + * @param File $phpcsFile + * @param int $previousCommentOpenPtr + * @param int $previousCommentClosePtr + */ + private function validateInheritdocAnnotationWithoutBracesExists( + File $phpcsFile, + int $previousCommentOpenPtr, + int $previousCommentClosePtr + ): bool { + return $this->validateInheritdocAnnotationExists( + $phpcsFile, + $previousCommentOpenPtr, + $previousCommentClosePtr, + '@inheritdoc' + ); + } + + /** + * Validates whether @inheritdoc with braces [{@inheritdoc}] exists or not + * + * @param File $phpcsFile + * @param int $previousCommentOpenPtr + * @param int $previousCommentClosePtr + */ + private function validateInheritdocAnnotationWithBracesExists( + File $phpcsFile, + int $previousCommentOpenPtr, + int $previousCommentClosePtr + ): bool { + return $this->validateInheritdocAnnotationExists( + $phpcsFile, + $previousCommentOpenPtr, + $previousCommentClosePtr, + '{@inheritdoc}' + ); + } + + /** + * Validates inheritdoc annotation exists + * + * @param File $phpcsFile + * @param int $previousCommentOpenPtr + * @param int $previousCommentClosePtr + * @param string $inheritdocAnnotation + * @return bool + */ + private function validateInheritdocAnnotationExists( + File $phpcsFile, + int $previousCommentOpenPtr, + int $previousCommentClosePtr, + string $inheritdocAnnotation + ): bool { + $tokens = $phpcsFile->getTokens(); + for ($ptr = $previousCommentOpenPtr; $ptr < $previousCommentClosePtr; $ptr++) { + if (strtolower($tokens[$ptr]['content']) === $inheritdocAnnotation) { + return true; + } + } + return false; + } + + /** + * Validates if annotation exists for parameter in method annotation + * + * @param File $phpcsFile + * @param int $argumentsCount + * @param int $parametersCount + * @param int $previousCommentOpenPtr + * @param int $previousCommentClosePtr + * @param int $stackPtr + */ + private function validateParameterAnnotationForArgumentExists( + File $phpcsFile, + int $argumentsCount, + int $parametersCount, + int $previousCommentOpenPtr, + int $previousCommentClosePtr, + int $stackPtr + ): void { + if ($argumentsCount > 0 && $parametersCount === 0) { + $inheritdocAnnotationWithoutBracesExists = $this->validateInheritdocAnnotationWithoutBracesExists( + $phpcsFile, + $previousCommentOpenPtr, + $previousCommentClosePtr + ); + $inheritdocAnnotationWithBracesExists = $this->validateInheritdocAnnotationWithBracesExists( + $phpcsFile, + $previousCommentOpenPtr, + $previousCommentClosePtr + ); + if ($inheritdocAnnotationWithBracesExists) { + $phpcsFile->addFixableError( + '{@inheritdoc} does not import parameter annotation', + $stackPtr, + 'MethodArguments' + ); + } elseif ($this->validateCommentBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr) + && !$inheritdocAnnotationWithoutBracesExists + ) { + $phpcsFile->addFixableError( + 'Missing @param for argument in method annotation', + $stackPtr, + 'MethodArguments' + ); + } + } + } + + /** + * Validates whether comment block have extra the parameters listed in method annotation + * + * @param File $phpcsFile + * @param int $argumentsCount + * @param int $parametersCount + * @param int $stackPtr + */ + private function validateCommentBlockDoesnotHaveExtraParameterAnnotation( + File $phpcsFile, + int $argumentsCount, + int $parametersCount, + int $stackPtr + ): void { + if ($argumentsCount < $parametersCount && $argumentsCount > 0) { + $phpcsFile->addFixableError( + 'Extra @param found in method annotation', + $stackPtr, + 'MethodArguments' + ); + } elseif ($argumentsCount > 0 && $argumentsCount != $parametersCount && $parametersCount != 0) { + $phpcsFile->addFixableError( + '@param is not found for one or more params in method annotation', + $stackPtr, + 'MethodArguments' + ); + } + } + + /** + * Validates whether the argument name exists in method parameter annotation + * + * @param int $stackPtr + * @param int $ptr + * @param File $phpcsFile + * @param array $methodArguments + * @param array $paramDefinitions + */ + private function validateArgumentNameInParameterAnnotationExists( + int $stackPtr, + int $ptr, + File $phpcsFile, + array $methodArguments, + array $paramDefinitions + ): void { + $parameterNames = $this->getMethodParameters($paramDefinitions); + if (!in_array($methodArguments[$ptr], $parameterNames)) { + $error = $methodArguments[$ptr] . ' parameter is missing in method annotation'; + $phpcsFile->addFixableError($error, $stackPtr, 'MethodArguments'); + } + } + + /** + * Validates whether parameter present in method signature + * + * @param int $ptr + * @param int $paramDefinitionsArguments + * @param array $methodArguments + * @param File $phpcsFile + * @param array $paramPointers + */ + private function validateParameterPresentInMethodSignature( + int $ptr, + string $paramDefinitionsArguments, + array $methodArguments, + File $phpcsFile, + array $paramPointers + ): void { + if (!in_array($paramDefinitionsArguments, $methodArguments)) { + $phpcsFile->addFixableError( + $paramDefinitionsArguments . ' parameter is missing in method arguments signature', + $paramPointers[$ptr], + 'MethodArguments' + ); + } + } + + /** + * Validates whether the parameters are in order or not in method annotation + * + * @param array $paramDefinitions + * @param array $methodArguments + * @param File $phpcsFile + * @param array $paramPointers + */ + private function validateParameterOrderIsCorrect( + array $paramDefinitions, + array $methodArguments, + File $phpcsFile, + array $paramPointers + ): void { + $parameterNames = $this->getMethodParameters($paramDefinitions); + $paramDefinitionsCount = count($paramDefinitions); + for ($ptr = 0; $ptr < $paramDefinitionsCount; $ptr++) { + if (isset($methodArguments[$ptr]) && isset($parameterNames[$ptr]) + && in_array($methodArguments[$ptr], $parameterNames) + ) { + if ($methodArguments[$ptr] != $parameterNames[$ptr]) { + $phpcsFile->addFixableError( + $methodArguments[$ptr] . ' parameter is not in order', + $paramPointers[$ptr], + 'MethodArguments' + ); + } + } + } + } + + /** + * Validate whether duplicate annotation present in method annotation + * + * @param int $stackPtr + * @param array $paramDefinitions + * @param array $paramPointers + * @param File $phpcsFile + * @param array $methodArguments + */ + private function validateDuplicateAnnotationDoesnotExists( + int $stackPtr, + array $paramDefinitions, + array $paramPointers, + File $phpcsFile, + array $methodArguments + ): void { + $argumentsCount = count($methodArguments); + $parametersCount = count($paramPointers); + if ($argumentsCount <= $parametersCount && $argumentsCount > 0) { + $duplicateParameters = []; + foreach ($paramDefinitions as $i => $paramDefinition) { + if (isset($paramDefinition['paramName'])) { + $parameterContent = $paramDefinition['paramName']; + foreach (array_slice($paramDefinitions, $i + 1) as $nextParamDefinition) { + if (isset($nextParamDefinition['paramName']) + && $parameterContent === $nextParamDefinition['paramName'] + ) { + $duplicateParameters[] = $parameterContent; + } + } + } + } + foreach ($duplicateParameters as $value) { + $phpcsFile->addFixableError( + $value . ' duplicate found in method annotation', + $stackPtr, + 'MethodArguments' + ); + } + } + } + + /** + * Validate parameter annotation format is correct or not + * + * @param int $ptr + * @param File $phpcsFile + * @param array $methodArguments + * @param array $paramDefinitions + * @param array $paramPointers + */ + private function validateParameterAnnotationFormatIsCorrect( + int $ptr, + File $phpcsFile, + array $methodArguments, + array $paramDefinitions, + array $paramPointers + ): void { + switch (count($paramDefinitions)) { + case 0: + $phpcsFile->addFixableError( + 'Missing both type and parameter', + $paramPointers[$ptr], + 'MethodArguments' + ); + break; + case 1: + if (preg_match('/^\$.*/', $paramDefinitions[0])) { + $phpcsFile->addError( + 'Type is not specified', + $paramPointers[$ptr], + 'MethodArguments' + ); + } + break; + case 2: + if ($this->isInvalidType($paramDefinitions[0])) { + $phpcsFile->addFixableError( + $paramDefinitions[0] . ' is not a valid PHP type', + $paramPointers[$ptr], + 'MethodArguments' + ); + } + $this->validateParameterPresentInMethodSignature( + $ptr, + ltrim($paramDefinitions[1], '&'), + $methodArguments, + $phpcsFile, + $paramPointers + ); + break; + default: + if (preg_match('/^\$.*/', $paramDefinitions[0])) { + $phpcsFile->addError( + 'Type is not specified', + $paramPointers[$ptr], + 'MethodArguments' + ); + if ($this->isInvalidType($paramDefinitions[0])) { + $phpcsFile->addFixableError( + $paramDefinitions[0] . ' is not a valid PHP type', + $paramPointers[$ptr], + 'MethodArguments' + ); + } + } + break; + } + } + + /** + * Validate method parameter annotations + * + * @param int $stackPtr + * @param array $paramDefinitions + * @param array $paramPointers + * @param File $phpcsFile + * @param array $methodArguments + * @param int $previousCommentOpenPtr + * @param int $previousCommentClosePtr + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + private function validateMethodParameterAnnotations( + int $stackPtr, + array $paramDefinitions, + array $paramPointers, + File $phpcsFile, + array $methodArguments, + int $previousCommentOpenPtr, + int $previousCommentClosePtr + ): void { + $argumentCount = count($methodArguments); + $paramCount = count($paramPointers); + $this->validateParameterAnnotationForArgumentExists( + $phpcsFile, + $argumentCount, + $paramCount, + $previousCommentOpenPtr, + $previousCommentClosePtr, + $stackPtr + ); + $this->validateCommentBlockDoesnotHaveExtraParameterAnnotation( + $phpcsFile, + $argumentCount, + $paramCount, + $stackPtr + ); + $this->validateDuplicateAnnotationDoesnotExists( + $stackPtr, + $paramDefinitions, + $paramPointers, + $phpcsFile, + $methodArguments + ); + $this->validateParameterOrderIsCorrect( + $paramDefinitions, + $methodArguments, + $phpcsFile, + $paramPointers + ); + $this->validateFormattingConsistency( + $paramDefinitions, + $methodArguments, + $phpcsFile, + $paramPointers + ); + $tokens = $phpcsFile->getTokens(); + + foreach ($methodArguments as $ptr => $methodArgument) { + if (isset($paramPointers[$ptr])) { + $this->validateArgumentNameInParameterAnnotationExists( + $stackPtr, + $ptr, + $phpcsFile, + $methodArguments, + $paramDefinitions + ); + $paramContent = $tokens[$paramPointers[$ptr] + 2]['content']; + $paramContentExplode = explode(' ', $paramContent); + $this->validateParameterAnnotationFormatIsCorrect( + $ptr, + $phpcsFile, + $methodArguments, + $paramContentExplode, + $paramPointers + ); + } + } + } + + /** + * @inheritdoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $numTokens = count($tokens); + $previousCommentOpenPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1, 0); + $previousCommentClosePtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $stackPtr - 1, 0); + if ($previousCommentClosePtr && $previousCommentOpenPtr) { + if (!$this->validateCommentBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr)) { + $phpcsFile->addError('Comment block is missing', $stackPtr, 'MethodArguments'); + return; + } + } else { + return; + } + $openParenthesisPtr = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr + 1, $numTokens); + $closedParenthesisPtr = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, $stackPtr + 1, $numTokens); + $methodArguments = $this->getMethodArguments($phpcsFile, $openParenthesisPtr, $closedParenthesisPtr); + $paramPointers = $paramDefinitions = []; + for ($tempPtr = $previousCommentOpenPtr; $tempPtr < $previousCommentClosePtr; $tempPtr++) { + if (strtolower($tokens[$tempPtr]['content']) === '@param') { + $paramPointers[] = $tempPtr; + $content = preg_replace('/\s+/', ' ', $tokens[$tempPtr + 2]['content'], 2); + $paramAnnotationParts = explode(' ', $content, 3); + if (count($paramAnnotationParts) === 1) { + if ((preg_match('/^\$.*/', $paramAnnotationParts[0]))) { + $paramDefinitions[] = [ + 'type' => null, + 'paramName' => rtrim(ltrim($tokens[$tempPtr + 2]['content'], '&'), ','), + 'comment' => null + ]; + } else { + $paramDefinitions[] = [ + 'type' => $tokens[$tempPtr + 2]['content'], + 'paramName' => null, + 'comment' => null + ]; + } + } else { + $paramDefinitions[] = [ + 'type' => $paramAnnotationParts[0], + 'paramName' => rtrim(ltrim($paramAnnotationParts[1], '&'), ','), + 'comment' => $paramAnnotationParts[2] ?? null, + ]; + } + } + } + $this->validateMethodParameterAnnotations( + $stackPtr, + $paramDefinitions, + $paramPointers, + $phpcsFile, + $methodArguments, + $previousCommentOpenPtr, + $previousCommentClosePtr + ); + } + + /** + * Validates function params format consistency. + * + * @param array $paramDefinitions + * @param array $methodArguments + * @param File $phpcsFile + * @param array $paramPointers + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * + * @see https://devdocs.magento.com/guides/v2.4/coding-standards/docblock-standard-general.html#format-consistency + */ + private function validateFormattingConsistency( + array $paramDefinitions, + array $methodArguments, + File $phpcsFile, + array $paramPointers + ): void { + $argumentPositions = []; + $commentPositions = []; + $tokens = $phpcsFile->getTokens(); + foreach ($methodArguments as $ptr => $methodArgument) { + if (isset($paramPointers[$ptr])) { + $paramContent = $tokens[$paramPointers[$ptr] + 2]['content']; + $paramDefinition = $paramDefinitions[$ptr]; + $argumentPositions[] = strpos($paramContent, $paramDefinition['paramName']); + $commentPositions[] = $paramDefinition['comment'] + ? strrpos($paramContent, $paramDefinition['comment']) : null; + } + } + if (!$this->allParamsAligned($argumentPositions, $commentPositions) + && !$this->noneParamsAligned($argumentPositions, $commentPositions, $paramDefinitions)) { + $phpcsFile->addFixableError( + 'Method arguments visual alignment must be consistent', + $paramPointers[0], + 'MethodArguments' + ); + } + } + + /** + * Check all params are aligned. + * + * @param array $argumentPositions + * @param array $commentPositions + * @return bool + */ + private function allParamsAligned(array $argumentPositions, array $commentPositions): bool + { + return count(array_unique($argumentPositions)) === 1 + && count(array_unique(array_filter($commentPositions))) <= 1; + } + + /** + * Check none of params are aligned. + * + * @param array $argumentPositions + * @param array $commentPositions + * @param array $paramDefinitions + * @return bool + */ + private function noneParamsAligned(array $argumentPositions, array $commentPositions, array $paramDefinitions): bool + { + $flag = true; + foreach ($argumentPositions as $index => $argumentPosition) { + $commentPosition = $commentPositions[$index]; + $type = $paramDefinitions[$index]['type']; + if ($type === null) { + continue; + } + $paramName = $paramDefinitions[$index]['paramName']; + if (($argumentPosition !== strlen($type) + 1) || + (isset($commentPosition) && ($commentPosition !== $argumentPosition + strlen($paramName) + 1))) { + $flag = false; + break; + } + } + + return $flag; + } +} diff --git a/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php b/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php index d7a19621..f127cf17 100644 --- a/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php +++ b/Magento2/Sniffs/GraphQL/ValidArgumentNameSniff.php @@ -128,8 +128,10 @@ private function getArgumentListClosePointer($openParenthesisPointer, array $tok } /** - * Seeks the next available {@link T_OPEN_PARENTHESIS} token that comes directly after $stackPointer. - * token. + * Find the argument list open pointer + * + * Seeks the next available {@link T_OPEN_PARENTHESIS} token + * that comes directly after $stackPointer token. * * @param int $stackPointer * @param array $tokens diff --git a/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php index a9c5f245..466f2c27 100644 --- a/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php +++ b/Magento2/Sniffs/GraphQL/ValidEnumValueSniff.php @@ -68,8 +68,10 @@ public function process(File $phpcsFile, $stackPtr) } /** - * Seeks the next available token of type {@link T_CLOSE_CURLY_BRACKET} in $tokens and returns its - * pointer. + * Find the closing curly bracket pointer + * + * Seeks the next available token of type {@link T_CLOSE_CURLY_BRACKET} + * in $tokens and returns its pointer. * * @param int $startPointer * @param array $tokens @@ -81,10 +83,12 @@ private function getClosingCurlyBracketPointer($startPointer, array $tokens) } /** - * Seeks the next available token of type {@link T_OPEN_CURLY_BRACKET} in $tokens and returns its - * pointer. + * Find the opening curly bracket pointer + * + * Seeks the next available token of type {@link T_OPEN_CURLY_BRACKET} + * in $tokens and returns its pointer. * - * @param $startPointer + * @param int $startPointer * @param array $tokens * @return bool|int */ diff --git a/Magento2/Sniffs/Html/HtmlBindingSniff.php b/Magento2/Sniffs/Html/HtmlBindingSniff.php new file mode 100644 index 00000000..63718b5b --- /dev/null +++ b/Magento2/Sniffs/Html/HtmlBindingSniff.php @@ -0,0 +1,91 @@ +getTokensAsString($stackPointer, count($file->getTokens())); + $dom = new \DOMDocument(); + try { + // phpcs:disable Generic.PHP.NoSilencedErrors + @$dom->loadHTML($html); + return $dom; + } catch (\Throwable $exception) { + return null; + } + } + + return null; + } + + /** + * @inheritDoc + * + * Find HTML data bindings and check variables used. + */ + public function process(File $phpcsFile, $stackPtr) + { + if (!$dom = $this->loadHtmlDocument($stackPtr, $phpcsFile)) { + return; + } + + /** @var string[] $htmlBindings */ + $htmlBindings = []; + $domXpath = new \DOMXPath($dom); + $dataBindAttributes = $domXpath->query('//@*[name() = "data-bind"]'); + foreach ($dataBindAttributes as $dataBindAttribute) { + $knockoutBinding = $dataBindAttribute->nodeValue; + preg_match('/^(.+\s*?)?html\s*?\:(.+)/ims', $knockoutBinding, $htmlBindingStart); + if ($htmlBindingStart) { + $htmlBinding = trim(preg_replace('/\,[a-z0-9\_\s]+\:.+/ims', '', $htmlBindingStart[2])); + $htmlBindings[] = $htmlBinding; + } + } + $htmlAttributes = $domXpath->query('//@*[name() = "html"]'); + foreach ($htmlAttributes as $htmlAttribute) { + $magentoBinding = $htmlAttribute->nodeValue; + $htmlBindings[] = trim($magentoBinding); + } + foreach ($htmlBindings as $htmlBinding) { + if (!preg_match('/^[0-9\\\'\"]/ims', $htmlBinding) + && !preg_match('/UnsanitizedHtml(\(.*?\))*?$/', $htmlBinding) + ) { + $phpcsFile->addError( + 'Variables/functions used for HTML binding must have UnsanitizedHtml suffix' + . ' - "' . $htmlBinding . '" doesn\'t,' . PHP_EOL + . 'consider using text binding if the value is supposed to be text', + null, + 'UIComponentTemplate.KnockoutBinding.HtmlSuffix' + ); + } + } + } +} diff --git a/Magento2/Sniffs/Html/HtmlDirectiveSniff.php b/Magento2/Sniffs/Html/HtmlDirectiveSniff.php new file mode 100644 index 00000000..6179f90f --- /dev/null +++ b/Magento2/Sniffs/Html/HtmlDirectiveSniff.php @@ -0,0 +1,257 @@ +usedVariables = []; + $this->unfilteredVariables = []; + if ($stackPtr !== 0) { + return; + } + + $html = $phpcsFile->getTokensAsString($stackPtr, count($phpcsFile->getTokens())); + + if (empty($html)) { + return; + } + + $html = $this->processIfDirectives($html, $phpcsFile); + $html = $this->processDependDirectives($html, $phpcsFile); + $html = $this->processForDirectives($html, $phpcsFile); + $html = $this->processVarDirectivesAndParams($html, $phpcsFile); + + $this->validateDefinedVariables($phpcsFile, $html); + } + + /** + * Process the {{if}} directives in the file + * + * @param string $html + * @param File $phpcsFile + * @return string The processed template + */ + private function processIfDirectives(string $html, File $phpcsFile): string + { + if (preg_match_all(Template::CONSTRUCTION_IF_PATTERN, $html, $constructions, PREG_SET_ORDER)) { + foreach ($constructions as $construction) { + // validate {{if }} + $this->validateVariableUsage($phpcsFile, $construction[1]); + $html = str_replace($construction[0], $construction[2] . ($construction[4] ?? ''), $html); + } + } + + return $html; + } + + /** + * Process the {{depend}} directives in the file + * + * @param string $html + * @param File $phpcsFile + * @return string The processed template + */ + private function processDependDirectives(string $html, File $phpcsFile): string + { + if (preg_match_all(Template::CONSTRUCTION_DEPEND_PATTERN, $html, $constructions, PREG_SET_ORDER)) { + foreach ($constructions as $construction) { + // validate {{depend }} + $this->validateVariableUsage($phpcsFile, $construction[1]); + $html = str_replace($construction[0], $construction[2], $html); + } + } + + return $html; + } + + /** + * Process the {{for}} directives in the file + * + * @param string $html + * @param File $phpcsFile + * @return string The processed template + */ + private function processForDirectives(string $html, File $phpcsFile): string + { + if (preg_match_all(Template::LOOP_PATTERN, $html, $constructions, PREG_SET_ORDER)) { + foreach ($constructions as $construction) { + // validate {{for in }} + $this->validateVariableUsage($phpcsFile, $construction['loopData']); + $html = str_replace($construction[0], $construction['loopBody'], $html); + } + } + + return $html; + } + + /** + * Process the all var directives and var directive params in the file + * + * @param string $html + * @param File $phpcsFile + * @return string The processed template + */ + private function processVarDirectivesAndParams(string $html, File $phpcsFile): string + { + if (preg_match_all(Template::CONSTRUCTION_PATTERN, $html, $constructions, PREG_SET_ORDER)) { + foreach ($constructions as $construction) { + if (empty($construction[2])) { + continue; + } + + if ($construction[1] === 'var') { + $this->validateVariableUsage($phpcsFile, $construction[2]); + } else { + $this->validateDirectiveBody($phpcsFile, $construction[2]); + } + } + } + + return $html; + } + + /** + * Validate directive body is valid. e.g. {{somedir }} + * + * @param File $phpcsFile + * @param string $body + */ + private function validateDirectiveBody(File $phpcsFile, string $body): void + { + $parameterTokenizer = new Template\Tokenizer\Parameter(); + $parameterTokenizer->setString($body); + $params = $parameterTokenizer->tokenize(); + + foreach ($params as $param) { + if (substr($param, 0, 1) === '$') { + $this->validateVariableUsage($phpcsFile, substr($param, 1)); + } + } + } + + /** + * Validate directive variable usage is valid. e.g. {{var }} or {{somedir some_param="$foo.bar()"}} + * + * @param File $phpcsFile + * @param string $body + */ + private function validateVariableUsage(File $phpcsFile, string $body): void + { + $this->usedVariables[] = 'var ' . trim($body); + if (strpos($body, '|') !== false) { + $this->unfilteredVariables[] = 'var ' . trim(explode('|', $body, 2)[0]); + } + $variableTokenizer = new Template\Tokenizer\Variable(); + $variableTokenizer->setString($body); + $stack = $variableTokenizer->tokenize(); + + if (empty($stack)) { + return; + } + + foreach ($stack as $token) { + // As a static analyzer there are no data types to know if this is a DataObject so allow all get* methods + if ($token['type'] === 'method' && substr($token['name'], 0, 3) !== 'get') { + $phpcsFile->addError( + 'Template directives may not invoke methods. Only scalar array access is allowed.' . PHP_EOL + . 'Found "' . trim($body) . '"', + null, + 'HtmlTemplates.DirectiveUsage.ProhibitedMethodCall' + ); + } + } + } + + /** + * Validate the variables defined in the template comment block match the variables actually used in the template + * + * @param File $phpcsFile + * @param string $templateText + */ + private function validateDefinedVariables(File $phpcsFile, string $templateText): void + { + preg_match('//us', $templateText, $matches); + + $definedVariables = []; + + if (!empty($matches[1])) { + $definedVariables = json_decode(str_replace("\n", '', $matches[1]), true); + if (json_last_error()) { + $phpcsFile->addError( + 'Template @vars comment block contains invalid JSON.', + null, + 'HtmlTemplates.DirectiveUsage.InvalidVarsJSON' + ); + return; + } + + foreach ($definedVariables as $var => $label) { + if (empty($label)) { + $phpcsFile->addError( + 'Template @vars comment block contains invalid label.' . PHP_EOL + . 'Label for variable "' . $var . '" is empty.', + null, + 'HtmlTemplates.DirectiveUsage.InvalidVariableLabel' + ); + } + } + + $definedVariables = array_keys($definedVariables); + foreach ($definedVariables as $definedVariable) { + if (strpos($definedVariable, '|') !== false) { + $definedVariables[] = trim(explode('|', $definedVariable, 2)[0]); + } + } + } + + $undefinedVariables = array_diff($this->usedVariables, $definedVariables, $this->unfilteredVariables); + foreach ($undefinedVariables as $undefinedVariable) { + $phpcsFile->addError( + 'Template @vars comment block is missing a variable used in the template.' . PHP_EOL + . 'Missing variable: ' . $undefinedVariable, + null, + 'HtmlTemplates.DirectiveUsage.UndefinedVariable' + ); + } + } +} diff --git a/Magento2/Sniffs/Less/AvoidIdSniff.php b/Magento2/Sniffs/Less/AvoidIdSniff.php new file mode 100644 index 00000000..d4e1b213 --- /dev/null +++ b/Magento2/Sniffs/Less/AvoidIdSniff.php @@ -0,0 +1,100 @@ + div, + * #foo ~ div, + * #foo\3Abar ~ div, + * #foo\:bar ~ div, + * #foo.bar .baz, + * div#foo { + * blah: 'abc'; + * } + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Find the next non-selector token + $nextToken = $phpcsFile->findNext($this->selectorTokens, $stackPtr + 1, null, true); + + // Anything except a { or a , means this is not a selector + if ($nextToken !== false && in_array($tokens[$nextToken]['code'], [T_OPEN_CURLY_BRACKET, T_COMMA])) { + $phpcsFile->addError('Id selector is used', $stackPtr, 'IdSelectorUsage'); + } + } +} diff --git a/Magento2/Sniffs/Less/BracesFormattingSniff.php b/Magento2/Sniffs/Less/BracesFormattingSniff.php new file mode 100644 index 00000000..54575160 --- /dev/null +++ b/Magento2/Sniffs/Less/BracesFormattingSniff.php @@ -0,0 +1,88 @@ +getTokens(); + + if (T_OPEN_CURLY_BRACKET === $tokens[$stackPtr]['code']) { + if (TokenizerSymbolsInterface::WHITESPACE !== $tokens[$stackPtr - 1]['content']) { + $phpcsFile->addError('Space before opening brace is missing', $stackPtr, 'SpacingBeforeOpen'); + } + + return; + } + + $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($next === false) { + return; + } + + if (!in_array($tokens[$next]['code'], [T_CLOSE_TAG, T_CLOSE_CURLY_BRACKET])) { + $found = (($tokens[$next]['line'] - $tokens[$stackPtr]['line']) - 1); + if ($found !== 1) { + $error = 'Expected one blank line after closing brace of class definition; %s found'; + $data = [$found]; + // Will be implemented in MAGETWO-49778 + //$phpcsFile->addWarning($error, $stackPtr, 'SpacingAfterClose', $data); + } + } + + // Ignore nested style definitions from here on. The spacing before the closing brace + // (a single blank line) will be enforced by the above check, which ensures there is a + // blank line after the last nested class. + $found = $phpcsFile->findPrevious( + T_CLOSE_CURLY_BRACKET, + ($stackPtr - 1), + $tokens[$stackPtr]['bracket_opener'] + ); + + if ($found !== false) { + return; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prev !== false && $tokens[$prev]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { + $num = ($tokens[$stackPtr]['line'] - $tokens[$prev]['line'] - 1); + $error = 'Expected 0 blank lines before closing brace of class definition; %s found'; + $data = [$num]; + $phpcsFile->addError($error, $stackPtr, 'SpacingBeforeClose', $data); + } + } +} diff --git a/Magento2/Sniffs/Less/ClassNamingSniff.php b/Magento2/Sniffs/Less/ClassNamingSniff.php new file mode 100644 index 00000000..90d44280 --- /dev/null +++ b/Magento2/Sniffs/Less/ClassNamingSniff.php @@ -0,0 +1,68 @@ +getTokens(); + + if (T_WHITESPACE !== $tokens[$stackPtr - 1]['code'] + && !in_array( + $tokens[$stackPtr - 1]['content'], + [ + TokenizerSymbolsInterface::INDENT_SPACES, + TokenizerSymbolsInterface::NEW_LINE, + ] + ) + ) { + return; + } + + $className = $tokens[$stackPtr + 1]['content']; + if (preg_match_all('/[^a-z0-9\-_]/U', $className, $matches)) { + $phpcsFile->addError('Class name contains not allowed symbols', $stackPtr, 'NotAllowedSymbol', $matches); + } + } +} diff --git a/Magento2/Sniffs/Less/ColonSpacingSniff.php b/Magento2/Sniffs/Less/ColonSpacingSniff.php new file mode 100644 index 00000000..6ada0fac --- /dev/null +++ b/Magento2/Sniffs/Less/ColonSpacingSniff.php @@ -0,0 +1,113 @@ +getTokens(); + + if ($this->needValidateSpaces($phpcsFile, $stackPtr, $tokens)) { + $this->validateSpaces($phpcsFile, $stackPtr, $tokens); + } + } + + /** + * Check is it need to check spaces + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * + * @return bool + */ + private function needValidateSpaces(File $phpcsFile, $stackPtr, $tokens) + { + $nextSemicolon = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); + + if (false === $nextSemicolon + || ($tokens[$nextSemicolon]['line'] !== $tokens[$stackPtr]['line']) + || TokenizerSymbolsInterface::BITWISE_AND === $tokens[$stackPtr - 1]['content'] + ) { + return false; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($tokens[$prev]['code'] !== T_STYLE) { + // The colon is not part of a style definition. + return false; + } + + if ($tokens[$prev]['content'] === 'progid') { + // Special case for IE filters. + return false; + } + + return true; + } + + /** + * Validate Colon Spacing according to requirements + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * + * @return void + */ + private function validateSpaces(File $phpcsFile, $stackPtr, array $tokens) + { + if (T_WHITESPACE === $tokens[($stackPtr - 1)]['code']) { + $phpcsFile->addError('There must be no space before a colon in a style definition', $stackPtr, 'Before'); + } + + if (T_WHITESPACE !== $tokens[($stackPtr + 1)]['code']) { + $phpcsFile->addError('Expected 1 space after colon in style definition; 0 found', $stackPtr, 'NoneAfter'); + } else { + $content = $tokens[($stackPtr + 1)]['content']; + if (false === strpos($content, $phpcsFile->eolChar)) { + $length = strlen($content); + if ($length !== 1) { + $error = 'Expected 1 space after colon in style definition; %s found'; + $phpcsFile->addError($error, $stackPtr, 'After'); + } + } else { + $error = 'Expected 1 space after colon in style definition; newline found'; + $phpcsFile->addError($error, $stackPtr, 'AfterNewline'); + } + } + } +} diff --git a/Magento2/Sniffs/Less/ColourDefinitionSniff.php b/Magento2/Sniffs/Less/ColourDefinitionSniff.php new file mode 100644 index 00000000..82568824 --- /dev/null +++ b/Magento2/Sniffs/Less/ColourDefinitionSniff.php @@ -0,0 +1,65 @@ +getTokens(); + $colour = $tokens[$stackPtr]['content']; + + $variablePtr = $phpcsFile->findPrevious(T_ASPERAND, $stackPtr); + if ((false === $variablePtr) || ($tokens[$stackPtr]['line'] !== $tokens[$variablePtr]['line'])) { + $phpcsFile->addError('Hexadecimal value should be used for variable', $stackPtr, 'NotInVariable'); + } + + $expected = strtolower($colour); + if ($colour !== $expected) { + $error = 'CSS colours must be defined in lowercase; expected %s but found %s'; + $phpcsFile->addError($error, $stackPtr, 'NotLower', [$expected, $colour]); + } + + // Now check if shorthand can be used. + if (strlen($colour) !== 7) { + return; + } + + if ($colour[1] === $colour[2] && $colour[3] === $colour[4] && $colour[5] === $colour[6]) { + $expected = '#' . $colour[1] . $colour[3] . $colour[5]; + $error = 'CSS colours must use shorthand if available; expected %s but found %s'; + $phpcsFile->addError($error, $stackPtr, 'Shorthand', [$expected, $colour]); + } + } +} diff --git a/Magento2/Sniffs/Less/CombinatorIndentationSniff.php b/Magento2/Sniffs/Less/CombinatorIndentationSniff.php new file mode 100644 index 00000000..e6954c31 --- /dev/null +++ b/Magento2/Sniffs/Less/CombinatorIndentationSniff.php @@ -0,0 +1,49 @@ +getTokens(); + + $prevPtr = $stackPtr - 1; + $nextPtr = $stackPtr + 1; + + if (($tokens[$prevPtr]['code'] !== T_WHITESPACE) || ($tokens[$nextPtr]['code'] !== T_WHITESPACE)) { + $phpcsFile->addError('Spaces should be before and after combinators', $stackPtr, 'NoSpaces'); + } + } +} diff --git a/Magento2/Sniffs/Less/CommentLevelsSniff.php b/Magento2/Sniffs/Less/CommentLevelsSniff.php new file mode 100644 index 00000000..e192f659 --- /dev/null +++ b/Magento2/Sniffs/Less/CommentLevelsSniff.php @@ -0,0 +1,214 @@ + T_STRING, + self::SECOND_LEVEL_COMMENT => T_DEC, + ]; + + /** + * A list of tokenizers this sniff supports. + * + * @var array + */ + public $supportedTokenizers = [TokenizerSymbolsInterface::TOKENIZER_CSS]; + + /** + * @inheritdoc + */ + public function register() + { + return [T_STRING]; + } + + /** + * @inheritdoc + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if ((T_STRING !== $tokens[$stackPtr]['code']) + || (self::COMMENT_STRING !== $tokens[$stackPtr]['content']) + || (1 === $tokens[$stackPtr]['line']) + ) { + return; + } + + $textInSameLine = $phpcsFile->findPrevious([T_STRING, T_STYLE], $stackPtr - 1); + + // is inline comment + if ((false !== $textInSameLine) + && ($tokens[$textInSameLine]['line'] === $tokens[$stackPtr]['line']) + ) { + $this->validateInlineComment($phpcsFile, $stackPtr, $tokens); + return; + } + + $this->validateCommentLevel($phpcsFile, $stackPtr, $tokens); + + if (!$this->isNthLevelComment($phpcsFile, $stackPtr, $tokens)) { + return; + } + + if (!$this->checkNthLevelComment($phpcsFile, $stackPtr, $tokens)) { + $phpcsFile->addError( + 'First and second level comments must be surrounded by empty lines', + $stackPtr, + 'SpaceMissed' + ); + } + } + + /** + * Validate that inline comment responds to given requirements + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @return bool + */ + private function validateInlineComment(File $phpcsFile, $stackPtr, array $tokens) + { + if ($tokens[$stackPtr + 1]['content'] !== TokenizerSymbolsInterface::WHITESPACE) { + $phpcsFile->addError('Inline comment should have 1 space after "//"', $stackPtr, 'SpaceMissedAfter'); + } + if ($tokens[$stackPtr - 1]['content'] !== TokenizerSymbolsInterface::WHITESPACE) { + $phpcsFile->addError('Inline comment should have 1 space before "//"', $stackPtr, 'SpaceMissedBefore'); + } + } + + /** + * Check is it n-th level comment was found + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @return bool + */ + private function isNthLevelComment(File $phpcsFile, $stackPtr, array $tokens) + { + $nthLevelCommentFound = false; + $levelComment = 0; + + foreach ($this->levelComments as $code => $comment) { + $levelComment = $phpcsFile->findNext($comment, $stackPtr, null, false, $code); + if (false !== $levelComment) { + $nthLevelCommentFound = true; + break; + } + } + + if (false === $nthLevelCommentFound) { + return false; + } + + $currentLine = $tokens[$stackPtr]['line']; + $levelCommentLine = $tokens[$levelComment]['line']; + + if ($currentLine !== $levelCommentLine) { + return false; + } + + return true; + } + + /** + * Check is it n-th level comment is correct + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @return bool + */ + private function checkNthLevelComment(File $phpcsFile, $stackPtr, array $tokens) + { + $correct = false; + + $nextLine = $phpcsFile->findNext( + T_WHITESPACE, + $stackPtr, + null, + false, + TokenizerSymbolsInterface::NEW_LINE + ); + + if (false === $nextLine) { + return $correct; + } + + if (($tokens[$nextLine]['content'] !== TokenizerSymbolsInterface::NEW_LINE) + || ($tokens[$nextLine + 1]['content'] !== TokenizerSymbolsInterface::NEW_LINE) + ) { + return $correct; + } + + $commentLinePtr = $stackPtr; + while ($tokens[$commentLinePtr - 2]['line'] > 1) { + $commentLinePtr = $phpcsFile->findPrevious(T_STRING, $commentLinePtr - 1, null, false, '//'); + + if (false === $commentLinePtr) { + continue; + } + + if (($tokens[$commentLinePtr - 1]['content'] === TokenizerSymbolsInterface::NEW_LINE) + && ($tokens[$commentLinePtr - 2]['content'] === TokenizerSymbolsInterface::NEW_LINE) + ) { + $correct = true; + break; + } + } + + return $correct; + } + + /** + * Validation of comment level. + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + */ + private function validateCommentLevel(File $phpcsFile, int $stackPtr, array $tokens): void + { + if ($tokens[$stackPtr + 2]['content'] !== 'magento_import' && + !in_array( + $tokens[$stackPtr + 1]['content'], + [ + TokenizerSymbolsInterface::DOUBLE_WHITESPACE, + TokenizerSymbolsInterface::NEW_LINE, + ], + true + ) + ) { + $phpcsFile->addError('Level\'s comment does not have 2 spaces after "//"', $stackPtr, 'SpacesMissed'); + } + } +} diff --git a/Magento2/Sniffs/Less/ImportantPropertySniff.php b/Magento2/Sniffs/Less/ImportantPropertySniff.php new file mode 100644 index 00000000..ad1fd10c --- /dev/null +++ b/Magento2/Sniffs/Less/ImportantPropertySniff.php @@ -0,0 +1,51 @@ +getTokens(); + + // Will be implemented in MAGETWO-49778 + //$phpcsFile->addWarning('!important is used', $stackPtr, '!ImportantIsUsed'); + + if (($tokens[$stackPtr + 1]['content'] === 'important') + && ($tokens[$stackPtr - 1]['content'] !== TokenizerSymbolsInterface::WHITESPACE) + ) { + $phpcsFile->addError('Space before !important is missing', $stackPtr, 'NoSpace'); + } + } +} diff --git a/Magento2/Sniffs/Less/IndentationSniff.php b/Magento2/Sniffs/Less/IndentationSniff.php new file mode 100644 index 00000000..76a6fbcc --- /dev/null +++ b/Magento2/Sniffs/Less/IndentationSniff.php @@ -0,0 +1,107 @@ +getTokens(); + + $numTokens = (count($tokens) - 2); + $indentLevel = 0; + for ($i = 1; $i < $numTokens; $i++) { + if ($tokens[$i]['code'] === T_COMMENT) { + // Don't check the indent of comments. + continue; + } + + if ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET) { + $indentLevel++; + } elseif ($tokens[($i + 1)]['code'] === T_CLOSE_CURLY_BRACKET) { + $indentLevel--; + } + + if ($tokens[$i]['column'] !== 1) { + continue; + } + + // We started a new line, so check indent. + if ($tokens[$i]['code'] === T_WHITESPACE) { + $content = str_replace($phpcsFile->eolChar, '', $tokens[$i]['content']); + $foundIndent = strlen($content); + } else { + $foundIndent = 0; + } + + $expectedIndent = ($indentLevel * $this->indent); + if (!($expectedIndent > 0 && strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) + && ($foundIndent !== $expectedIndent) + && (!in_array($tokens[$i + 1]['code'], $this->styleCodesToSkip)) + ) { + $error = 'Line indented incorrectly; expected %s spaces, found %s'; + $phpcsFile->addError($error, $i, 'Incorrect', [$expectedIndent, $foundIndent]); + } + + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + if ($indentLevel > $this->maxIndentLevel) { + // Will be implemented in MAGETWO-49778 + // $phpcsFile->addWarning('Avoid using more than three levels of nesting', $i, 'IncorrectNestingLevel'); + } + } + } +} diff --git a/Magento2/Sniffs/Less/PropertiesLineBreakSniff.php b/Magento2/Sniffs/Less/PropertiesLineBreakSniff.php new file mode 100644 index 00000000..06eb9c6a --- /dev/null +++ b/Magento2/Sniffs/Less/PropertiesLineBreakSniff.php @@ -0,0 +1,52 @@ +getTokens(); + + $prevPtr = $phpcsFile->findPrevious(T_SEMICOLON, ($stackPtr - 1)); + if (false === $prevPtr) { + return; + } + + if ($tokens[$prevPtr]['line'] === $tokens[$stackPtr]['line']) { + $error = 'Each property must be on a line by itself'; + $phpcsFile->addError($error, $stackPtr, 'SameLine'); + } + } +} diff --git a/Magento2/Sniffs/Less/PropertiesSortingSniff.php b/Magento2/Sniffs/Less/PropertiesSortingSniff.php new file mode 100644 index 00000000..d1c7c387 --- /dev/null +++ b/Magento2/Sniffs/Less/PropertiesSortingSniff.php @@ -0,0 +1,119 @@ +getTokens(); + $currentToken = $tokens[$stackPtr]; + + // if variables, mixins, extends area used - skip + if ((T_ASPERAND === $tokens[$stackPtr - 1]['code']) + || in_array($tokens[$stackPtr]['content'], $this->styleSymbolsToSkip) + ) { + return; + } + + $nextCurlyBracket = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr + 1); + if (in_array($currentToken['code'], [T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET]) + || ((false !== $nextCurlyBracket) && ($tokens[$nextCurlyBracket]['line'] === $tokens[$stackPtr]['line'])) + ) { + if ($this->properties) { + // validate collected properties before erase them + $this->validatePropertiesSorting($phpcsFile, $stackPtr, $this->properties); + } + + $this->properties = []; + return; + } + + if (T_STYLE === $currentToken['code']) { + $this->properties[] = $currentToken['content']; + } + } + + /** + * Validate sorting of properties of class + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $properties + * + * @return void + */ + private function validatePropertiesSorting(File $phpcsFile, $stackPtr, array $properties) + { + // Fix needed for cases when incorrect properties passed for validation due to bug in PHP tokens. + $symbolsForSkip = ['(', 'block', 'field']; + $properties = array_filter( + $properties, + function ($var) use ($symbolsForSkip) { + return !in_array($var, $symbolsForSkip); + } + ); + + $originalProperties = $properties; + sort($properties); + + if ($originalProperties !== $properties) { + $delimiter = $phpcsFile->findPrevious(T_SEMICOLON, $stackPtr); + $phpcsFile->addError('Properties sorted not alphabetically', $delimiter, 'PropertySorting'); + } + } +} diff --git a/Magento2/Sniffs/Less/QuotesSniff.php b/Magento2/Sniffs/Less/QuotesSniff.php new file mode 100644 index 00000000..a1328c51 --- /dev/null +++ b/Magento2/Sniffs/Less/QuotesSniff.php @@ -0,0 +1,46 @@ +getTokens(); + + if (false !== strpos($tokens[$stackPtr]['content'], '"')) { + $phpcsFile->addError('Use single quotes', $stackPtr, 'DoubleQuotes'); + } + } +} diff --git a/Magento2/Sniffs/Less/SelectorDelimiterSniff.php b/Magento2/Sniffs/Less/SelectorDelimiterSniff.php new file mode 100644 index 00000000..cdbd5117 --- /dev/null +++ b/Magento2/Sniffs/Less/SelectorDelimiterSniff.php @@ -0,0 +1,90 @@ +getTokens(); + + // Check that there's no spaces before delimiter + if ($tokens[$stackPtr - 1]['code'] === T_WHITESPACE) { + $phpcsFile->addError('Spaces should not be before delimiter', $stackPtr - 1, 'SpacesBeforeDelimiter'); + } + + $this->validateParenthesis($phpcsFile, $stackPtr, $tokens); + } + + /** + * Parenthesis validation. + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @return void + */ + private function validateParenthesis(File $phpcsFile, $stackPtr, array $tokens) + { + $nextPtr = $stackPtr + 1; + + $nextClassPtr = $phpcsFile->findNext(T_STRING_CONCAT, $nextPtr); + $nextOpenBrace = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $nextPtr); + + if ($nextClassPtr === false || $nextOpenBrace === false) { + return; + } + + $stackLine = $tokens[$stackPtr]['line']; + $nextClassLine = $tokens[$nextPtr]['line']; + $nextOpenBraceLine = $tokens[$nextOpenBrace]['line']; + + // Check that each class declaration goes from new line + if (($stackLine === $nextClassLine) && ($stackLine === $nextOpenBraceLine)) { + $prevParenthesis = $phpcsFile->findPrevious(T_OPEN_PARENTHESIS, $stackPtr); + $nextParenthesis = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr); + + if ((false !== $prevParenthesis) && (false !== $nextParenthesis) + && ($tokens[$prevParenthesis]['line'] === $tokens[$stackPtr]['line']) + && ($tokens[$nextParenthesis]['line'] === $tokens[$stackPtr]['line']) + ) { + return; + } + + $error = 'Add a line break after each selector delimiter'; + $phpcsFile->addError($error, $nextOpenBrace, 'LineBreakAfterDelimiter'); + } + } +} diff --git a/Magento2/Sniffs/Less/SemicolonSpacingSniff.php b/Magento2/Sniffs/Less/SemicolonSpacingSniff.php new file mode 100644 index 00000000..cad13290 --- /dev/null +++ b/Magento2/Sniffs/Less/SemicolonSpacingSniff.php @@ -0,0 +1,115 @@ +getTokens(); + + if (in_array($tokens[$stackPtr]['content'], $this->styleSymbolsToSkip)) { + return; + } + + $semicolonPtr = $phpcsFile->findNext(T_SEMICOLON, ($stackPtr + 1)); + if ($tokens[$semicolonPtr]['line'] !== $tokens[$stackPtr]['line']) { + $semicolonPtr = $phpcsFile->findNext(T_STYLE, ($stackPtr + 1), null, false, ";"); + } + + $this->validateSemicolon($phpcsFile, $stackPtr, $tokens, $semicolonPtr); + $this->validateSpaces($phpcsFile, $stackPtr, $tokens, $semicolonPtr); + } + + /** + * Semicolon validation. + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @param int $semicolonPtr + * @return void + */ + private function validateSemicolon(File $phpcsFile, $stackPtr, array $tokens, $semicolonPtr) + { + if ((false === $semicolonPtr || $tokens[$semicolonPtr]['line'] !== $tokens[$stackPtr]['line']) + && (isset($tokens[$stackPtr - 1]) && !in_array($tokens[$stackPtr - 1]['code'], $this->styleCodesToSkip)) + && (T_COLON !== $tokens[$stackPtr + 1]['code']) + ) { + $error = 'Style definitions must end with a semicolon'; + $phpcsFile->addError($error, $stackPtr, 'NotAtEnd'); + } + } + + /** + * Spaces validation. + * + * @param File $phpcsFile + * @param int $stackPtr + * @param array $tokens + * @param int $semicolonPtr + * @return void + */ + private function validateSpaces(File $phpcsFile, $stackPtr, array $tokens, $semicolonPtr) + { + if (!isset($tokens[($semicolonPtr - 1)])) { + return; + } + + if ($tokens[($semicolonPtr - 1)]['code'] === T_WHITESPACE) { + $length = strlen($tokens[($semicolonPtr - 1)]['content']); + $error = 'Expected 0 spaces before semicolon in style definition; %s found'; + $data = [$length]; + $phpcsFile->addError($error, $stackPtr, 'SpaceFound', $data); + } + } +} diff --git a/Magento2/Sniffs/Less/TokenizerSymbolsInterface.php b/Magento2/Sniffs/Less/TokenizerSymbolsInterface.php new file mode 100644 index 00000000..da690f79 --- /dev/null +++ b/Magento2/Sniffs/Less/TokenizerSymbolsInterface.php @@ -0,0 +1,27 @@ +getTokens(); + + if (0 === strpos($tokens[$stackPtr + 1]['content'], '-') + && in_array($tokens[$stackPtr - 1]['content'], $this->symbolsBeforeConcat) + ) { + $phpcsFile->addError('Concatenation is used', $stackPtr, 'ConcatenationUsage'); + } + } +} diff --git a/Magento2/Sniffs/Less/TypeSelectorsSniff.php b/Magento2/Sniffs/Less/TypeSelectorsSniff.php new file mode 100644 index 00000000..00c8b35c --- /dev/null +++ b/Magento2/Sniffs/Less/TypeSelectorsSniff.php @@ -0,0 +1,98 @@ +getTokens(); + + $bracketPtr = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr); + + if (false === $bracketPtr) { + return; + } + + $isBracketOnSameLane = (bool)($tokens[$bracketPtr]['line'] === $tokens[$stackPtr]['line']); + + if (!$isBracketOnSameLane) { + return; + } + + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + if ((T_STRING === $tokens[$stackPtr - 1]['code']) + && in_array($tokens[$stackPtr - 1]['content'], $this->tags) + ) { + // Will be implemented in MAGETWO-49778 + //$phpcsFile->addWarning('Type selector is used', $stackPtr, 'TypeSelector'); + } + + for ($i = $stackPtr; $i < $bracketPtr; $i++) { + if (preg_match('/[A-Z]/', $tokens[$i]['content'])) { + $phpcsFile->addError('Selector contains uppercase symbols', $stackPtr, 'UpperCaseSelector'); + } + } + } +} diff --git a/Magento2/Sniffs/Less/VariablesSniff.php b/Magento2/Sniffs/Less/VariablesSniff.php new file mode 100644 index 00000000..ec9986a6 --- /dev/null +++ b/Magento2/Sniffs/Less/VariablesSniff.php @@ -0,0 +1,80 @@ +getTokens(); + $currentToken = $tokens[$stackPtr]; + + $nextColon = $phpcsFile->findNext(T_COLON, $stackPtr); + $nextSemicolon = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); + if ((false === $nextColon) || (false === $nextSemicolon)) { + return; + } + + $isVariableDeclaration = ($currentToken['line'] === $tokens[$nextColon]['line']) + && ($currentToken['line'] === $tokens[$nextSemicolon]['line']) + && (T_STRING === $tokens[$stackPtr + 1]['code']) + && (T_COLON === $tokens[$stackPtr + 2]['code']); + + if (!$isVariableDeclaration) { + return; + } + + $classBefore = $phpcsFile->findPrevious(T_STYLE, $stackPtr); + if (false !== $classBefore) { + $phpcsFile->addError( + 'Variable declaration located not in the beginning of general comments', + $stackPtr, + 'VariableLocation' + ); + } + + $variableName = $tokens[$stackPtr + 1]['content']; + if (preg_match('/[A-Z]/', $variableName)) { + $phpcsFile->addError( + 'Variable declaration contains uppercase symbols', + $stackPtr, + 'VariableUppercase' + ); + } + } +} diff --git a/Magento2/Sniffs/Less/ZeroUnitsSniff.php b/Magento2/Sniffs/Less/ZeroUnitsSniff.php new file mode 100644 index 00000000..51971c78 --- /dev/null +++ b/Magento2/Sniffs/Less/ZeroUnitsSniff.php @@ -0,0 +1,78 @@ +getTokens(); + $tokenCode = $tokens[$stackPtr]['code']; + $tokenContent = $tokens[$stackPtr]['content']; + + $nextToken = $tokens[$stackPtr + 1]; + + if (T_LNUMBER === $tokenCode + && "0" === $tokenContent + && T_STRING === $nextToken['code'] + && in_array($nextToken['content'], $this->units) + ) { + $phpcsFile->addError('Units specified for "0" value', $stackPtr, 'ZeroUnitFound'); + } + + if ((T_DNUMBER === $tokenCode) + && 0 === strpos($tokenContent, "0") + && ((float)$tokenContent < 1) + ) { + $phpcsFile->addError('Values starts from "0"', $stackPtr, 'ZeroUnitFound'); + } + } +} diff --git a/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php b/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php index 19bcd586..dc921b17 100644 --- a/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php +++ b/Magento2/Tests/GraphQL/AbstractGraphQLSniffUnitTestCase.php @@ -13,6 +13,9 @@ */ abstract class AbstractGraphQLSniffUnitTestCase extends AbstractSniffUnitTest { + /** + * @inheritDoc + */ protected function setUp() { //let parent do its job diff --git a/Magento2/ruleset.xml b/Magento2/ruleset.xml index 0a0bed7b..f77859e9 100644 --- a/Magento2/ruleset.xml +++ b/Magento2/ruleset.xml @@ -580,4 +580,15 @@ 5 warning + + */_files/* + */Test/* + *Test.php + + + + + 0 + +