diff --git a/.travis.yml b/.travis.yml
index 58e89616..32f5042b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
language: php
php:
- - 5.5
- 5.6
- 7.0
- 7.1
diff --git a/Magento/Sniffs/Annotation/AnnotationFormatValidator.php b/Magento/Sniffs/Annotation/AnnotationFormatValidator.php
new file mode 100644
index 00000000..93679614
--- /dev/null
+++ b/Magento/Sniffs/Annotation/AnnotationFormatValidator.php
@@ -0,0 +1,318 @@
+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,
+ $shortPtr,
+ $commentEndPtr
+ ) {
+ $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->addWarning($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,
+ $shortPtr,
+ $commentEndPtr,
+ array $emptyTypeTokens
+ ) {
+ $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';
+ $phpcsFile->addWarning($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,
+ $shortPtr,
+ $stackPtr,
+ $commentEndPtr,
+ array $emptyTypeTokens
+ ) {
+ $tokens = $phpcsFile->getTokens();
+ if ($tokens[$shortPtr]['line'] !== $tokens[$stackPtr]['line'] + 1) {
+ $error = 'No blank lines are allowed before short description';
+ $phpcsFile->addWarning($error, $shortPtr, 'MethodAnnotation');
+ }
+ if (strtolower($tokens[$shortPtr]['content']) === '{@inheritdoc}') {
+ $error = 'If the @inheritdoc not inline it shouldn’t have braces';
+ $phpcsFile->addWarning($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->addWarning($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,
+ $shortPtrEnd,
+ $commentEndPtr,
+ array $emptyTypeTokens
+ ) {
+ $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->addWarning($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->addWarning($error, $longPtr, 'MethodAnnotation');
+ }
+ if (preg_match('/^\p{Ll}/u', $tokens[$longPtr]['content']) === 1) {
+ $error = 'Long description must start with a capital letter';
+ $phpcsFile->addWarning($error, $longPtr, 'MethodAnnotation');
+ }
+ }
+ }
+
+ /**
+ * Validates tags spacing format
+ *
+ * @param File $phpcsFile
+ * @param int $commentStartPtr
+ * @param array $emptyTypeTokens
+ */
+ public function validateTagsSpacingFormat(File $phpcsFile, $commentStartPtr, array $emptyTypeTokens)
+ {
+ $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->addWarning($error, $firstTagPtr, 'MethodAnnotation');
+ }
+ }
+ }
+
+ /**
+ * Validates tag grouping format
+ *
+ * @param File $phpcsFile
+ * @param int $commentStartPtr
+ */
+ public function validateTagGroupingFormat(File $phpcsFile, $commentStartPtr)
+ {
+ $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->addWarning($error, $tag, 'MethodAnnotation');
+ }
+ if ($paramGroupId === null) {
+ $paramGroupId = $groupId;
+ }
+ }
+ $tagGroups[$groupId][] = $tag;
+ }
+ }
+
+ /**
+ * Validates extra newline before short description
+ *
+ * @param File $phpcsFile
+ * @param int $commentStartPtr
+ * @param int $commentEndPtr
+ * @param array $emptyTypeTokens
+ */
+ private function validateNoExtraNewLineBeforeShortDescription(
+ File $phpcsFile,
+ $commentStartPtr,
+ $commentEndPtr,
+ array $emptyTypeTokens
+ ) {
+ $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->addWarning($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,
+ $commentStartPtr,
+ $shortPtr,
+ $commentEndPtr,
+ array $emptyTypeTokens
+ ) {
+ $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->addWarning($error, $commentStartPtr, 'MethodAnnotation');
+ } else {
+ $this->validateShortDescriptionFormat(
+ $phpcsFile,
+ (int)$shortPtr,
+ $commentStartPtr,
+ $commentEndPtr,
+ $emptyTypeTokens
+ );
+ }
+ } else {
+ $this->validateShortDescriptionFormat(
+ $phpcsFile,
+ (int)$shortPtr,
+ $commentStartPtr,
+ $commentEndPtr,
+ $emptyTypeTokens
+ );
+ }
+ }
+}
diff --git a/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php b/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php
new file mode 100644
index 00000000..9e82bc91
--- /dev/null
+++ b/Magento/Sniffs/Annotation/ClassAnnotationStructureSniff.php
@@ -0,0 +1,123 @@
+annotationFormatValidator = new AnnotationFormatValidator();
+ }
+
+ /**
+ * Validates whether annotation block exists for interface, abstract or final classes
+ *
+ * @param File $phpcsFile
+ * @param int $previousCommentClosePtr
+ * @param int $stackPtr
+ */
+ private function validateInterfaceOrAbstractOrFinalClassAnnotationBlockExists(
+ File $phpcsFile,
+ $previousCommentClosePtr,
+ $stackPtr
+ ) {
+ $tokens = $phpcsFile->getTokens();
+ if ($tokens[$stackPtr]['type'] === 'T_CLASS') {
+ if ($tokens[$stackPtr - 2]['type'] === 'T_ABSTRACT' &&
+ $tokens[$stackPtr - 4]['content'] != $tokens[$previousCommentClosePtr]['content']
+ ) {
+ $error = 'Interface or abstract class is missing annotation block';
+ $phpcsFile->addWarning($error, $stackPtr, 'ClassAnnotation');
+ }
+ if ($tokens[$stackPtr - 2]['type'] === 'T_FINAL' &&
+ $tokens[$stackPtr - 4]['content'] != $tokens[$previousCommentClosePtr]['content']
+ ) {
+ $error = 'Final class is missing annotation block';
+ $phpcsFile->addWarning($error, $stackPtr, 'ClassAnnotation');
+ }
+ }
+ }
+
+ /**
+ * Validates whether annotation block exists
+ *
+ * @param File $phpcsFile
+ * @param int $previousCommentClosePtr
+ * @param int $stackPtr
+ */
+ private function validateAnnotationBlockExists(File $phpcsFile, $previousCommentClosePtr, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+ $this->validateInterfaceOrAbstractOrFinalClassAnnotationBlockExists(
+ $phpcsFile,
+ $previousCommentClosePtr,
+ $stackPtr
+ );
+ if ($tokens[$stackPtr - 2]['content'] != 'class' && $tokens[$stackPtr - 2]['content'] != 'abstract'
+ && $tokens[$stackPtr - 2]['content'] != 'final'
+ && $tokens[$stackPtr - 2]['content'] !== $tokens[$previousCommentClosePtr]['content']
+ ) {
+ $error = 'Class is missing annotation block';
+ $phpcsFile->addWarning($error, $stackPtr, 'ClassAnnotation');
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+ $previousCommentClosePtr = $phpcsFile->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $stackPtr - 1, 0);
+ $this->validateAnnotationBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr);
+ $commentStartPtr = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $stackPtr - 1, 0);
+ if ($commentStartPtr === false) {
+ return;
+ }
+ $commentCloserPtr = $tokens[$commentStartPtr]['comment_closer'];
+ $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->addWarning($error, $commentStartPtr, 'MethodAnnotation');
+ } else {
+ $this->annotationFormatValidator->validateDescriptionFormatStructure(
+ $phpcsFile,
+ $commentStartPtr,
+ (int) $shortPtr,
+ $previousCommentClosePtr,
+ $emptyTypeTokens
+ );
+ }
+ }
+}
diff --git a/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php b/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php
new file mode 100644
index 00000000..b4c51f63
--- /dev/null
+++ b/Magento/Sniffs/Annotation/MethodAnnotationStructureSniff.php
@@ -0,0 +1,79 @@
+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);
+ $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->addWarning($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);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Annotation/MethodArgumentsSniff.php b/Magento/Sniffs/Annotation/MethodArgumentsSniff.php
new file mode 100644
index 00000000..b0b03a38
--- /dev/null
+++ b/Magento/Sniffs/Annotation/MethodArgumentsSniff.php
@@ -0,0 +1,584 @@
+validTokensBeforeClosingCommentTag);
+ }
+
+ /**
+ * Validates whether comment block exists
+ *
+ * @param File $phpcsFile
+ * @param int $previousCommentClosePtr
+ * @param int $stackPtr
+ * @return bool
+ */
+ private function validateCommentBlockExists(File $phpcsFile, $previousCommentClosePtr, $stackPtr)
+ {
+ $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($type)
+ {
+ 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, $openParenthesisPtr, $closedParenthesisPtr)
+ {
+ $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)
+ {
+ $paramName = [];
+ for ($i = 0; $i < count($paramDefinitions); $i++) {
+ if (isset($paramDefinitions[$i]['paramName'])) {
+ $paramName[] = $paramDefinitions[$i]['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,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ ) {
+ 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,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ ) {
+ 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,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr,
+ $inheritdocAnnotation
+ ) {
+ $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,
+ $argumentsCount,
+ $parametersCount,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr,
+ $stackPtr
+ ) {
+ if ($argumentsCount > 0 && $parametersCount === 0) {
+ $inheritdocAnnotationWithoutBracesExists = $this->validateInheritdocAnnotationWithoutBracesExists(
+ $phpcsFile,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ );
+ $inheritdocAnnotationWithBracesExists = $this->validateInheritdocAnnotationWithBracesExists(
+ $phpcsFile,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ );
+ if ($inheritdocAnnotationWithBracesExists) {
+ $phpcsFile->addWarning(
+ '{@inheritdoc} does not import parameter annotation',
+ $stackPtr,
+ 'MethodArguments'
+ );
+ } elseif ($this->validateCommentBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr)
+ && !$inheritdocAnnotationWithoutBracesExists
+ ) {
+ $phpcsFile->addWarning(
+ '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,
+ $argumentsCount,
+ $parametersCount,
+ $stackPtr
+ ) {
+ if ($argumentsCount < $parametersCount && $argumentsCount > 0) {
+ $phpcsFile->addWarning(
+ 'Extra @param found in method annotation',
+ $stackPtr,
+ 'MethodArguments'
+ );
+ } elseif ($argumentsCount > 0 && $argumentsCount != $parametersCount && $parametersCount != 0) {
+ $phpcsFile->addWarning(
+ '@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(
+ $stackPtr,
+ $ptr,
+ File $phpcsFile,
+ array $methodArguments,
+ array $paramDefinitions
+ ) {
+ $parameterNames = $this->getMethodParameters($paramDefinitions);
+ if (!in_array($methodArguments[$ptr], $parameterNames)) {
+ $error = $methodArguments[$ptr]. ' parameter is missing in method annotation';
+ $phpcsFile->addWarning($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(
+ $ptr,
+ $paramDefinitionsArguments,
+ array $methodArguments,
+ File $phpcsFile,
+ array $paramPointers
+ ) {
+ if (!in_array($paramDefinitionsArguments, $methodArguments)) {
+ $phpcsFile->addWarning(
+ $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
+ ) {
+ $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->addWarning(
+ $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(
+ $stackPtr,
+ array $paramDefinitions,
+ array $paramPointers,
+ File $phpcsFile,
+ array $methodArguments
+ ) {
+ $argumentsCount = count($methodArguments);
+ $parametersCount = count($paramPointers);
+ if ($argumentsCount <= $parametersCount && $argumentsCount > 0) {
+ $duplicateParameters = [];
+ for ($i = 0; $i < sizeof($paramDefinitions); $i++) {
+ if (isset($paramDefinitions[$i]['paramName'])) {
+ $parameterContent = $paramDefinitions[$i]['paramName'];
+ for ($j = $i + 1; $j < count($paramDefinitions); $j++) {
+ if (isset($paramDefinitions[$j]['paramName'])
+ && $parameterContent === $paramDefinitions[$j]['paramName']
+ ) {
+ $duplicateParameters[] = $parameterContent;
+ }
+ }
+ }
+ }
+ foreach ($duplicateParameters as $value) {
+ $phpcsFile->addWarning(
+ $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(
+ $ptr,
+ File $phpcsFile,
+ array $methodArguments,
+ array $paramDefinitions,
+ array $paramPointers
+ ) {
+ switch (count($paramDefinitions)) {
+ case 0:
+ $phpcsFile->addWarning(
+ 'Missing both type and parameter',
+ $paramPointers[$ptr],
+ 'MethodArguments'
+ );
+ break;
+ case 1:
+ if (preg_match('/^\$.*/', $paramDefinitions[0])) {
+ $phpcsFile->addWarning(
+ 'Type is not specified',
+ $paramPointers[$ptr],
+ 'MethodArguments'
+ );
+ }
+ break;
+ case 2:
+ if ($this->isInvalidType($paramDefinitions[0])) {
+ $phpcsFile->addWarning(
+ $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->addWarning(
+ 'Type is not specified',
+ $paramPointers[$ptr],
+ 'MethodArguments'
+ );
+ if ($this->isInvalidType($paramDefinitions[0])) {
+ $phpcsFile->addWarning(
+ $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
+ */
+ private function validateMethodParameterAnnotations(
+ $stackPtr,
+ array $paramDefinitions,
+ array $paramPointers,
+ File $phpcsFile,
+ array $methodArguments,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ ) {
+ $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
+ );
+ for ($ptr = 0; $ptr < count($methodArguments); $ptr++) {
+ $tokens = $phpcsFile->getTokens();
+ 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 (!$this->validateCommentBlockExists($phpcsFile, $previousCommentClosePtr, $stackPtr)) {
+ $phpcsFile->addWarning('Comment block is missing', $stackPtr, 'MethodArguments');
+ 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;
+ $paramAnnotationParts = explode(' ', $tokens[$tempPtr+2]['content']);
+ if (count($paramAnnotationParts) === 1) {
+ if ((preg_match('/^\$.*/', $paramAnnotationParts[0]))) {
+ $paramDefinitions[] = [
+ 'type' => null,
+ 'paramName' => rtrim(ltrim($tokens[$tempPtr+2]['content'], '&'), ',')
+ ];
+ } else {
+ $paramDefinitions[] = [
+ 'type' => $tokens[$tempPtr+2]['content'],
+ 'paramName' => null
+ ];
+ }
+ } else {
+ $paramDefinitions[] = [
+ 'type' => $paramAnnotationParts[0],
+ 'paramName' => rtrim(ltrim($paramAnnotationParts[1], '&'), ',')
+ ];
+ }
+ }
+ }
+ $this->validateMethodParameterAnnotations(
+ $stackPtr,
+ $paramDefinitions,
+ $paramPointers,
+ $phpcsFile,
+ $methodArguments,
+ $previousCommentOpenPtr,
+ $previousCommentClosePtr
+ );
+ }
+}
diff --git a/Magento/Sniffs/Classes/ObjectManagerSniff.php b/Magento/Sniffs/Classes/ObjectManagerSniff.php
new file mode 100644
index 00000000..e2c41ae6
--- /dev/null
+++ b/Magento/Sniffs/Classes/ObjectManagerSniff.php
@@ -0,0 +1,85 @@
+getTokens();
+ $methodPosition = $phpcsFile->findNext(T_STRING, $stackPtr + 1);
+ if ($methodPosition !== false &&
+ in_array($tokens[$methodPosition]['content'], $this->objectManagerMethods)
+ ) {
+ $objectManagerPosition = $phpcsFile->findPrevious([T_STRING, T_VARIABLE], $stackPtr - 1);
+ if ($objectManagerPosition !== false) {
+ $objectManagerName = strtolower($tokens[$objectManagerPosition]['content']);
+ if ($tokens[$objectManagerPosition]['code'] === T_VARIABLE) {
+ $objectManagerName = substr($objectManagerName, 1);
+ }
+ if (in_array($objectManagerName, $this->objectManagerNames)) {
+ $phpcsFile->addWarning($this->warningMessage, $stackPtr, $this->warningCode);
+ }
+ }
+ }
+ }
+}
diff --git a/Magento/Sniffs/Exceptions/NamespaceSniff.php b/Magento/Sniffs/Exceptions/NamespaceSniff.php
index 8c2a244d..5d0e14eb 100644
--- a/Magento/Sniffs/Exceptions/NamespaceSniff.php
+++ b/Magento/Sniffs/Exceptions/NamespaceSniff.php
@@ -52,7 +52,7 @@ public function process(File $phpcsFile, $stackPtr)
$exceptionClassName = trim($tokens[$posOfExceptionClassName]['content']);
$posOfClassInUse = $phpcsFile->findNext(T_STRING, 0, $stackPtr, false, $exceptionClassName);
if ($posOfClassInUse === false || $tokens[$posOfClassInUse]['level'] != 0) {
- $phpcsFile->addError(
+ $phpcsFile->addWarning(
$this->errorMessage,
$stackPtr,
$this->errorCode,
diff --git a/Magento/Sniffs/Files/LineLengthSniff.php b/Magento/Sniffs/Files/LineLengthSniff.php
new file mode 100644
index 00000000..528baeed
--- /dev/null
+++ b/Magento/Sniffs/Files/LineLengthSniff.php
@@ -0,0 +1,36 @@
+previousLineContent) !== 0;
+ $this->previousLineContent = $lineContent;
+ if (! $currentLineMatch && !$previousLineMatch) {
+ parent::checkLineLength($phpcsFile, $stackPtr, $lineContent);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/AvoidIdSniff.php b/Magento/Sniffs/Less/AvoidIdSniff.php
new file mode 100644
index 00000000..ae0c7e23
--- /dev/null
+++ b/Magento/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->addWarning('Id selector is used', $stackPtr, 'IdSelectorUsage');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/BracesFormattingSniff.php b/Magento/Sniffs/Less/BracesFormattingSniff.php
new file mode 100644
index 00000000..f56fe3e3
--- /dev/null
+++ b/Magento/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->addWarning('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->addWarning($error, $stackPtr, 'SpacingBeforeClose', $data);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/ClassNamingSniff.php b/Magento/Sniffs/Less/ClassNamingSniff.php
new file mode 100644
index 00000000..64995712
--- /dev/null
+++ b/Magento/Sniffs/Less/ClassNamingSniff.php
@@ -0,0 +1,65 @@
+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->addWarning('Class name contains not allowed symbols', $stackPtr, 'NotAllowedSymbol', $matches);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/ColonSpacingSniff.php b/Magento/Sniffs/Less/ColonSpacingSniff.php
new file mode 100644
index 00000000..82c8b5ad
--- /dev/null
+++ b/Magento/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->addWarning('There must be no space before a colon in a style definition', $stackPtr, 'Before');
+ }
+
+ if (T_WHITESPACE !== $tokens[($stackPtr + 1)]['code']) {
+ $phpcsFile->addWarning('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->addWarning($error, $stackPtr, 'After');
+ }
+ } else {
+ $error = 'Expected 1 space after colon in style definition; newline found';
+ $phpcsFile->addWarning($error, $stackPtr, 'AfterNewline');
+ }
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/ColourDefinitionSniff.php b/Magento/Sniffs/Less/ColourDefinitionSniff.php
new file mode 100644
index 00000000..80e4642a
--- /dev/null
+++ b/Magento/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->addWarning('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->addWarning($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->addWarning($error, $stackPtr, 'Shorthand', [$expected, $colour]);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/CombinatorIndentationSniff.php b/Magento/Sniffs/Less/CombinatorIndentationSniff.php
new file mode 100644
index 00000000..e7e87122
--- /dev/null
+++ b/Magento/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->addWarning('Spaces should be before and after combinators', $stackPtr, 'NoSpaces');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/CommentLevelsSniff.php b/Magento/Sniffs/Less/CommentLevelsSniff.php
new file mode 100644
index 00000000..660acfc7
--- /dev/null
+++ b/Magento/Sniffs/Less/CommentLevelsSniff.php
@@ -0,0 +1,198 @@
+ 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;
+ }
+
+ // validation of levels comments
+ if (!in_array($tokens[$stackPtr + 1]['content'], [
+ TokenizerSymbolsInterface::DOUBLE_WHITESPACE,
+ TokenizerSymbolsInterface::NEW_LINE,
+ ])
+ ) {
+ $phpcsFile->addWarning('Level\'s comment does not have 2 spaces after "//"', $stackPtr, 'SpacesMissed');
+ }
+
+ if (!$this->isNthLevelComment($phpcsFile, $stackPtr, $tokens)) {
+ return;
+ }
+
+ if (!$this->checkNthLevelComment($phpcsFile, $stackPtr, $tokens)) {
+ $phpcsFile->addWarning(
+ '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->addWarning('Inline comment should have 1 space after "//"', $stackPtr, 'SpaceMissedAfter');
+ }
+ if ($tokens[$stackPtr - 1]['content'] !== TokenizerSymbolsInterface::WHITESPACE) {
+ $phpcsFile->addWarning('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;
+ }
+}
diff --git a/Magento/Sniffs/Less/ImportantPropertySniff.php b/Magento/Sniffs/Less/ImportantPropertySniff.php
new file mode 100644
index 00000000..c097bae1
--- /dev/null
+++ b/Magento/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->addWarning('Space before !important is missing', $stackPtr, 'NoSpace');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/IndentationSniff.php b/Magento/Sniffs/Less/IndentationSniff.php
new file mode 100644
index 00000000..aeaaf89a
--- /dev/null
+++ b/Magento/Sniffs/Less/IndentationSniff.php
@@ -0,0 +1,106 @@
+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->addWarning($error, $i, 'Incorrect', [$expectedIndent, $foundIndent]);
+ }
+
+ 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/Magento/Sniffs/Less/PropertiesLineBreakSniff.php b/Magento/Sniffs/Less/PropertiesLineBreakSniff.php
new file mode 100644
index 00000000..23c130b5
--- /dev/null
+++ b/Magento/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->addWarning($error, $stackPtr, 'SameLine');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/PropertiesSortingSniff.php b/Magento/Sniffs/Less/PropertiesSortingSniff.php
new file mode 100644
index 00000000..4ad04444
--- /dev/null
+++ b/Magento/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->addWarning('Properties sorted not alphabetically', $delimiter, 'PropertySorting');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/QuotesSniff.php b/Magento/Sniffs/Less/QuotesSniff.php
new file mode 100644
index 00000000..fb5bb1d8
--- /dev/null
+++ b/Magento/Sniffs/Less/QuotesSniff.php
@@ -0,0 +1,46 @@
+getTokens();
+
+ if (false !== strpos($tokens[$stackPtr]['content'], '"')) {
+ $phpcsFile->addWarning('Use single quotes', $stackPtr, 'DoubleQuotes');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/SelectorDelimiterSniff.php b/Magento/Sniffs/Less/SelectorDelimiterSniff.php
new file mode 100644
index 00000000..838f00d6
--- /dev/null
+++ b/Magento/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->addWarning('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->addWarning($error, $nextOpenBrace, 'LineBreakAfterDelimiter');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/SemicolonSpacingSniff.php b/Magento/Sniffs/Less/SemicolonSpacingSniff.php
new file mode 100644
index 00000000..12bb3e7c
--- /dev/null
+++ b/Magento/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->addWarning($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->addWarning($error, $stackPtr, 'SpaceFound', $data);
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/TokenizerSymbolsInterface.php b/Magento/Sniffs/Less/TokenizerSymbolsInterface.php
new file mode 100644
index 00000000..db6a2fe1
--- /dev/null
+++ b/Magento/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->addWarning('Concatenation is used', $stackPtr, 'ConcatenationUsage');
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/TypeSelectorsSniff.php b/Magento/Sniffs/Less/TypeSelectorsSniff.php
new file mode 100644
index 00000000..5d2823f8
--- /dev/null
+++ b/Magento/Sniffs/Less/TypeSelectorsSniff.php
@@ -0,0 +1,97 @@
+getTokens();
+
+ $bracketPtr = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, $stackPtr);
+
+ if (false === $bracketPtr) {
+ return;
+ }
+
+ $isBracketOnSameLane = (bool)($tokens[$bracketPtr]['line'] === $tokens[$stackPtr]['line']);
+
+ if (!$isBracketOnSameLane) {
+ return;
+ }
+
+ 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->addWarning('Selector contains uppercase symbols', $stackPtr, 'UpperCaseSelector');
+ }
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/VariablesSniff.php b/Magento/Sniffs/Less/VariablesSniff.php
new file mode 100644
index 00000000..3663254f
--- /dev/null
+++ b/Magento/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->addWarning(
+ 'Variable declaration located not in the beginning of general comments',
+ $stackPtr,
+ 'VariableLocation'
+ );
+ }
+
+ $variableName = $tokens[$stackPtr + 1]['content'];
+ if (preg_match('/[A-Z]/', $variableName)) {
+ $phpcsFile->addWarning(
+ 'Variable declaration contains uppercase symbols',
+ $stackPtr,
+ 'VariableUppercase'
+ );
+ }
+ }
+}
diff --git a/Magento/Sniffs/Less/ZeroUnitsSniff.php b/Magento/Sniffs/Less/ZeroUnitsSniff.php
new file mode 100644
index 00000000..6ab2fc6c
--- /dev/null
+++ b/Magento/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->addWarning('Units specified for "0" value', $stackPtr, 'ZeroUnitFound');
+ }
+
+ if ((T_DNUMBER === $tokenCode)
+ && 0 === strpos($tokenContent, "0")
+ && ((float)$tokenContent < 1)
+ ) {
+ $phpcsFile->addWarning('Values starts from "0"', $stackPtr, 'ZeroUnitFound');
+ }
+ }
+}
diff --git a/Magento/Sniffs/NamingConvention/ReservedWordsSniff.php b/Magento/Sniffs/NamingConvention/ReservedWordsSniff.php
new file mode 100644
index 00000000..69c9c793
--- /dev/null
+++ b/Magento/Sniffs/NamingConvention/ReservedWordsSniff.php
@@ -0,0 +1,116 @@
+ '7',
+ 'float' => '7',
+ 'bool' => '7',
+ 'string' => '7',
+ 'true' => '7',
+ 'false' => '7',
+ 'null' => '7',
+ 'void' => '7.1',
+ 'iterable' => '7.1',
+ 'resource' => '7',
+ 'object' => '7',
+ 'mixed' => '7',
+ 'numeric' => '7',
+ ];
+
+ /**
+ * @inheritdoc
+ */
+ public function register()
+ {
+ return [T_CLASS, T_INTERFACE, T_TRAIT, T_NAMESPACE];
+ }
+
+ /**
+ * Check all namespace parts
+ *
+ * @param File $sourceFile
+ * @param int $stackPtr
+ * @return void
+ */
+ protected function validateNamespace(File $sourceFile, $stackPtr)
+ {
+ $stackPtr += 2;
+ $tokens = $sourceFile->getTokens();
+ while ($stackPtr < $sourceFile->numTokens && $tokens[$stackPtr]['code'] !== T_SEMICOLON) {
+ if ($tokens[$stackPtr]['code'] === T_WHITESPACE || $tokens[$stackPtr]['code'] === T_NS_SEPARATOR) {
+ $stackPtr++; //skip "namespace" and whitespace
+ continue;
+ }
+ $namespacePart = $tokens[$stackPtr]['content'];
+ if (isset($this->reservedWords[strtolower($namespacePart)])) {
+ $sourceFile->addError(
+ 'Cannot use "%s" in namespace as it is reserved since PHP %s',
+ $stackPtr,
+ 'Namespace',
+ [$namespacePart, $this->reservedWords[strtolower($namespacePart)]]
+ );
+ }
+ $stackPtr++;
+ }
+ }
+
+ /**
+ * Check class name not having reserved words
+ *
+ * @param File $sourceFile
+ * @param int $stackPtr
+ * @return void
+ */
+ protected function validateClass(File $sourceFile, $stackPtr)
+ {
+ $tokens = $sourceFile->getTokens();
+ $stackPtr += 2; //skip "class" and whitespace
+ $className = strtolower($tokens[$stackPtr]['content']);
+ if (isset($this->reservedWords[$className])) {
+ $sourceFile->addError(
+ 'Cannot use "%s" as class name as it is reserved since PHP %s',
+ $stackPtr,
+ 'Class',
+ [$className, $this->reservedWords[$className]]
+ );
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function process(File $sourceFile, $stackPtr)
+ {
+ $tokens = $sourceFile->getTokens();
+ switch ($tokens[$stackPtr]['code']) {
+ case T_CLASS:
+ case T_INTERFACE:
+ case T_TRAIT:
+ $this->validateClass($sourceFile, $stackPtr);
+ break;
+ case T_NAMESPACE:
+ $this->validateNamespace($sourceFile, $stackPtr);
+ break;
+ }
+ }
+}
diff --git a/Magento/Sniffs/PHP/DiscouragedFunctionSniff.php b/Magento/Sniffs/PHP/DiscouragedFunctionSniff.php
index 8a0fbe6e..7cf74e84 100644
--- a/Magento/Sniffs/PHP/DiscouragedFunctionSniff.php
+++ b/Magento/Sniffs/PHP/DiscouragedFunctionSniff.php
@@ -176,6 +176,7 @@ class DiscouragedFunctionSniff extends ForbiddenFunctionsSniff
'^ngettext$' => null,
'^ob_get_contents$' => null,
'^ob_get_flush$' => null,
+ '^ob_start$' => null,
'^rawurldecode$' => null,
'^shm_get_var$' => null,
'^stripcslashes$' => null,
diff --git a/Magento/Sniffs/PHP/LiteralNamespacesSniff.php b/Magento/Sniffs/PHP/LiteralNamespacesSniff.php
new file mode 100644
index 00000000..c1e83b96
--- /dev/null
+++ b/Magento/Sniffs/PHP/LiteralNamespacesSniff.php
@@ -0,0 +1,75 @@
+getTokens();
+ if ($sourceFile->findPrevious(T_STRING_CONCAT, $stackPtr, $stackPtr - 3) ||
+ $sourceFile->findNext(T_STRING_CONCAT, $stackPtr, $stackPtr + 3)
+ ) {
+ return;
+ }
+
+ $content = trim($tokens[$stackPtr]['content'], "\"'");
+ // replace double slashes from class name for avoiding problems with class autoload
+ if (strpos($content, '\\') !== false) {
+ $content = preg_replace('|\\\{2,}|', '\\', $content);
+ }
+
+ if (preg_match($this->literalNamespacePattern, $content) === 1 && $this->classExists($content)) {
+ $sourceFile->addWarning(
+ "Use ::class notation instead.",
+ $stackPtr,
+ 'LiteralClassUsage'
+ );
+ }
+ }
+
+ /**
+ * @param string $className
+ * @return bool
+ */
+ private function classExists($className)
+ {
+ if (!isset($this->classNames[$className])) {
+ $this->classNames[$className] = class_exists($className) || interface_exists($className);
+ }
+ return $this->classNames[$className];
+ }
+}
diff --git a/Magento/Sniffs/PHP/ShortEchoSyntaxSniff.php b/Magento/Sniffs/PHP/ShortEchoSyntaxSniff.php
new file mode 100644
index 00000000..7cbc87e8
--- /dev/null
+++ b/Magento/Sniffs/PHP/ShortEchoSyntaxSniff.php
@@ -0,0 +1,47 @@
+getTokens();
+ $openTag = $tokens[$stackPtr];
+
+ // HHVM Will classify = as a T_OPEN_TAG
+ if ($openTag['content'] === '=') {
+ return;
+ }
+
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($tokens[$nextToken]['code'] == T_ECHO) {
+ $phpcsFile->addWarning(
+ 'Short echo tag syntax must be used; expected "=" but found "getTokens();
- $doubleColon = $tokens[$stackPtr];
- $currentLine = $doubleColon['line'];
- $lineContent = $this->getLineContent($phpcsFile, $currentLine);
- $previousLineContent = $this->getLineContent($phpcsFile, $currentLine - 1);
- $previousLineRegexp = '~__\($|Phrase\($~';
- $currentLineRegexp = '~__\(.+\)|Phrase\(.+\)~';
- $currentLineMatch = preg_match($currentLineRegexp, $lineContent) !== 0;
- $previousLineMatch = preg_match($previousLineRegexp, $previousLineContent) !== 0;
- $this->previousLineContent = $lineContent;
- $constantRegexp = '[^\'"]+::[A-Z_0-9]+.*';
- if ($currentLineMatch) {
- $variableRegexp = "~__\({$constantRegexp}\)|Phrase\({$constantRegexp}\)~";
- if (preg_match($variableRegexp, $lineContent) !== 0) {
- $phpcsFile->addWarning(
- $this->warningMessage,
- $this->getFirstLineToken($phpcsFile, $currentLine),
- $this->warningCode
- );
- }
- } else {
- if ($previousLineMatch) {
- $variableRegexp = "~^\s+{$constantRegexp}~";
- if (preg_match($variableRegexp, $lineContent) !== 0) {
- $phpcsFile->addWarning(
- $this->warningMessage,
- $this->getFirstLineToken($phpcsFile, $currentLine - 1),
- $this->warningCode
- );
- }
+
+ // Make sure this is the first open tag
+ $previousOpenTag = $phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1));
+ if ($previousOpenTag !== false) {
+ return;
+ }
+
+ $tokenCount = 0;
+ $currentLineContent = '';
+ $currentLine = 1;
+
+ for (; $tokenCount < $phpcsFile->numTokens; $tokenCount++) {
+ if ($tokens[$tokenCount]['line'] === $currentLine) {
+ $currentLineContent .= $tokens[$tokenCount]['content'];
+ } else {
+ $this->checkIfFirstArgumentConstant($phpcsFile, ($tokenCount - 1), $currentLineContent);
+ $currentLineContent = $tokens[$tokenCount]['content'];
+ $currentLine++;
}
}
- }
- /**
- * Get line content by it's line number.
- *
- * @param File $phpcsFile
- * @param int $line
- * @return string
- */
- private function getLineContent(File $phpcsFile, $line)
- {
- $tokens = $phpcsFile->getTokens();
- return implode('', array_column(array_filter($tokens, function ($item) use ($line) {
- return $item['line'] == $line;
- }), 'content'));
+ $this->checkIfFirstArgumentConstant($phpcsFile, ($tokenCount - 1), $currentLineContent);
}
/**
- * Get index of first token in line.
+ * Checks if first argument of \Magento\Framework\Phrase or translation function is a constant
*
* @param File $phpcsFile
- * @param int $line
- * @return int
+ * @param int $stackPtr
+ * @param string $lineContent
+ * @return void
*/
- private function getFirstLineToken(File $phpcsFile, $line)
- {
- $tokens = $phpcsFile->getTokens();
- return array_keys(array_filter($tokens, function ($item) use ($line) {
- return $item['line'] == $line;
- }))[0];
+ private function checkIfFirstArgumentConstant(
+ File $phpcsFile,
+ $stackPtr,
+ $lineContent
+ ) {
+ $previousLineRegexp = '/(__|Phrase)\($/im';
+ $currentLineRegexp = '/(__|Phrase)\(.+\)/';
+ $currentLineMatch = preg_match($currentLineRegexp, $lineContent) !== 0;
+ $previousLineMatch = preg_match($previousLineRegexp, $this->previousLineContent) !== 0;
+ $this->previousLineContent = $lineContent;
+ $error = 'Constants are not allowed as the first argument of translation function, use string literal instead';
+ $constantRegexp = '[^\$\'"]+::[A-Z_0-9]+.*';
+ if ($currentLineMatch) {
+ $variableRegexp = "/(__|Phrase)\({$constantRegexp}\)/";
+ if (preg_match($variableRegexp, $lineContent) !== 0) {
+ $phpcsFile->addWarning($error, $stackPtr, 'VariableTranslation');
+ }
+ } elseif ($previousLineMatch) {
+ $variableRegexp = "/^{$constantRegexp}/";
+ if (preg_match($variableRegexp, $lineContent) !== 0) {
+ $phpcsFile->addWarning($error, $stackPtr, 'VariableTranslation');
+ }
+ }
}
}
diff --git a/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php b/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php
new file mode 100644
index 00000000..9afd983b
--- /dev/null
+++ b/Magento/Sniffs/Whitespace/EmptyLineMissedSniff.php
@@ -0,0 +1,68 @@
+getTokens();
+ if ($this->doCheck($phpcsFile, $stackPtr, $tokens)) {
+ $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true);
+ if ($tokens[$stackPtr]['line'] - $tokens[$previous]['line'] < 2) {
+ $error = 'Empty line missed';
+ $phpcsFile->addWarning($error, $stackPtr, '', null);
+ }
+ }
+ }
+
+ /**
+ * Execute empty line missed check.
+ *
+ * @param File $phpcsFile
+ * @param int $stackPtr
+ * @param array $tokens
+ * @return bool
+ */
+ private function doCheck(File $phpcsFile, $stackPtr, $tokens)
+ {
+ $result = false;
+ if ($phpcsFile->hasCondition($stackPtr, T_CLASS) || $phpcsFile->hasCondition($stackPtr, T_INTERFACE)) {
+ $result = true;
+ }
+
+ if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION)) {
+ $result = false;
+ }
+ $previous = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true);
+ if ($tokens[$previous]['type'] === 'T_OPEN_CURLY_BRACKET') {
+ $result = false;
+ }
+
+ if (strpos($tokens[$stackPtr]['content'], '/**') === false) {
+ $result = false;
+ }
+
+ return $result;
+ }
+}
diff --git a/Magento/Tests/Annotation/ClassAnnotationStructureUnitTest.inc b/Magento/Tests/Annotation/ClassAnnotationStructureUnitTest.inc
new file mode 100644
index 00000000..46b46037
--- /dev/null
+++ b/Magento/Tests/Annotation/ClassAnnotationStructureUnitTest.inc
@@ -0,0 +1,31 @@
+ 1,
+ 26 => 1,
+ ];
+ }
+}
diff --git a/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.inc b/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.inc
new file mode 100644
index 00000000..6d8f0d60
--- /dev/null
+++ b/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.inc
@@ -0,0 +1,309 @@
+productVisibility = $productVisibility;
+ }
+
+ /**
+ * block description
+ *
+ * {@inheritdoc}
+ *
+ * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
+ * @return void
+ */
+ public function construct(AbstractDb $collection)
+ {
+ /** @var */
+ $collection->setVisibility($this->productVisibility->getVisibleInCatalogIds());
+ }
+
+ /**
+ * Move category
+ *
+ *
+ * @param int $parentId new parent category id
+ *
+ * @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException|\Exception
+ */
+ public function move($parentId)
+ {
+ /**
+ * Validate new parent category id. (category model is used for backward
+ * compatibility in event params)
+ */
+ try {
+ $this->categoryRepository->get($parentId, $this->getStoreId());
+ } catch (NoSuchEntityException $e) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Sorry, but we can\'t find the new parent category you selected.'),
+ $e
+ );
+ }
+ return true;
+ }
+
+ /**
+ * Block for short description
+ *
+ * This a long description {@inheritdoc} consists more lines as part of the long description
+ * on multi line.
+ *
+ * @param int $store
+ *
+ *
+ */
+ public function getProductListDefaultSortBy26032($store)
+ {
+ return $store;
+ }
+
+ /**
+ *
+ *
+ *
+ */
+ public function getProductListDefaultSortBy2632()
+ {
+ }
+
+ /**
+ * Block for short description
+ *
+ * This a long description {@inheritdoc} consists more lines as part of the long description
+ * on multi line.
+ *
+ * @param int $store
+ *
+ *
+ *
+ */
+ public function getProductListDefaultSortBy2002($store)
+ {
+ return $store;
+ }
+
+ /**
+ *
+ * block for short description
+ *
+ * @param int $store
+ * @return int
+ */
+ public function getProductListDefaultSortBy3002($store)
+ {
+ return $store;
+ }
+
+ /**
+ * Block for short description
+ *
+ * @see consists more lines as part of the long description
+ * on multi line.
+ *
+ * @param string $store
+ * @param string $foo
+ */
+ public function getProductListDefaultSortBy12($store, $foo)
+ {
+ return $store === $foo;
+ }
+
+ /**
+ * Block for short description
+ *
+ * {@inheritdoc}
+ *
+ * @param string $store
+ * @param string $foo
+ */
+ public function getProductListDefaultSort2($store, $foo)
+ {
+ return $store === $foo;
+ }
+
+ /**
+ * Block for short description
+ *
+ * a long description {@inheritdoc} consists more lines as part of the long description
+ * on multi line.
+ *
+ * @param string $store
+ * @param string $foo
+ */
+ public function getProductListDefault($store, $foo)
+ {
+ return $store === $foo;
+ }
+
+ /**
+ * Retrieve custom options
+ *
+ * @param ProductOptionInterface $productOption
+ *
+ * @return array
+ */
+ protected function getCustomOptions(ProductOptionInterface $productOption)
+ {
+ if ($productOption
+ && $productOption->getExtensionAttributes()
+ && $productOption->getExtensionAttributes()->getCustomOptions()
+ ) {
+ return $productOption->getExtensionAttributes()
+ ->getCustomOptions();
+ }
+ return [];
+ }
+
+ /**
+ * This is the summary for a DocBlock.
+ *
+ * This is the description for a DocBlock. This text may contain
+ * multiple lines and even some _markdown_.
+ * * Markdown style lists function too
+ * * Just try this out once
+ * The section after the description contains the tags; which provide
+ * structured meta-data concerning the given element.
+ *
+ * @param int $example This is an example function/method parameter description.
+ * @param string $example2 This is a second example.
+ *
+ */
+ public function getProductListDefaultSortBy2($example, $example2)
+ {
+ return $example === $example2;
+ }
+
+ /**
+ * Returns the content of the tokens from the specified start position in
+ * the token stack for the specified length.
+ *
+ * @param int $start
+ * @param int $length
+ *
+ * @return string The token contents.
+ */
+ public function getProductListDefaultSortBy($start, $length)
+ {
+ return $start === $length;
+ }
+
+ /**
+ * Some text about this step/method returns the content of the tokens the token stack for the specified length
+ *
+ * @param string $name
+ * @param string $folder
+ *
+ * @see this file
+ * @When I create a file called :name in :folder
+ */
+ public function getProductListDefaultSortBy222($name, $folder)
+ {
+ return $name === $folder;
+ }
+
+ public function setExtensionAs(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes)
+ {
+ return $this->_setExtensionAttributes($extensionAttributes);
+ }
+
+ /**
+ *
+ * short description
+ * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes
+ * @return mixed
+ */
+ public function setEn(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes)
+ {
+ return $this->_setExtensionAttributes($extensionAttributes);
+ }
+
+ /**
+ * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes
+ * @return mixed
+ */
+ public function setExtenw(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes)
+ {
+ return $this->_setExtensionAttributes($extensionAttributes);
+ }
+
+ /**
+ *
+ * Short description
+ * @param \Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes
+ * @return mixed
+ */
+ public function setExff(\Magento\Catalog\Api\Data\CategoryExtensionInterface $extensionAttributes)
+ {
+ return $this->_setExtensionAttributes($extensionAttributes);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param int $start
+ * @param int $length
+ *
+ * @return string The token contents.
+ */
+ public function getProductSortBy($start, $length)
+ {
+ return $start === $length;
+ }
+}
diff --git a/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.php b/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.php
new file mode 100644
index 00000000..3744ec6c
--- /dev/null
+++ b/Magento/Tests/Annotation/MethodAnnotationStructureUnitTest.php
@@ -0,0 +1,54 @@
+ 1,
+ 18 => 1,
+ 30 => 1,
+ 36 => 1,
+ 45 => 2,
+ 47 => 1,
+ 55 => 1,
+ 63 => 1,
+ 80 => 1,
+ 111 => 1,
+ 117 => 1,
+ 136 => 1,
+ 144 => 2,
+ 184 => 1,
+ 227 => 1,
+ 235 => 1,
+ 268 => 2,
+ 269 => 1,
+ 277 => 1,
+ 278 => 1,
+ 288 => 1,
+ 289 => 1,
+ 298 => 1,
+ ];
+ }
+}
diff --git a/Magento/Tests/Classes/ObjectManagerUnitTest.inc b/Magento/Tests/Classes/ObjectManagerUnitTest.inc
new file mode 100644
index 00000000..7492f011
--- /dev/null
+++ b/Magento/Tests/Classes/ObjectManagerUnitTest.inc
@@ -0,0 +1,23 @@
+_objectManager->get(\Magento\Sales\Model\Order::class);
+
+$this->_om->get(\Magento\Sales\Model\Order::class);
+
+$this->om->create(\Magento\Sales\Model\Order::class);
+
+$shipment = $this->objectManager->create(\Magento\Sales\Model\Order::class);
+
+$productModel = $this->_giftWrapHelper->getObjectManager()
+ ->create(\Magento\Sales\Model\Order::class);
+
+$order = $block->getObjectManager()->create(\Magento\Sales\Model\Order::class);
+
+$order = $block->getObjectManager()->get(\Magento\Sales\Model\Order::class);
+
+$om = $block->get(\Magento\Sales\Model\Order::class);
+
+$om = $objectManager::get(\Magento\Sales\Model\Order::class);
+
+$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
+$product = $objectManager->create(\Magento\Sales\Model\Order::class);
diff --git a/Magento/Tests/Classes/ObjectManagerUnitTest.php b/Magento/Tests/Classes/ObjectManagerUnitTest.php
new file mode 100644
index 00000000..4190f8f6
--- /dev/null
+++ b/Magento/Tests/Classes/ObjectManagerUnitTest.php
@@ -0,0 +1,40 @@
+ 1,
+ 5 => 1,
+ 7 => 1,
+ 9 => 1,
+ 12 => 1,
+ 14 => 1,
+ 16 => 1,
+ 20 => 1,
+ 23 => 1,
+ ];
+ }
+}
diff --git a/Magento/Tests/Exceptions/NamespaceUnitTest.php b/Magento/Tests/Exceptions/NamespaceUnitTest.php
index d52742a6..e8bcfe94 100644
--- a/Magento/Tests/Exceptions/NamespaceUnitTest.php
+++ b/Magento/Tests/Exceptions/NamespaceUnitTest.php
@@ -18,9 +18,6 @@ class NamespaceUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 9 => 1,
- 10 => 1,
- 48 => 1,
];
}
@@ -29,6 +26,10 @@ public function getErrorList()
*/
public function getWarningList()
{
- return [];
+ return [
+ 9 => 1,
+ 10 => 1,
+ 48 => 1,
+ ];
}
}
diff --git a/Magento/Tests/NamingConvention/ReservedWordsUnitTest.inc b/Magento/Tests/NamingConvention/ReservedWordsUnitTest.inc
new file mode 100644
index 00000000..8a165739
--- /dev/null
+++ b/Magento/Tests/NamingConvention/ReservedWordsUnitTest.inc
@@ -0,0 +1,22 @@
+ 1,
+ 4 => 1,
+ 6 => 1,
+ 8 => 1,
+ 10 => 1,
+ 12 => 1,
+ 14 => 1,
+ 16 => 1,
+ 18 => 1,
+ 20 => 1,
+ 22 => 1,
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.inc b/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.inc
new file mode 100644
index 00000000..b254ebc0
--- /dev/null
+++ b/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.inc
@@ -0,0 +1,5 @@
+= "foo" ?>
+
+
+
+
diff --git a/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.php b/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.php
new file mode 100644
index 00000000..512c2a9e
--- /dev/null
+++ b/Magento/Tests/PHP/ShortEchoSyntaxUnitTest.php
@@ -0,0 +1,32 @@
+ 1,
+ ];
+ }
+}
diff --git a/Magento/Tests/Strings/RegExUnitTest.inc b/Magento/Tests/Strings/ExecutableRegExUnitTest.inc
similarity index 100%
rename from Magento/Tests/Strings/RegExUnitTest.inc
rename to Magento/Tests/Strings/ExecutableRegExUnitTest.inc
diff --git a/Magento/Tests/Strings/RegExUnitTest.php b/Magento/Tests/Strings/ExecutableRegExUnitTest.php
similarity index 84%
rename from Magento/Tests/Strings/RegExUnitTest.php
rename to Magento/Tests/Strings/ExecutableRegExUnitTest.php
index 6ec3b53e..1c2e0f51 100644
--- a/Magento/Tests/Strings/RegExUnitTest.php
+++ b/Magento/Tests/Strings/ExecutableRegExUnitTest.php
@@ -8,9 +8,9 @@
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
/**
- * Class RegExUnitTest
+ * Class ExecutableRegExUnitTest
*/
-class RegExUnitTest extends AbstractSniffUnitTest
+class ExecutableRegExUnitTest extends AbstractSniffUnitTest
{
/**
* @inheritdoc
diff --git a/Magento/Tests/Translation/ConstantUsageUnitTest.inc b/Magento/Tests/Translation/ConstantUsageUnitTest.inc
index 6fbec171..89560092 100644
--- a/Magento/Tests/Translation/ConstantUsageUnitTest.inc
+++ b/Magento/Tests/Translation/ConstantUsageUnitTest.inc
@@ -1,19 +1,52 @@
1,
+ 7 => 1,
9 => 1,
- 11 => 1,
12 => 1,
15 => 1,
+ 17 => 1,
+ 19 => 1,
+ 21 => 1,
+ 24 => 1,
];
}
}
diff --git a/Magento/ruleset.xml b/Magento/ruleset.xml
index 112b031f..10bd7805 100644
--- a/Magento/ruleset.xml
+++ b/Magento/ruleset.xml
@@ -60,9 +60,15 @@
10
+
+ 6
+
8
+
+ 8
+
8
warning
@@ -73,14 +79,24 @@
8
+
+
+
+
+
+ 6
+
10
+
+ 6
+
6
-
- 8
+
+ 10
6
@@ -91,12 +107,21 @@
10
+
+ 8
+
10
+
+ 8
+
8
+
+ 8
+
8
@@ -113,7 +138,7 @@
*.phtml
8
-
+
10
@@ -126,7 +151,7 @@
6
-
+
6
@@ -137,6 +162,7 @@
8
warning
+
10
diff --git a/composer.json b/composer.json
index 6500684e..6c27724c 100644
--- a/composer.json
+++ b/composer.json
@@ -7,7 +7,7 @@
],
"type": "phpcodesniffer-standard",
"require": {
- "php": ">=5.5.0",
+ "php": ">=5.6.0",
"squizlabs/php_codesniffer": "~3.3.0"
},
"require-dev": {