diff --git a/Magento2/Helpers/Tokenizer/AbstractTokenizer.php b/Magento2/Helpers/Tokenizer/AbstractTokenizer.php new file mode 100644 index 00000000..f49c753f --- /dev/null +++ b/Magento2/Helpers/Tokenizer/AbstractTokenizer.php @@ -0,0 +1,128 @@ +_currentIndex + 1 >= strlen($this->_string)) { + return false; + } + + $this->_currentIndex++; + return true; + } + + /** + * Move current index to previous char. + * + * If index out of bounds returns false + * + * @return boolean + */ + public function prev() + { + if ($this->_currentIndex - 1 < 0) { + return false; + } + + $this->_currentIndex--; + return true; + } + + /** + * Move current index backwards. + * + * If index out of bounds returns false + * + * @param int $distance number of characters to backtrack + * @return bool + */ + public function back($distance) + { + if ($this->_currentIndex - $distance < 0) { + return false; + } + + $this->_currentIndex -= $distance; + return true; + } + + /** + * Return current char + * + * @return string + */ + public function char() + { + return $this->_string[$this->_currentIndex]; + } + + /** + * Set string for tokenize + * + * @param string $value + * @return void + */ + public function setString($value) + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $this->_string = rawurldecode($value); + $this->reset(); + } + + /** + * Move char index to begin of string + * + * @return void + */ + public function reset() + { + $this->_currentIndex = 0; + } + + /** + * Return true if current char is white-space + * + * @return boolean + */ + public function isWhiteSpace() + { + return $this->_string === '' ?: trim($this->char()) !== $this->char(); + } + + /** + * Tokenize string + * + * @return array + */ + abstract public function tokenize(); +} diff --git a/Magento2/Helpers/Tokenizer/Parameter.php b/Magento2/Helpers/Tokenizer/Parameter.php new file mode 100644 index 00000000..ec3a3564 --- /dev/null +++ b/Magento2/Helpers/Tokenizer/Parameter.php @@ -0,0 +1,76 @@ +isWhiteSpace()) { + continue; + } + + if ($this->char() !== '=') { + $parameterName .= $this->char(); + } else { + $parameters[$parameterName] = $this->getValue(); + $parameterName = ''; + } + } while ($this->next()); + return $parameters; + } + + /** + * Get string value in parameters through tokenize + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getValue() + { + $this->next(); + $value = ''; + if ($this->isWhiteSpace()) { + return $value; + } + $quoteStart = $this->char() == "'" || $this->char() == '"'; + + if ($quoteStart) { + $breakSymbol = $this->char(); + } else { + $breakSymbol = false; + $value .= $this->char(); + } + + while ($this->next()) { + if (!$breakSymbol && $this->isWhiteSpace()) { + break; + } elseif ($breakSymbol && $this->char() == $breakSymbol) { + break; + } elseif ($this->char() == '\\') { + $this->next(); + if ($this->char() != '\\') { + $value .= '\\'; + } + $value .= $this->char(); + } else { + $value .= $this->char(); + } + } + return $value; + } +} diff --git a/Magento2/Helpers/Tokenizer/Variable.php b/Magento2/Helpers/Tokenizer/Variable.php new file mode 100644 index 00000000..81c8b1d8 --- /dev/null +++ b/Magento2/Helpers/Tokenizer/Variable.php @@ -0,0 +1,326 @@ +isWhiteSpace()) { + // Ignore white spaces + continue; + } elseif ($this->char() != '.' && $this->char() != '(') { + // Property or method name + $parameterName .= $this->char(); + } elseif ($this->char() == '(') { + // Method declaration + $methodArgs = $this->getMethodArgs(); + $actions[] = ['type' => 'method', 'name' => $parameterName, 'args' => $methodArgs]; + $parameterName = ''; + } elseif ($parameterName != '') { + // Property or variable declaration + if ($variableSet) { + $actions[] = ['type' => 'property', 'name' => $parameterName]; + } else { + $variableSet = true; + $actions[] = ['type' => 'variable', 'name' => $parameterName]; + } + $parameterName = ''; + } + } while ($this->next()); + + if ($parameterName != '') { + if ($variableSet) { + $actions[] = ['type' => 'property', 'name' => $parameterName]; + } else { + $actions[] = ['type' => 'variable', 'name' => $parameterName]; + } + } + + return $actions; + } + + /** + * Get string value for method args + * + * @return string + */ + public function getString() + { + $value = ''; + if ($this->isWhiteSpace()) { + return $value; + } + $quoteStart = $this->isQuote(); + + if ($quoteStart) { + $breakSymbol = $this->char(); + } else { + $breakSymbol = false; + $value .= $this->char(); + } + + while ($this->next()) { + if (!$breakSymbol && $this->isStringBreak()) { + $this->prev(); + break; + } elseif ($breakSymbol && $this->char() == $breakSymbol) { + break; + } elseif ($this->char() == '\\') { + $this->next(); + $value .= $this->char(); + } else { + $value .= $this->char(); + } + } + + return $value; + } + + /** + * Get array member key or return false if none present + * + * @return bool|string + */ + public function getMemberKey() + { + $value = ''; + if ($this->isWhiteSpace()) { + return $value; + } + + $quoteStart = $this->isQuote(); + + if ($quoteStart) { + $closeQuote = $this->char(); + } else { + $closeQuote = false; + $value .= $this->char(); + } + + while ($this->next()) { + if ($closeQuote) { + if ($this->char() == $closeQuote) { + $closeQuote = false; + continue; + } + $value .= $this->char(); + } elseif ($this->char() == ':') { + $this->next(); + + return $value; + } elseif ($this->isStringBreak()) { + $this->prev(); + break; + } else { + $value .= $this->char(); + } + } + + if ($quoteStart) { + $this->back(strlen($value) + 1); + } else { + $this->back(strlen($value) - 1); + } + + return false; + } + + /** + * Get array value for method args + * + * Parses arrays demarcated via open/closing brackets. Keys/value pairs are separated by a + * single colon character. Multi-dimensional arrays are supported. Example input: + * + * [key:value, "key2":"value2", [ + * [123, foo], + * ]] + * + * @return array + */ + public function getArray() + { + $values = []; + if (!$this->isArray()) { + return $values; + } + + $this->incArrayDepth(); + + while ($this->next()) { + if ($this->char() == ']') { + break; + } elseif ($this->isWhiteSpace() || $this->char() == ',') { + continue; + } + + $key = $this->getMemberKey(); + + if ($this->isNumeric()) { + $val = $this->getNumber(); + } elseif ($this->isArray()) { + $val = $this->getArray(); + } else { + $val = $this->getString(); + } + + if ($key) { + $values[$key] = $val; + } else { + $values[] = $val; + } + } + + $this->decArrayDepth(); + + return $values; + } + + /** + * Return the internal array depth counter + * + * @return int + */ + protected function getArrayDepth() + { + return $this->arrayDepth; + } + + /** + * Increment the internal array depth counter + * + * @return void + */ + protected function incArrayDepth() + { + $this->arrayDepth++; + } + + /** + * Decrement the internal array depth counter + * + * If depth is already 0 do nothing + * + * @return void + */ + protected function decArrayDepth() + { + if ($this->arrayDepth == 0) { + return; + } + $this->arrayDepth--; + } + + /** + * Return true if current char is a number + * + * @return boolean + */ + public function isNumeric() + { + return $this->char() >= '0' && $this->char() <= '9'; + } + + /** + * Return true if current char is quote or apostrophe + * + * @return boolean + */ + public function isQuote() + { + return $this->char() == '"' || $this->char() == "'"; + } + + /** + * Retrun true if current char is opening boundary for an array + * + * @return bool + */ + public function isArray() + { + return $this->char() == '['; + } + + /** + * Return true if current char is closing boundary for string + * + * @return bool + */ + public function isStringBreak() + { + if ($this->getArrayDepth() == 0 && ($this->isWhiteSpace() || $this->char() == ',' || $this->char() == ')')) { + return true; + } elseif ($this->getArrayDepth() > 0 && ($this->char() == ',' || $this->char() == ']')) { + return true; + } + + return false; + } + + /** + * Return array of arguments for method + * + * @return array + */ + public function getMethodArgs() + { + $value = []; + + while ($this->next() && $this->char() != ')') { + if ($this->isWhiteSpace() || $this->char() == ',') { + continue; + } elseif ($this->isNumeric()) { + $value[] = $this->getNumber(); + } elseif ($this->isArray()) { + $value[] = $this->getArray(); + } else { + $value[] = $this->getString(); + } + } + + return $value; + } + + /** + * Return number value for method args + * + * @return float + */ + public function getNumber() + { + $value = $this->char(); + while (($this->isNumeric() || $this->char() == '.') && $this->next()) { + $value .= $this->char(); + } + + if (!$this->isNumeric()) { + $this->prev(); + } + + return (float)$value; + } +} diff --git a/Magento2/Sniffs/Html/HtmlDirectiveSniff.php b/Magento2/Sniffs/Html/HtmlDirectiveSniff.php index b14ae7ba..62267ec1 100644 --- a/Magento2/Sniffs/Html/HtmlDirectiveSniff.php +++ b/Magento2/Sniffs/Html/HtmlDirectiveSniff.php @@ -20,7 +20,7 @@ class HtmlDirectiveSniff implements Sniff const CONSTRUCTION_IF_PATTERN = '/{{if\s*(.*?)}}(.*?)({{else}}(.*?))?{{\\/if\s*}}/si'; const LOOP_PATTERN = '/{{for(?P.*? )(in)(?P.*?)}}(?P.*?){{\/for}}/si'; const CONSTRUCTION_PATTERN = '/{{([a-z]{0,10})(.*?)}}(?:(.*?)(?:{{\/(?:\\1)}}))?/si'; - + /** * @var array */ @@ -162,7 +162,7 @@ private function processVarDirectivesAndParams(string $html, File $phpcsFile): s */ private function validateDirectiveBody(File $phpcsFile, string $body): void { - $parameterTokenizer = new Template\Tokenizer\Parameter(); + $parameterTokenizer = new \Magento2\Helpers\Tokenizer\Parameter(); $parameterTokenizer->setString($body); $params = $parameterTokenizer->tokenize(); @@ -185,7 +185,7 @@ private function validateVariableUsage(File $phpcsFile, string $body): void if (strpos($body, '|') !== false) { $this->unfilteredVariables[] = 'var ' . trim(explode('|', $body, 2)[0]); } - $variableTokenizer = new Template\Tokenizer\Variable(); + $variableTokenizer = new \Magento2\Helpers\Tokenizer\Variable(); $variableTokenizer->setString($body); $stack = $variableTokenizer->tokenize();