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
+
+