* array(
- * 'scope' => 'public', // Public, private, or protected
- * 'scope_specified' => true, // TRUE if the scope keyword was found.
- * 'return_type' => '', // The return type of the method.
- * 'return_type_token' => integer, // The stack pointer to the start of the return type
- * // or FALSE if there is no return type.
- * 'nullable_return_type' => false, // TRUE if the return type is nullable.
- * 'is_abstract' => false, // TRUE if the abstract keyword was found.
- * 'is_final' => false, // TRUE if the final keyword was found.
- * 'is_static' => false, // TRUE if the static keyword was found.
- * 'has_body' => false, // TRUE if the method has a body
+ * 'scope' => 'public', // Public, private, or protected
+ * 'scope_specified' => true, // TRUE if the scope keyword was found.
+ * 'return_type' => '', // The return type of the method.
+ * 'return_type_token' => integer, // The stack pointer to the start of the return type
+ * // or FALSE if there is no return type.
+ * 'return_type_end_token' => integer, // The stack pointer to the end of the return type
+ * // or FALSE if there is no return type.
+ * 'nullable_return_type' => false, // TRUE if the return type is preceded by the
+ * // nullability operator.
+ * 'is_abstract' => false, // TRUE if the abstract keyword was found.
+ * 'is_final' => false, // TRUE if the final keyword was found.
+ * 'is_static' => false, // TRUE if the static keyword was found.
+ * 'has_body' => false, // TRUE if the method has a body
* );
*
*
@@ -1615,6 +1688,7 @@ public function getMethodProperties($stackPtr)
$returnType = '';
$returnTypeToken = false;
+ $returnTypeEndToken = false;
$nullableReturnType = false;
$hasBody = true;
@@ -1625,12 +1699,17 @@ public function getMethodProperties($stackPtr)
}
$valid = [
- T_STRING => T_STRING,
- T_CALLABLE => T_CALLABLE,
- T_SELF => T_SELF,
- T_PARENT => T_PARENT,
- T_STATIC => T_STATIC,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_STRING => T_STRING,
+ T_CALLABLE => T_CALLABLE,
+ T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
+ T_STATIC => T_STATIC,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
+ T_NAMESPACE => T_NAMESPACE,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_TYPE_UNION => T_TYPE_UNION,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
];
for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
@@ -1650,9 +1729,10 @@ public function getMethodProperties($stackPtr)
$returnTypeToken = $i;
}
- $returnType .= $this->tokens[$i]['content'];
+ $returnType .= $this->tokens[$i]['content'];
+ $returnTypeEndToken = $i;
}
- }
+ }//end for
if ($this->tokens[$stackPtr]['code'] === T_FN) {
$bodyToken = T_FN_ARROW;
@@ -1669,15 +1749,16 @@ public function getMethodProperties($stackPtr)
}
return [
- 'scope' => $scope,
- 'scope_specified' => $scopeSpecified,
- 'return_type' => $returnType,
- 'return_type_token' => $returnTypeToken,
- 'nullable_return_type' => $nullableReturnType,
- 'is_abstract' => $isAbstract,
- 'is_final' => $isFinal,
- 'is_static' => $isStatic,
- 'has_body' => $hasBody,
+ 'scope' => $scope,
+ 'scope_specified' => $scopeSpecified,
+ 'return_type' => $returnType,
+ 'return_type_token' => $returnTypeToken,
+ 'return_type_end_token' => $returnTypeEndToken,
+ 'nullable_return_type' => $nullableReturnType,
+ 'is_abstract' => $isAbstract,
+ 'is_final' => $isFinal,
+ 'is_static' => $isStatic,
+ 'has_body' => $hasBody,
];
}//end getMethodProperties()
@@ -1693,12 +1774,14 @@ public function getMethodProperties($stackPtr)
* 'scope' => string, // Public, private, or protected.
* 'scope_specified' => boolean, // TRUE if the scope was explicitly specified.
* 'is_static' => boolean, // TRUE if the static keyword was found.
+ * 'is_readonly' => boolean, // TRUE if the readonly keyword was found.
* 'type' => string, // The type of the var (empty if no type specified).
* 'type_token' => integer, // The stack pointer to the start of the type
* // or FALSE if there is no type.
* 'type_end_token' => integer, // The stack pointer to the end of the type
* // or FALSE if there is no type.
- * 'nullable_type' => boolean, // TRUE if the type is nullable.
+ * 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability
+ * // operator.
* );
*
*
@@ -1724,23 +1807,26 @@ public function getMemberProperties($stackPtr)
&& $this->tokens[$ptr]['code'] !== T_TRAIT)
) {
if (isset($this->tokens[$ptr]) === true
- && $this->tokens[$ptr]['code'] === T_INTERFACE
+ && ($this->tokens[$ptr]['code'] === T_INTERFACE
+ || $this->tokens[$ptr]['code'] === T_ENUM)
) {
- // T_VARIABLEs in interfaces can actually be method arguments
- // but they wont be seen as being inside the method because there
+ // T_VARIABLEs in interfaces/enums can actually be method arguments
+ // but they won't be seen as being inside the method because there
// are no scope openers and closers for abstract methods. If it is in
// parentheses, we can be pretty sure it is a method argument.
if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
|| empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
) {
- $error = 'Possible parse error: interfaces may not include member vars';
- $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar');
+ $error = 'Possible parse error: %ss may not include member vars';
+ $code = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content']));
+ $data = [strtolower($this->tokens[$ptr]['content'])];
+ $this->addWarning($error, $stackPtr, $code, $data);
return [];
}
} else {
throw new RuntimeException('$stackPtr is not a class member var');
}
- }
+ }//end if
// Make sure it's not a method parameter.
if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
@@ -1760,6 +1846,7 @@ public function getMemberProperties($stackPtr)
T_PROTECTED => T_PROTECTED,
T_STATIC => T_STATIC,
T_VAR => T_VAR,
+ T_READONLY => T_READONLY,
];
$valid += Util\Tokens::$emptyTokens;
@@ -1767,12 +1854,14 @@ public function getMemberProperties($stackPtr)
$scope = 'public';
$scopeSpecified = false;
$isStatic = false;
+ $isReadonly = false;
$startOfStatement = $this->findPrevious(
[
T_SEMICOLON,
T_OPEN_CURLY_BRACKET,
T_CLOSE_CURLY_BRACKET,
+ T_ATTRIBUTE_END,
],
($stackPtr - 1)
);
@@ -1798,6 +1887,9 @@ public function getMemberProperties($stackPtr)
case T_STATIC:
$isStatic = true;
break;
+ case T_READONLY:
+ $isReadonly = true;
+ break;
}
}//end for
@@ -1809,11 +1901,16 @@ public function getMemberProperties($stackPtr)
if ($i < $stackPtr) {
// We've found a type.
$valid = [
- T_STRING => T_STRING,
- T_CALLABLE => T_CALLABLE,
- T_SELF => T_SELF,
- T_PARENT => T_PARENT,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_STRING => T_STRING,
+ T_CALLABLE => T_CALLABLE,
+ T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
+ T_NAMESPACE => T_NAMESPACE,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_TYPE_UNION => T_TYPE_UNION,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
];
for ($i; $i < $stackPtr; $i++) {
@@ -1845,6 +1942,7 @@ public function getMemberProperties($stackPtr)
'scope' => $scope,
'scope_specified' => $scopeSpecified,
'is_static' => $isStatic,
+ 'is_readonly' => $isReadonly,
'type' => $type,
'type_token' => $typeToken,
'type_end_token' => $typeEndToken,
@@ -1862,6 +1960,7 @@ public function getMemberProperties($stackPtr)
* array(
* 'is_abstract' => false, // true if the abstract keyword was found.
* 'is_final' => false, // true if the final keyword was found.
+ * 'is_readonly' => false, // true if the readonly keyword was found.
* );
*
*
@@ -1881,6 +1980,7 @@ public function getClassProperties($stackPtr)
$valid = [
T_FINAL => T_FINAL,
T_ABSTRACT => T_ABSTRACT,
+ T_READONLY => T_READONLY,
T_WHITESPACE => T_WHITESPACE,
T_COMMENT => T_COMMENT,
T_DOC_COMMENT => T_DOC_COMMENT,
@@ -1888,6 +1988,7 @@ public function getClassProperties($stackPtr)
$isAbstract = false;
$isFinal = false;
+ $isReadonly = false;
for ($i = ($stackPtr - 1); $i > 0; $i--) {
if (isset($valid[$this->tokens[$i]['code']]) === false) {
@@ -1902,12 +2003,17 @@ public function getClassProperties($stackPtr)
case T_FINAL:
$isFinal = true;
break;
+
+ case T_READONLY:
+ $isReadonly = true;
+ break;
}
}//end for
return [
'is_abstract' => $isAbstract,
'is_final' => $isFinal,
+ 'is_readonly' => $isReadonly,
];
}//end getClassProperties()
@@ -1978,22 +2084,11 @@ public function isReference($stackPtr)
$owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
if ($owner['code'] === T_FUNCTION
|| $owner['code'] === T_CLOSURE
+ || $owner['code'] === T_FN
) {
$params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
foreach ($params as $param) {
- $varToken = $tokenAfter;
- if ($param['variable_length'] === true) {
- $varToken = $this->findNext(
- (Util\Tokens::$emptyTokens + [T_ELLIPSIS]),
- ($stackPtr + 1),
- null,
- true
- );
- }
-
- if ($param['token'] === $varToken
- && $param['pass_by_reference'] === true
- ) {
+ if ($param['reference_token'] === $stackPtr) {
// Function parameter declared to be passed by reference.
return true;
}
@@ -2103,12 +2198,12 @@ public function getTokensAsString($start, $length, $origContent=false)
* @param int|string|array $types The type(s) of tokens to search for.
* @param int $start The position to start searching from in the
* token stack.
- * @param int $end The end position to fail if no token is found.
+ * @param int|null $end The end position to fail if no token is found.
* if not specified or null, end will default to
* the start of the token stack.
* @param bool $exclude If true, find the previous token that is NOT of
* the types specified in $types.
- * @param string $value The value that the token(s) must be equal to.
+ * @param string|null $value The value that the token(s) must be equal to.
* If value is omitted, tokens with any value will
* be returned.
* @param bool $local If true, tokens outside the current statement
@@ -2184,12 +2279,12 @@ public function findPrevious(
* @param int|string|array $types The type(s) of tokens to search for.
* @param int $start The position to start searching from in the
* token stack.
- * @param int $end The end position to fail if no token is found.
+ * @param int|null $end The end position to fail if no token is found.
* if not specified or null, end will default to
* the end of the token stack.
* @param bool $exclude If true, find the next token that is NOT of
* a type specified in $types.
- * @param string $value The value that the token(s) must be equal to.
+ * @param string|null $value The value that the token(s) must be equal to.
* If value is omitted, tokens with any value will
* be returned.
* @param bool $local If true, tokens outside the current statement
@@ -2250,27 +2345,104 @@ public function findNext(
*/
public function findStartOfStatement($start, $ignore=null)
{
- $endTokens = Util\Tokens::$blockOpeners;
+ $startTokens = Util\Tokens::$blockOpeners;
+ $startTokens[T_OPEN_SHORT_ARRAY] = true;
+ $startTokens[T_OPEN_TAG] = true;
+ $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
- $endTokens[T_COLON] = true;
- $endTokens[T_COMMA] = true;
- $endTokens[T_DOUBLE_ARROW] = true;
- $endTokens[T_SEMICOLON] = true;
- $endTokens[T_OPEN_TAG] = true;
- $endTokens[T_CLOSE_TAG] = true;
- $endTokens[T_OPEN_SHORT_ARRAY] = true;
+ $endTokens = [
+ T_CLOSE_TAG => true,
+ T_COLON => true,
+ T_COMMA => true,
+ T_DOUBLE_ARROW => true,
+ T_MATCH_ARROW => true,
+ T_SEMICOLON => true,
+ ];
if ($ignore !== null) {
$ignore = (array) $ignore;
foreach ($ignore as $code) {
- unset($endTokens[$code]);
+ if (isset($startTokens[$code]) === true) {
+ unset($startTokens[$code]);
+ }
+
+ if (isset($endTokens[$code]) === true) {
+ unset($endTokens[$code]);
+ }
}
}
+ // If the start token is inside the case part of a match expression,
+ // find the start of the condition. If it's in the statement part, find
+ // the token that comes after the match arrow.
+ $matchExpression = $this->getCondition($start, T_MATCH);
+ if ($matchExpression !== false) {
+ for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
+ if ($prevMatch !== $start
+ && ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW
+ || $this->tokens[$prevMatch]['code'] === T_COMMA)
+ ) {
+ break;
+ }
+
+ // Skip nested statements.
+ if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
+ && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
+ ) {
+ $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
+ } else if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
+ && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
+ ) {
+ $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
+ }
+ }
+
+ if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
+ // We're before the arrow in the first case.
+ $next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
+ if ($next === false) {
+ return $start;
+ }
+
+ return $next;
+ }
+
+ if ($this->tokens[$prevMatch]['code'] === T_COMMA) {
+ // We're before the arrow, but not in the first case.
+ $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($prevMatch - 1), $this->tokens[$matchExpression]['scope_opener']);
+ if ($prevMatchArrow === false) {
+ // We're before the arrow in the first case.
+ $next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
+ return $next;
+ }
+
+ $end = $this->findEndOfStatement($prevMatchArrow);
+ $next = $this->findNext(Util\Tokens::$emptyTokens, ($end + 1), null, true);
+ return $next;
+ }
+ }//end if
+
$lastNotEmpty = $start;
+ // If we are starting at a token that ends a scope block, skip to
+ // the start and continue from there.
+ // If we are starting at a token that ends a statement, skip this
+ // token so we find the true start of the statement.
+ while (isset($endTokens[$this->tokens[$start]['code']]) === true
+ || (isset($this->tokens[$start]['scope_condition']) === true
+ && $start === $this->tokens[$start]['scope_closer'])
+ ) {
+ if (isset($this->tokens[$start]['scope_condition']) === true) {
+ $start = $this->tokens[$start]['scope_condition'];
+ } else {
+ $start--;
+ }
+ }
+
for ($i = $start; $i >= 0; $i--) {
- if (isset($endTokens[$this->tokens[$i]['code']]) === true) {
+ if (isset($startTokens[$this->tokens[$i]['code']]) === true
+ || isset($endTokens[$this->tokens[$i]['code']]) === true
+ ) {
// Found the end of the previous statement.
return $lastNotEmpty;
}
@@ -2278,6 +2450,13 @@ public function findStartOfStatement($start, $ignore=null)
if (isset($this->tokens[$i]['scope_opener']) === true
&& $i === $this->tokens[$i]['scope_closer']
&& $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
+ && $this->tokens[$i]['code'] !== T_END_NOWDOC
+ && $this->tokens[$i]['code'] !== T_END_HEREDOC
+ && $this->tokens[$i]['code'] !== T_BREAK
+ && $this->tokens[$i]['code'] !== T_RETURN
+ && $this->tokens[$i]['code'] !== T_CONTINUE
+ && $this->tokens[$i]['code'] !== T_THROW
+ && $this->tokens[$i]['code'] !== T_EXIT
) {
// Found the end of the previous scope block.
return $lastNotEmpty;
@@ -2292,7 +2471,12 @@ public function findStartOfStatement($start, $ignore=null)
&& $i === $this->tokens[$i]['parenthesis_closer']
) {
$i = $this->tokens[$i]['parenthesis_opener'];
- }
+ } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
+ $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
+ if ($start !== false) {
+ $i = $start;
+ }
+ }//end if
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
$lastNotEmpty = $i;
@@ -2334,6 +2518,33 @@ public function findEndOfStatement($start, $ignore=null)
}
}
+ // If the start token is inside the case part of a match expression,
+ // advance to the match arrow and continue looking for the
+ // end of the statement from there so that we skip over commas.
+ if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
+ $matchExpression = $this->getCondition($start, T_MATCH);
+ if ($matchExpression !== false) {
+ $beforeArrow = true;
+ $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
+ if ($prevMatchArrow !== false) {
+ $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
+ if ($prevComma === false) {
+ // No comma between this token and the last match arrow,
+ // so this token exists after the arrow and we can continue
+ // checking as normal.
+ $beforeArrow = false;
+ }
+ }
+
+ if ($beforeArrow === true) {
+ $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
+ if ($nextMatchArrow !== false) {
+ $start = $nextMatchArrow;
+ }
+ }
+ }//end if
+ }//end if
+
$lastNotEmpty = $start;
for ($i = $start; $i < $this->numTokens; $i++) {
if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
@@ -2494,7 +2705,7 @@ public function hasCondition($stackPtr, $types)
* @param int $stackPtr The position of the token we are checking.
* @param int|string $type The type of token to search for.
* @param bool $first If TRUE, will return the matched condition
- * furtherest away from the passed token.
+ * furthest away from the passed token.
* If FALSE, will return the matched condition
* closest to the passed token.
*
@@ -2582,11 +2793,11 @@ public function findExtendedClassName($stackPtr)
/**
- * Returns the names of the interfaces that the specified class implements.
+ * Returns the names of the interfaces that the specified class or enum implements.
*
* Returns FALSE on error or if there are no implemented interface names.
*
- * @param int $stackPtr The stack position of the class.
+ * @param int $stackPtr The stack position of the class or enum token.
*
* @return array|false
*/
@@ -2599,6 +2810,7 @@ public function findImplementedInterfaceNames($stackPtr)
if ($this->tokens[$stackPtr]['code'] !== T_CLASS
&& $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
+ && $this->tokens[$stackPtr]['code'] !== T_ENUM
) {
return false;
}
diff --git a/src/Files/FileList.php b/src/Files/FileList.php
index 877b1c003e..66833a3ee4 100644
--- a/src/Files/FileList.php
+++ b/src/Files/FileList.php
@@ -16,6 +16,7 @@
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Exceptions\DeepExitException;
+use ReturnTypeWillChange;
class FileList implements \Iterator, \Countable
{
@@ -169,6 +170,7 @@ private function getFilterClass()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function rewind()
{
reset($this->files);
@@ -181,10 +183,11 @@ public function rewind()
*
* @return \PHP_CodeSniffer\Files\File
*/
+ #[ReturnTypeWillChange]
public function current()
{
$path = key($this->files);
- if ($this->files[$path] === null) {
+ if (isset($this->files[$path]) === false) {
$this->files[$path] = new LocalFile($path, $this->ruleset, $this->config);
}
@@ -198,6 +201,7 @@ public function current()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function key()
{
return key($this->files);
@@ -210,6 +214,7 @@ public function key()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function next()
{
next($this->files);
@@ -222,6 +227,7 @@ public function next()
*
* @return boolean
*/
+ #[ReturnTypeWillChange]
public function valid()
{
if (current($this->files) === false) {
@@ -238,6 +244,7 @@ public function valid()
*
* @return integer
*/
+ #[ReturnTypeWillChange]
public function count()
{
return $this->numFiles;
diff --git a/src/Files/LocalFile.php b/src/Files/LocalFile.php
index 7c2c671d39..ca2e74ad39 100644
--- a/src/Files/LocalFile.php
+++ b/src/Files/LocalFile.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Util\Cache;
+use PHP_CodeSniffer\Util\Common;
class LocalFile extends File
{
@@ -29,7 +30,7 @@ class LocalFile extends File
public function __construct($path, Ruleset $ruleset, Config $config)
{
$this->path = trim($path);
- if (is_readable($this->path) === false) {
+ if (Common::isReadable($this->path) === false) {
parent::__construct($this->path, $ruleset, $config);
$error = 'Error opening file; file no longer exists or you do not have access to read the file';
$this->addMessage(true, $error, 1, 1, 'Internal.LocalFile', [], 5, false);
diff --git a/src/Filters/Filter.php b/src/Filters/Filter.php
index fa7360f0cd..a1246a2c52 100644
--- a/src/Filters/Filter.php
+++ b/src/Filters/Filter.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Util;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;
+use ReturnTypeWillChange;
class Filter extends \RecursiveFilterIterator
{
@@ -89,6 +90,7 @@ public function __construct($iterator, $basedir, Config $config, Ruleset $rulese
*
* @return bool
*/
+ #[ReturnTypeWillChange]
public function accept()
{
$filePath = $this->current();
@@ -130,6 +132,7 @@ public function accept()
*
* @return \RecursiveIterator
*/
+ #[ReturnTypeWillChange]
public function getChildren()
{
$filterClass = get_called_class();
@@ -218,7 +221,7 @@ protected function shouldIgnorePath($path)
// Need to check this pattern for dirs as well as individual file paths.
$this->ignoreFilePatterns[$pattern] = $type;
- $pattern = substr($pattern, 0, -2);
+ $pattern = substr($pattern, 0, -2).'(?=/|$)';
$this->ignoreDirPatterns[$pattern] = $type;
} else {
// This is a file-specific pattern, so only need to check this
diff --git a/src/Fixer.php b/src/Fixer.php
index 897e14771b..b8dc05b16e 100644
--- a/src/Fixer.php
+++ b/src/Fixer.php
@@ -184,6 +184,9 @@ public function fixFile()
}
echo ']... ';
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo PHP_EOL;
+ }
}
if ($this->numFixes === 0 && $this->inConflict === false) {
@@ -255,6 +258,10 @@ public function generateDiff($filePath=null, $colors=true)
unlink($tempName);
}
+ if ($diff === null) {
+ return '';
+ }
+
if ($colors === false) {
return $diff;
}
@@ -414,6 +421,7 @@ public function endChangeset()
}
$this->changeset = [];
+ return true;
}//end endChangeset()
@@ -743,7 +751,7 @@ public function addContentBefore($stackPtr, $content)
* @param int $change The number of spaces to adjust the indent by
* (positive or negative).
*
- * @return bool If the change was accepted.
+ * @return void
*/
public function changeCodeBlockIndent($start, $end, $change)
{
diff --git a/src/Generators/Markdown.php b/src/Generators/Markdown.php
index b51b267bcb..9756bcf140 100644
--- a/src/Generators/Markdown.php
+++ b/src/Generators/Markdown.php
@@ -84,7 +84,7 @@ protected function printFooter()
protected function processSniff(\DOMNode $doc)
{
$title = $this->getTitle($doc);
- echo "## $title".PHP_EOL;
+ echo PHP_EOL."## $title".PHP_EOL;
foreach ($doc->childNodes as $node) {
if ($node->nodeName === 'standard') {
diff --git a/src/Reports/Cbf.php b/src/Reports/Cbf.php
index 25249e858d..c85353d988 100644
--- a/src/Reports/Cbf.php
+++ b/src/Reports/Cbf.php
@@ -28,10 +28,10 @@ class Cbf implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException
@@ -44,6 +44,9 @@ public function generateFileReport($report, File $phpcsFile, $showSources=false,
ob_end_clean();
$startTime = microtime(true);
echo "\t=> Fixing file: $errors/$errors violations remaining";
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo PHP_EOL;
+ }
}
$fixed = $phpcsFile->fixer->fixFile();
diff --git a/src/Reports/Checkstyle.php b/src/Reports/Checkstyle.php
index 06a78e19fa..313e086710 100644
--- a/src/Reports/Checkstyle.php
+++ b/src/Reports/Checkstyle.php
@@ -23,10 +23,10 @@ class Checkstyle implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Code.php b/src/Reports/Code.php
index c54c1e1a4e..4a7a27af82 100644
--- a/src/Reports/Code.php
+++ b/src/Reports/Code.php
@@ -23,10 +23,10 @@ class Code implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
@@ -225,7 +225,7 @@ public function generateFileReport($report, File $phpcsFile, $showSources=false,
if (strpos($tokenContent, "\t") !== false) {
$token = $tokens[$i];
$token['content'] = $tokenContent;
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ if (stripos(PHP_OS, 'WIN') === 0) {
$tab = "\000";
} else {
$tab = "\033[30;1m»\033[0m";
diff --git a/src/Reports/Csv.php b/src/Reports/Csv.php
index 6db7ecfc18..1f02edbd85 100644
--- a/src/Reports/Csv.php
+++ b/src/Reports/Csv.php
@@ -22,10 +22,10 @@ class Csv implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Diff.php b/src/Reports/Diff.php
index ce4b31fc08..68af0a0261 100644
--- a/src/Reports/Diff.php
+++ b/src/Reports/Diff.php
@@ -22,10 +22,10 @@ class Diff implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Emacs.php b/src/Reports/Emacs.php
index 3555f55418..cae9d22d85 100644
--- a/src/Reports/Emacs.php
+++ b/src/Reports/Emacs.php
@@ -22,10 +22,10 @@ class Emacs implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Full.php b/src/Reports/Full.php
index 084bc8aa58..5f6bcde80a 100644
--- a/src/Reports/Full.php
+++ b/src/Reports/Full.php
@@ -23,10 +23,10 @@ class Full implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Gitblame.php b/src/Reports/Gitblame.php
index 947f3d80b1..0919e0e0ba 100644
--- a/src/Reports/Gitblame.php
+++ b/src/Reports/Gitblame.php
@@ -70,7 +70,7 @@ protected function getBlameContent($filename)
$cwd = getcwd();
chdir(dirname($filename));
- $command = 'git blame --date=short "'.$filename.'" 2>&1';
+ $command = 'git blame --date=short "'.basename($filename).'" 2>&1';
$handle = popen($command, 'r');
if ($handle === false) {
$error = 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL;
@@ -78,7 +78,7 @@ protected function getBlameContent($filename)
}
$rawContent = stream_get_contents($handle);
- fclose($handle);
+ pclose($handle);
$blames = explode("\n", $rawContent);
chdir($cwd);
diff --git a/src/Reports/Hgblame.php b/src/Reports/Hgblame.php
index b0af6643fd..f88a06836b 100644
--- a/src/Reports/Hgblame.php
+++ b/src/Reports/Hgblame.php
@@ -97,7 +97,7 @@ protected function getBlameContent($filename)
}
$rawContent = stream_get_contents($handle);
- fclose($handle);
+ pclose($handle);
$blames = explode("\n", $rawContent);
chdir($cwd);
diff --git a/src/Reports/Info.php b/src/Reports/Info.php
index 3181bcc546..fa0698efdf 100644
--- a/src/Reports/Info.php
+++ b/src/Reports/Info.php
@@ -23,10 +23,10 @@ class Info implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Json.php b/src/Reports/Json.php
index 59d8f305e5..f7aedb30a5 100644
--- a/src/Reports/Json.php
+++ b/src/Reports/Json.php
@@ -23,10 +23,10 @@ class Json implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Junit.php b/src/Reports/Junit.php
index d3ede61d47..0b59604ae0 100644
--- a/src/Reports/Junit.php
+++ b/src/Reports/Junit.php
@@ -24,10 +24,10 @@ class Junit implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Notifysend.php b/src/Reports/Notifysend.php
index 5ffc5baf5d..71aed108ed 100644
--- a/src/Reports/Notifysend.php
+++ b/src/Reports/Notifysend.php
@@ -18,6 +18,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Util\Common;
class Notifysend implements Report
{
@@ -58,7 +59,7 @@ public function __construct()
{
$path = Config::getExecutablePath('notifysend');
if ($path !== null) {
- $this->path = escapeshellcmd($path);
+ $this->path = Common::escapeshellcmd($path);
}
$timeout = Config::getConfigData('notifysend_timeout');
@@ -87,10 +88,10 @@ public function __construct()
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Report.php b/src/Reports/Report.php
index 38f8a6298c..fd60185e94 100644
--- a/src/Reports/Report.php
+++ b/src/Reports/Report.php
@@ -22,10 +22,10 @@ interface Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Source.php b/src/Reports/Source.php
index ce8c3cfec1..44f0d8748e 100644
--- a/src/Reports/Source.php
+++ b/src/Reports/Source.php
@@ -23,10 +23,10 @@ class Source implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Summary.php b/src/Reports/Summary.php
index 8fe18e7640..cd5ca69476 100644
--- a/src/Reports/Summary.php
+++ b/src/Reports/Summary.php
@@ -23,10 +23,10 @@ class Summary implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Svnblame.php b/src/Reports/Svnblame.php
index f4719fe5dc..a7f65e154b 100644
--- a/src/Reports/Svnblame.php
+++ b/src/Reports/Svnblame.php
@@ -61,7 +61,7 @@ protected function getBlameContent($filename)
}
$rawContent = stream_get_contents($handle);
- fclose($handle);
+ pclose($handle);
$blames = explode("\n", $rawContent);
diff --git a/src/Reports/VersionControl.php b/src/Reports/VersionControl.php
index 0f414567dc..e8e399a7dd 100644
--- a/src/Reports/VersionControl.php
+++ b/src/Reports/VersionControl.php
@@ -31,10 +31,10 @@ abstract class VersionControl implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Reports/Xml.php b/src/Reports/Xml.php
index 066383debe..c748dca726 100644
--- a/src/Reports/Xml.php
+++ b/src/Reports/Xml.php
@@ -23,10 +23,10 @@ class Xml implements Report
* and FALSE if it ignored the file. Returning TRUE indicates that the file and
* its data should be counted in the grand totals.
*
- * @param array $report Prepared report data.
- * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on.
- * @param bool $showSources Show sources?
- * @param int $width Maximum allowed line width.
+ * @param array $report Prepared report data.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on.
+ * @param bool $showSources Show sources?
+ * @param int $width Maximum allowed line width.
*
* @return bool
*/
diff --git a/src/Ruleset.php b/src/Ruleset.php
index 3220a18f22..5ba0d180fc 100644
--- a/src/Ruleset.php
+++ b/src/Ruleset.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Util;
+use stdClass;
class Ruleset
{
@@ -126,15 +127,9 @@ class Ruleset
*/
public function __construct(Config $config)
{
- // Ignore sniff restrictions if caching is on.
- $restrictions = [];
- $exclusions = [];
- if ($config->cache === false) {
- $restrictions = $config->sniffs;
- $exclusions = $config->exclude;
- }
-
$this->config = $config;
+ $restrictions = $config->sniffs;
+ $exclusions = $config->exclude;
$sniffs = [];
$standardPaths = [];
@@ -200,6 +195,12 @@ public function __construct(Config $config)
$sniffs = array_merge($sniffs, $this->processRuleset($standard));
}//end foreach
+ // Ignore sniff restrictions if caching is on.
+ if ($config->cache === true) {
+ $restrictions = [];
+ $exclusions = [];
+ }
+
$sniffRestrictions = [];
foreach ($restrictions as $sniffCode) {
$parts = explode('.', strtolower($sniffCode));
@@ -249,7 +250,12 @@ public function explain()
// one last time and clear the output buffer.
$sniffs[] = '';
- echo PHP_EOL."The $this->name standard contains $sniffCount sniffs".PHP_EOL;
+ $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL;
+ if ($sniffCount !== 1) {
+ $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine);
+ }
+
+ echo $summaryLine;
ob_start();
@@ -726,7 +732,7 @@ private function expandRulesetReference($ref, $rulesetDir, $depth=0)
} else {
// See if this is a whole standard being referenced.
$path = Util\Standards::getInstalledStandardPath($ref);
- if (Util\Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
+ if ($path !== null && Util\Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) {
// If the ruleset exists inside the phar file, use it.
if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) {
$path .= DIRECTORY_SEPARATOR.'ruleset.xml';
@@ -955,6 +961,11 @@ private function processRule($rule, $newSniffs, $depth=0)
if (isset($rule->properties) === true
&& $this->shouldProcessElement($rule->properties) === true
) {
+ $propertyScope = 'standard';
+ if ($code === $ref || substr($ref, -9) === 'Sniff.php') {
+ $propertyScope = 'sniff';
+ }
+
foreach ($rule->properties->property as $prop) {
if ($this->shouldProcessElement($prop) === false) {
continue;
@@ -975,9 +986,9 @@ private function processRule($rule, $newSniffs, $depth=0)
$values = [];
if (isset($prop['extend']) === true
&& (string) $prop['extend'] === 'true'
- && isset($this->ruleset[$code]['properties'][$name]) === true
+ && isset($this->ruleset[$code]['properties'][$name]['value']) === true
) {
- $values = $this->ruleset[$code]['properties'][$name];
+ $values = $this->ruleset[$code]['properties'][$name]['value'];
}
if (isset($prop->element) === true) {
@@ -1012,7 +1023,10 @@ private function processRule($rule, $newSniffs, $depth=0)
}
}//end if
- $this->ruleset[$code]['properties'][$name] = $values;
+ $this->ruleset[$code]['properties'][$name] = [
+ 'value' => $values,
+ 'scope' => $propertyScope,
+ ];
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo str_repeat("\t", $depth);
echo "\t\t=> array property \"$name\" set to \"$printValue\"";
@@ -1023,7 +1037,10 @@ private function processRule($rule, $newSniffs, $depth=0)
echo PHP_EOL;
}
} else {
- $this->ruleset[$code]['properties'][$name] = (string) $prop['value'];
+ $this->ruleset[$code]['properties'][$name] = [
+ 'value' => (string) $prop['value'],
+ 'scope' => $propertyScope,
+ ];
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo str_repeat("\t", $depth);
echo "\t\t=> property \"$name\" set to \"".(string) $prop['value'].'"';
@@ -1213,8 +1230,8 @@ public function populateTokenListeners()
// Set custom properties.
if (isset($this->ruleset[$sniffCode]['properties']) === true) {
- foreach ($this->ruleset[$sniffCode]['properties'] as $name => $value) {
- $this->setSniffProperty($sniffClass, $name, $value);
+ foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) {
+ $this->setSniffProperty($sniffClass, $name, $settings);
}
}
@@ -1281,18 +1298,76 @@ public function populateTokenListeners()
*
* @param string $sniffClass The class name of the sniff.
* @param string $name The name of the property to change.
- * @param string $value The new value of the property.
+ * @param array $settings Array with the new value of the property and the scope of the property being set.
*
* @return void
+ *
+ * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When attempting to set a non-existent property on a sniff
+ * which doesn't declare the property or explicitly supports
+ * dynamic properties.
*/
- public function setSniffProperty($sniffClass, $name, $value)
+ public function setSniffProperty($sniffClass, $name, $settings)
{
// Setting a property for a sniff we are not using.
if (isset($this->sniffs[$sniffClass]) === false) {
return;
}
- $name = trim($name);
+ $name = trim($name);
+ $propertyName = $name;
+ if (substr($propertyName, -2) === '[]') {
+ $propertyName = substr($propertyName, 0, -2);
+ }
+
+ /*
+ * BC-compatibility layer for $settings using the pre-PHPCS 3.8.0 format.
+ *
+ * Prior to PHPCS 3.8.0, `$settings` was expected to only contain the new _value_
+ * for the property (which could be an array).
+ * Since PHPCS 3.8.0, `$settings` is expected to be an array with two keys: 'scope'
+ * and 'value', where 'scope' indicates whether the property should be set to the given 'value'
+ * for one individual sniff or for all sniffs in a standard.
+ *
+ * This BC-layer is only for integrations with PHPCS which may call this method directly
+ * and will be removed in PHPCS 4.0.0.
+ */
+
+ if (is_array($settings) === false
+ || isset($settings['scope'], $settings['value']) === false
+ ) {
+ // This will be an "old" format value.
+ $settings = [
+ 'value' => $settings,
+ 'scope' => 'standard',
+ ];
+
+ trigger_error(
+ __FUNCTION__.': the format of the $settings parameter has changed from (mixed) $value to array(\'scope\' => \'sniff|standard\', \'value\' => $value). Please update your integration code. See PR #3629 for more information.',
+ E_USER_DEPRECATED
+ );
+ }
+
+ $isSettable = false;
+ $sniffObject = $this->sniffs[$sniffClass];
+ if (property_exists($sniffObject, $propertyName) === true
+ || ($sniffObject instanceof stdClass) === true
+ || method_exists($sniffObject, '__set') === true
+ ) {
+ $isSettable = true;
+ }
+
+ if ($isSettable === false) {
+ if ($settings['scope'] === 'sniff') {
+ $notice = "Ruleset invalid. Property \"$propertyName\" does not exist on sniff ";
+ $notice .= array_search($sniffClass, $this->sniffCodes, true);
+ throw new RuntimeException($notice);
+ }
+
+ return;
+ }
+
+ $value = $settings['value'];
+
if (is_string($value) === true) {
$value = trim($value);
}
@@ -1307,7 +1382,7 @@ public function setSniffProperty($sniffClass, $name, $value)
} else if ($value === 'false') {
$value = false;
} else if (substr($name, -2) === '[]') {
- $name = substr($name, 0, -2);
+ $name = $propertyName;
$values = [];
if ($value !== null) {
foreach (explode(',', $value) as $val) {
@@ -1323,7 +1398,7 @@ public function setSniffProperty($sniffClass, $name, $value)
$value = $values;
}
- $this->sniffs[$sniffClass]->$name = $value;
+ $sniffObject->$name = $value;
}//end setSniffProperty()
diff --git a/src/Runner.php b/src/Runner.php
index bbed3c9d34..2f21edf482 100644
--- a/src/Runner.php
+++ b/src/Runner.php
@@ -53,6 +53,8 @@ class Runner
*/
public function runPHPCS()
{
+ $this->registerOutOfMemoryShutdownMessage('phpcs');
+
try {
Util\Timing::startTiming();
Runner::checkRequirements();
@@ -153,6 +155,8 @@ public function runPHPCS()
*/
public function runPHPCBF()
{
+ $this->registerOutOfMemoryShutdownMessage('phpcbf');
+
if (defined('PHP_CODESNIFFER_CBF') === false) {
define('PHP_CODESNIFFER_CBF', true);
}
@@ -232,7 +236,7 @@ public function runPHPCBF()
/**
* Exits if the minimum requirements of PHP_CodeSniffer are not met.
*
- * @return array
+ * @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If the requirements are not met.
*/
public function checkRequirements()
@@ -291,7 +295,7 @@ public function init()
// Ensure this option is enabled or else line endings will not always
// be detected properly for files created on a Mac with the /r line ending.
- ini_set('auto_detect_line_endings', true);
+ @ini_set('auto_detect_line_endings', true);
// Disable the PCRE JIT as this caused issues with parallel running.
ini_set('pcre.jit', false);
@@ -460,10 +464,7 @@ private function run()
if ($pid === -1) {
throw new RuntimeException('Failed to create child process');
} else if ($pid !== 0) {
- $childProcs[] = [
- 'pid' => $pid,
- 'out' => $childOutFilename,
- ];
+ $childProcs[$pid] = $childOutFilename;
} else {
// Move forward to the start of the batch.
$todo->rewind();
@@ -487,6 +488,7 @@ private function run()
$file = $todo->current();
if ($file->ignored === true) {
+ $todo->next();
continue;
}
@@ -535,7 +537,7 @@ private function run()
$output .= ";\n?".'>';
file_put_contents($childOutFilename, $output);
- exit($pid);
+ exit();
}//end if
}//end for
@@ -650,6 +652,39 @@ public function processFile($file)
}
} catch (\Exception $e) {
$error = 'An error occurred during processing; checking has been aborted. The error message was: '.$e->getMessage();
+
+ // Determine which sniff caused the error.
+ $sniffStack = null;
+ $nextStack = null;
+ foreach ($e->getTrace() as $step) {
+ if (isset($step['file']) === false) {
+ continue;
+ }
+
+ if (empty($sniffStack) === false) {
+ $nextStack = $step;
+ break;
+ }
+
+ if (substr($step['file'], -9) === 'Sniff.php') {
+ $sniffStack = $step;
+ continue;
+ }
+ }
+
+ if (empty($sniffStack) === false) {
+ if (empty($nextStack) === false
+ && isset($nextStack['class']) === true
+ && substr($nextStack['class'], -5) === 'Sniff'
+ ) {
+ $sniffCode = Common::getSniffCode($nextStack['class']);
+ } else {
+ $sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1);
+ }
+
+ $error .= sprintf(PHP_EOL.'The error originated in the %s sniff on line %s.', $sniffCode, $sniffStack['line']);
+ }
+
$file->addErrorOnLine($error, 1, 'Internal.Exception');
}//end try
@@ -718,54 +753,61 @@ private function processChildProcs($childProcs)
$success = true;
while (count($childProcs) > 0) {
- foreach ($childProcs as $key => $procData) {
- $res = pcntl_waitpid($procData['pid'], $status, WNOHANG);
- if ($res === $procData['pid']) {
- if (file_exists($procData['out']) === true) {
- include $procData['out'];
-
- unlink($procData['out']);
- unset($childProcs[$key]);
-
- $numProcessed++;
-
- if (isset($childOutput) === false) {
- // The child process died, so the run has failed.
- $file = new DummyFile(null, $this->ruleset, $this->config);
- $file->setErrorCounts(1, 0, 0, 0);
- $this->printProgress($file, $totalBatches, $numProcessed);
- $success = false;
- continue;
- }
+ $pid = pcntl_waitpid(0, $status);
+ if ($pid <= 0) {
+ continue;
+ }
- $this->reporter->totalFiles += $childOutput['totalFiles'];
- $this->reporter->totalErrors += $childOutput['totalErrors'];
- $this->reporter->totalWarnings += $childOutput['totalWarnings'];
- $this->reporter->totalFixable += $childOutput['totalFixable'];
- $this->reporter->totalFixed += $childOutput['totalFixed'];
+ $childProcessStatus = pcntl_wexitstatus($status);
+ if ($childProcessStatus !== 0) {
+ $success = false;
+ }
- if (isset($debugOutput) === true) {
- echo $debugOutput;
- }
+ $out = $childProcs[$pid];
+ unset($childProcs[$pid]);
+ if (file_exists($out) === false) {
+ continue;
+ }
- if (isset($childCache) === true) {
- foreach ($childCache as $path => $cache) {
- Cache::set($path, $cache);
- }
- }
+ include $out;
+ unlink($out);
- // Fake a processed file so we can print progress output for the batch.
- $file = new DummyFile(null, $this->ruleset, $this->config);
- $file->setErrorCounts(
- $childOutput['totalErrors'],
- $childOutput['totalWarnings'],
- $childOutput['totalFixable'],
- $childOutput['totalFixed']
- );
- $this->printProgress($file, $totalBatches, $numProcessed);
- }//end if
- }//end if
- }//end foreach
+ $numProcessed++;
+
+ if (isset($childOutput) === false) {
+ // The child process died, so the run has failed.
+ $file = new DummyFile('', $this->ruleset, $this->config);
+ $file->setErrorCounts(1, 0, 0, 0);
+ $this->printProgress($file, $totalBatches, $numProcessed);
+ $success = false;
+ continue;
+ }
+
+ $this->reporter->totalFiles += $childOutput['totalFiles'];
+ $this->reporter->totalErrors += $childOutput['totalErrors'];
+ $this->reporter->totalWarnings += $childOutput['totalWarnings'];
+ $this->reporter->totalFixable += $childOutput['totalFixable'];
+ $this->reporter->totalFixed += $childOutput['totalFixed'];
+
+ if (isset($debugOutput) === true) {
+ echo $debugOutput;
+ }
+
+ if (isset($childCache) === true) {
+ foreach ($childCache as $path => $cache) {
+ Cache::set($path, $cache);
+ }
+ }
+
+ // Fake a processed file so we can print progress output for the batch.
+ $file = new DummyFile('', $this->ruleset, $this->config);
+ $file->setErrorCounts(
+ $childOutput['totalErrors'],
+ $childOutput['totalWarnings'],
+ $childOutput['totalFixable'],
+ $childOutput['totalFixed']
+ );
+ $this->printProgress($file, $totalBatches, $numProcessed);
}//end while
return $success;
@@ -874,7 +916,10 @@ public function printProgress(File $file, $numFiles, $numProcessed)
$percent = round(($numProcessed / $numFiles) * 100);
$padding = (strlen($numFiles) - strlen($numProcessed));
- if ($numProcessed === $numFiles && $numFiles > $numPerLine) {
+ if ($numProcessed === $numFiles
+ && $numFiles > $numPerLine
+ && ($numProcessed % $numPerLine) !== 0
+ ) {
$padding += ($numPerLine - ($numFiles - (floor($numFiles / $numPerLine) * $numPerLine)));
}
@@ -883,4 +928,42 @@ public function printProgress(File $file, $numFiles, $numProcessed)
}//end printProgress()
+ /**
+ * Registers a PHP shutdown function to provide a more informative out of memory error.
+ *
+ * @param string $command The command which was used to initiate the PHPCS run.
+ *
+ * @return void
+ */
+ private function registerOutOfMemoryShutdownMessage($command)
+ {
+ // Allocate all needed memory beforehand as much as possible.
+ $errorMsg = PHP_EOL.'The PHP_CodeSniffer "%1$s" command ran out of memory.'.PHP_EOL;
+ $errorMsg .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime'.PHP_EOL;
+ $errorMsg .= 'using `%1$s -d memory_limit=512M` (replace 512M with the desired memory limit).'.PHP_EOL;
+ $errorMsg = sprintf($errorMsg, $command);
+ $memoryError = 'Allowed memory size of';
+ $errorArray = [
+ 'type' => 42,
+ 'message' => 'Some random dummy string to take up memory and take up some more memory and some more',
+ 'file' => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files',
+ 'line' => 31427,
+ ];
+
+ register_shutdown_function(
+ static function () use (
+ $errorMsg,
+ $memoryError,
+ $errorArray
+ ) {
+ $errorArray = error_get_last();
+ if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) {
+ echo $errorMsg;
+ }
+ }
+ );
+
+ }//end registerOutOfMemoryShutdownMessage()
+
+
}//end class
diff --git a/src/Sniffs/AbstractArraySniff.php b/src/Sniffs/AbstractArraySniff.php
index 141b9a133c..efe9969d8d 100644
--- a/src/Sniffs/AbstractArraySniff.php
+++ b/src/Sniffs/AbstractArraySniff.php
@@ -104,9 +104,9 @@ public function process(File $phpcsFile, $stackPtr)
/**
* Find next separator in array - either: comma or double arrow.
*
- * @param File $phpcsFile The current file being checked.
- * @param int $ptr The position of current token.
- * @param int $arrayEnd The token that ends the array definition.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked.
+ * @param int $ptr The position of current token.
+ * @param int $arrayEnd The token that ends the array definition.
*
* @return int
*/
diff --git a/src/Sniffs/Sniff.php b/src/Sniffs/Sniff.php
index c9d7daea82..3f0fb6a1cf 100644
--- a/src/Sniffs/Sniff.php
+++ b/src/Sniffs/Sniff.php
@@ -45,7 +45,7 @@ public function register();
* is found.
*
* The stackPtr variable indicates where in the stack the token was found.
- * A sniff can acquire information this token, along with all the other
+ * A sniff can acquire information about this token, along with all the other
* tokens within the stack by first acquiring the token stack:
*
*
diff --git a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml
index 6226a3fff6..6fa08be7a4 100644
--- a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml
+++ b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml
@@ -15,6 +15,14 @@ class Foo {
{
+}
+ ]]>
+
+
+
+ {
}
]]>
diff --git a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml
index dd3e773118..aea863695e 100644
--- a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml
+++ b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml
@@ -10,13 +10,10 @@
if (! $someVar || ! $x instanceOf stdClass) {};
]]>
-
+
$someVar || !$x instanceOf stdClass) {};
- ]]>
-
-
- $someVar || !
$x instanceOf stdClass) {};
]]>
diff --git a/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml
new file mode 100644
index 0000000000..c30d26e9d5
--- /dev/null
+++ b/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ AbstractBar
+{
+}
+ ]]>
+
+
+ Bar
+{
+}
+ ]]>
+
+
+
diff --git a/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml
new file mode 100644
index 0000000000..0aa0c76e4d
--- /dev/null
+++ b/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ BarInterface
+{
+}
+ ]]>
+
+
+ Bar
+{
+}
+ ]]>
+
+
+
diff --git a/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml
new file mode 100644
index 0000000000..711867e451
--- /dev/null
+++ b/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ BarTrait
+{
+}
+ ]]>
+
+
+ Bar
+{
+}
+ ]]>
+
+
+
diff --git a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml
index 30e0def93c..338c838933 100644
--- a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml
+++ b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml
@@ -10,13 +10,10 @@
$a = (null !== $extra);
]]>
-
+
-
-
- getTokens();
- $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true);
- $expectedIndent = ($tokens[$first]['column'] - 1 + $this->indent);
+ // Determine how far indented the entire array declaration should be.
+ $ignore = Tokens::$emptyTokens;
+ $ignore[] = T_DOUBLE_ARROW;
+ $ignore[] = T_COMMA;
+ $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
+ $start = $phpcsFile->findStartOfStatement($prev);
+ $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $start, true);
+ $baseIndent = ($tokens[$first]['column'] - 1);
+
+ $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true);
+ $startIndent = ($tokens[$first]['column'] - 1);
+
+ // If the open brace is not indented to at least to the level of the start
+ // of the statement, the sniff will conflict with other sniffs trying to
+ // check indent levels because it's not valid. But we don't enforce exactly
+ // how far indented it should be.
+ if ($startIndent < $baseIndent) {
+ $error = 'Array open brace not indented correctly; expected at least %s spaces but found %s';
+ $data = [
+ $baseIndent,
+ $startIndent,
+ ];
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, 'OpenBraceIncorrect', $data);
+ if ($fix === true) {
+ $padding = str_repeat(' ', $baseIndent);
+ if ($startIndent === 0) {
+ $phpcsFile->fixer->addContentBefore($first, $padding);
+ } else {
+ $phpcsFile->fixer->replaceToken(($first - 1), $padding);
+ }
+ }
+
+ return;
+ }//end if
+
+ $expectedIndent = ($startIndent + $this->indent);
foreach ($indices as $index) {
if (isset($index['index_start']) === true) {
diff --git a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php
index 8374bfd535..3243f8b7dc 100644
--- a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php
+++ b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php
@@ -53,6 +53,7 @@ public function process(File $phpcsFile, $stackPtr)
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
T_NAMESPACE,
T_CLOSE_TAG,
];
diff --git a/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php b/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php
index fb92d82c08..dfd925f918 100644
--- a/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php
+++ b/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php
index 63e831949e..8788a912ea 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php
@@ -60,6 +60,7 @@ public function register()
T_SWITCH,
T_CASE,
T_WHILE,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php
index a37d6cc68f..1c9e400019 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php
@@ -72,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr)
}
$scopeOwner = $tokens[$tokens[$prevNonEmpty]['scope_condition']]['code'];
- if ($scopeOwner === T_CLOSURE || $scopeOwner === T_ANON_CLASS) {
+ if ($scopeOwner === T_CLOSURE || $scopeOwner === T_ANON_CLASS || $scopeOwner === T_MATCH) {
return;
}
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php
index d9248af39c..574bb0ba5a 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php
@@ -50,6 +50,7 @@ public function register()
T_IF,
T_SWITCH,
T_WHILE,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php
index 94eab1f991..cc9958c586 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php
@@ -63,7 +63,7 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
- // Skip for-loop without body.
+ // Skip if statement without body.
if (isset($token['parenthesis_opener']) === false) {
return;
}
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php
index bed67c93c5..02ae2e7a32 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php
@@ -24,7 +24,6 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
-use PHP_CodeSniffer\Util\Tokens;
class UnnecessaryFinalModifierSniff implements Sniff
{
@@ -56,16 +55,13 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
- // Skip for-statements without body.
+ // Skip for statements without body.
if (isset($token['scope_opener']) === false) {
return;
}
- // Fetch previous token.
- $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
-
- // Skip for non final class.
- if ($prev === false || $tokens[$prev]['code'] !== T_FINAL) {
+ if ($phpcsFile->getClassProperties($stackPtr)['is_final'] === false) {
+ // This class is not final so we don't need to check it.
return;
}
@@ -77,6 +73,13 @@ public function process(File $phpcsFile, $stackPtr)
$error = 'Unnecessary FINAL modifier in FINAL class';
$phpcsFile->addWarning($error, $next, 'Found');
}
+
+ // Skip over the contents of functions as those can't contain the `final` keyword anyway.
+ if ($tokens[$next]['code'] === T_FUNCTION
+ && isset($tokens[$next]['scope_closer']) === true
+ ) {
+ $next = $tokens[$next]['scope_closer'];
+ }
}
}//end process()
diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php
index 427f509cae..58aa29fe85 100644
--- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php
+++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php
@@ -23,6 +23,13 @@
class UnusedFunctionParameterSniff implements Sniff
{
+ /**
+ * The list of class type hints which will be ignored.
+ *
+ * @var array
+ */
+ public $ignoreTypeHints = [];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -83,12 +90,24 @@ public function process(File $phpcsFile, $stackPtr)
}
foreach ($methodParams as $param) {
+ if (isset($param['property_visibility']) === true) {
+ // Ignore constructor property promotion.
+ continue;
+ }
+
$params[$param['name']] = $stackPtr;
}
$next = ++$token['scope_opener'];
$end = --$token['scope_closer'];
+ // Check the end token for arrow functions as
+ // they can end at a content token due to not having
+ // a clearly defined closing token.
+ if ($token['code'] === T_FN) {
+ ++$end;
+ }
+
$foundContent = false;
$validTokens = [
T_HEREDOC => T_HEREDOC,
@@ -191,6 +210,10 @@ public function process(File $phpcsFile, $stackPtr)
// If there is only one parameter and it is unused, no need for additional errorcode toggling logic.
if ($methodParamsCount === 1) {
foreach ($params as $paramName => $position) {
+ if (in_array($methodParams[0]['type_hint'], $this->ignoreTypeHints, true) === true) {
+ continue;
+ }
+
$data = [$paramName];
$phpcsFile->addWarning($error, $position, $errorCode, $data);
}
@@ -207,6 +230,7 @@ public function process(File $phpcsFile, $stackPtr)
$errorInfo[$methodParams[$i]['name']] = [
'position' => $params[$methodParams[$i]['name']],
'errorcode' => $errorCode.'BeforeLastUsed',
+ 'typehint' => $methodParams[$i]['type_hint'],
];
}
} else {
@@ -216,14 +240,19 @@ public function process(File $phpcsFile, $stackPtr)
$errorInfo[$methodParams[$i]['name']] = [
'position' => $params[$methodParams[$i]['name']],
'errorcode' => $errorCode.'AfterLastUsed',
+ 'typehint' => $methodParams[$i]['type_hint'],
];
}
}
- }
+ }//end for
if (count($errorInfo) > 0) {
$errorInfo = array_reverse($errorInfo);
foreach ($errorInfo as $paramName => $info) {
+ if (in_array($info['typehint'], $this->ignoreTypeHints, true) === true) {
+ continue;
+ }
+
$data = [$paramName];
$phpcsFile->addWarning($error, $info['position'], $info['errorcode'], $data);
}
diff --git a/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php b/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php
index d48a477ce1..a67a6a798c 100644
--- a/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php
+++ b/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php
@@ -142,8 +142,10 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- if ($tokens[$nextNonEmpty]['code'] === T_COLON) {
- // Alternative control structure.
+ if ($tokens[$nextNonEmpty]['code'] === T_OPEN_CURLY_BRACKET
+ || $tokens[$nextNonEmpty]['code'] === T_COLON
+ ) {
+ // T_CLOSE_CURLY_BRACKET missing, or alternative control structure with
// T_END... missing. Either live coding, parse error or end
// tag in short open tags and scan run with short_open_tag=Off.
// Bow out completely as any further detection will be unreliable
@@ -208,7 +210,10 @@ public function process(File $phpcsFile, $stackPtr)
if (isset($tokens[$end]['scope_opener']) === true) {
$type = $tokens[$end]['code'];
$end = $tokens[$end]['scope_closer'];
- if ($type === T_DO || $type === T_IF || $type === T_ELSEIF || $type === T_TRY) {
+ if ($type === T_DO
+ || $type === T_IF || $type === T_ELSEIF
+ || $type === T_TRY || $type === T_CATCH || $type === T_FINALLY
+ ) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
if ($next === false) {
break;
@@ -225,15 +230,20 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}
+ // Account for TRY... CATCH/FINALLY statements.
+ if (($type === T_TRY
+ || $type === T_CATCH
+ || $type === T_FINALLY)
+ && ($nextType === T_CATCH
+ || $nextType === T_FINALLY)
+ ) {
+ continue;
+ }
+
// Account for DO... WHILE conditions.
if ($type === T_DO && $nextType === T_WHILE) {
$end = $phpcsFile->findNext(T_SEMICOLON, ($next + 1));
}
-
- // Account for TRY... CATCH statements.
- if ($type === T_TRY && $nextType === T_CATCH) {
- $end = $tokens[$next]['scope_closer'];
- }
} else if ($type === T_CLOSURE) {
// There should be a semicolon after the closing brace.
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
diff --git a/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php b/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php
index d299dc845c..81284787a2 100644
--- a/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php
+++ b/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class CSSLintSniff implements Sniff
{
@@ -54,7 +55,7 @@ public function process(File $phpcsFile, $stackPtr)
$fileName = $phpcsFile->getFilename();
- $cmd = escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1';
+ $cmd = Common::escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1';
exec($cmd, $output, $retval);
if (is_array($output) === false) {
diff --git a/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php b/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php
index f6b6b729bc..19204718b0 100644
--- a/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php
+++ b/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class ClosureLinterSniff implements Sniff
{
@@ -71,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr)
$fileName = $phpcsFile->getFilename();
- $lintPath = escapeshellcmd($lintPath);
+ $lintPath = Common::escapeshellcmd($lintPath);
$cmd = $lintPath.' --nosummary --notime --unix_mode '.escapeshellarg($fileName);
exec($cmd, $output, $retval);
diff --git a/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php b/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php
index 081c3c1ebf..d11d347050 100644
--- a/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php
+++ b/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class ESLintSniff implements Sniff
{
@@ -76,7 +77,7 @@ public function process(File $phpcsFile, $stackPtr)
$eslintOptions[] = '--config '.escapeshellarg($configFile);
}
- $cmd = escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename));
+ $cmd = Common::escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename));
// Execute!
exec($cmd, $stdout, $code);
diff --git a/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php b/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php
index 8e1f18bd0d..738ab67ebe 100644
--- a/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php
+++ b/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class JSHintSniff implements Sniff
{
@@ -51,15 +52,15 @@ public function process(File $phpcsFile, $stackPtr)
{
$rhinoPath = Config::getExecutablePath('rhino');
$jshintPath = Config::getExecutablePath('jshint');
- if ($rhinoPath === null && $jshintPath === null) {
+ if ($jshintPath === null) {
return;
}
$fileName = $phpcsFile->getFilename();
- $jshintPath = escapeshellcmd($jshintPath);
+ $jshintPath = Common::escapeshellcmd($jshintPath);
if ($rhinoPath !== null) {
- $rhinoPath = escapeshellcmd($rhinoPath);
+ $rhinoPath = Common::escapeshellcmd($rhinoPath);
$cmd = "$rhinoPath \"$jshintPath\" ".escapeshellarg($fileName);
exec($cmd, $output, $retval);
diff --git a/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php b/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php
index ee52a1b975..d5375dc5c0 100644
--- a/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php
@@ -34,7 +34,10 @@ class EndFileNewlineSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php b/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php
index 2c7fcd35bc..41c9c74991 100644
--- a/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php
@@ -34,7 +34,10 @@ class EndFileNoNewlineSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php b/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php
index 51534bf377..e1213d5f64 100644
--- a/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php
@@ -23,7 +23,10 @@ class ExecutableFileSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php b/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php
index 763e7863ca..845e1bcfc4 100644
--- a/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php
@@ -41,7 +41,10 @@ class LineEndingsSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php b/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php
index 2f442b1123..a9fd4c5d44 100644
--- a/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php
@@ -23,7 +23,10 @@ class LowercasedFilenameSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php
index 9bcc00c1c2..52d5d84c74 100644
--- a/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php
@@ -39,7 +39,13 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1));
+ $tokens = $phpcsFile->getTokens();
+ $start = ($stackPtr + 1);
+ if (isset($tokens[$stackPtr]['scope_closer']) === true) {
+ $start = ($tokens[$stackPtr]['scope_closer'] + 1);
+ }
+
+ $nextClass = $phpcsFile->findNext($this->register(), $start);
if ($nextClass !== false) {
$error = 'Only one class is allowed in a file';
$phpcsFile->addError($error, $nextClass, 'MultipleFound');
diff --git a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php
index f7cfa8d30b..9a6f5bccd5 100644
--- a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php
@@ -39,7 +39,13 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $nextInterface = $phpcsFile->findNext($this->register(), ($stackPtr + 1));
+ $tokens = $phpcsFile->getTokens();
+ $start = ($stackPtr + 1);
+ if (isset($tokens[$stackPtr]['scope_closer']) === true) {
+ $start = ($tokens[$stackPtr]['scope_closer'] + 1);
+ }
+
+ $nextInterface = $phpcsFile->findNext($this->register(), $start);
if ($nextInterface !== false) {
$error = 'Only one interface is allowed in a file';
$phpcsFile->addError($error, $nextInterface, 'MultipleFound');
diff --git a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php
index d9d71b6996..4d417e069e 100644
--- a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
@@ -43,7 +44,13 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1));
+ $tokens = $phpcsFile->getTokens();
+ $start = ($stackPtr + 1);
+ if (isset($tokens[$stackPtr]['scope_closer']) === true) {
+ $start = ($tokens[$stackPtr]['scope_closer'] + 1);
+ }
+
+ $nextClass = $phpcsFile->findNext($this->register(), $start);
if ($nextClass !== false) {
$error = 'Only one object structure is allowed in a file';
$phpcsFile->addError($error, $nextClass, 'MultipleFound');
diff --git a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php
index dd97da85b4..7ae523f70e 100644
--- a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php
+++ b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php
@@ -39,7 +39,13 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1));
+ $tokens = $phpcsFile->getTokens();
+ $start = ($stackPtr + 1);
+ if (isset($tokens[$stackPtr]['scope_closer']) === true) {
+ $start = ($tokens[$stackPtr]['scope_closer'] + 1);
+ }
+
+ $nextClass = $phpcsFile->findNext($this->register(), $start);
if ($nextClass !== false) {
$error = 'Only one trait is allowed in a file';
$phpcsFile->addError($error, $nextClass, 'MultipleFound');
diff --git a/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php b/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php
index d67bb761e4..6f9ff9f917 100644
--- a/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php
+++ b/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php
@@ -59,16 +59,18 @@ public function process(File $phpcsFile, $stackPtr)
} while ($tokens[$prev]['code'] === T_PHPCS_IGNORE);
// Ignore multiple statements in a FOR condition.
- if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
- foreach ($tokens[$stackPtr]['nested_parenthesis'] as $bracket) {
- if (isset($tokens[$bracket]['parenthesis_owner']) === false) {
- // Probably a closure sitting inside a function call.
- continue;
- }
-
- $owner = $tokens[$bracket]['parenthesis_owner'];
- if ($tokens[$owner]['code'] === T_FOR) {
- return;
+ foreach ([$stackPtr, $prev] as $checkToken) {
+ if (isset($tokens[$checkToken]['nested_parenthesis']) === true) {
+ foreach ($tokens[$checkToken]['nested_parenthesis'] as $bracket) {
+ if (isset($tokens[$bracket]['parenthesis_owner']) === false) {
+ // Probably a closure sitting inside a function call.
+ continue;
+ }
+
+ $owner = $tokens[$bracket]['parenthesis_owner'];
+ if ($tokens[$owner]['code'] === T_FOR) {
+ return;
+ }
}
}
}
diff --git a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
index ab7caa9d48..802e594485 100644
--- a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
+++ b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
@@ -47,6 +47,13 @@ class MultipleStatementAlignmentSniff implements Sniff
*/
public $maxPadding = 1000;
+ /**
+ * Controls which side of the assignment token is used for alignment.
+ *
+ * @var boolean
+ */
+ public $alignAtEnd = true;
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -73,25 +80,6 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $tokens = $phpcsFile->getTokens();
-
- // Ignore assignments used in a condition, like an IF or FOR.
- if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
- // If the parenthesis is on the same line as the assignment,
- // then it should be ignored as it is specifically being grouped.
- $parens = $tokens[$stackPtr]['nested_parenthesis'];
- $lastParen = array_pop($parens);
- if ($tokens[$lastParen]['line'] === $tokens[$stackPtr]['line']) {
- return;
- }
-
- foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) {
- if (isset($tokens[$start]['parenthesis_owner']) === true) {
- return;
- }
- }
- }
-
$lastAssign = $this->checkAlignment($phpcsFile, $stackPtr);
return ($lastAssign + 1);
@@ -113,6 +101,23 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null)
{
$tokens = $phpcsFile->getTokens();
+ // Ignore assignments used in a condition, like an IF or FOR or closure param defaults.
+ if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
+ // If the parenthesis is on the same line as the assignment,
+ // then it should be ignored as it is specifically being grouped.
+ $parens = $tokens[$stackPtr]['nested_parenthesis'];
+ $lastParen = array_pop($parens);
+ if ($tokens[$lastParen]['line'] === $tokens[$stackPtr]['line']) {
+ return $stackPtr;
+ }
+
+ foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) {
+ if (isset($tokens[$start]['parenthesis_owner']) === true) {
+ return $stackPtr;
+ }
+ }
+ }
+
$assignments = [];
$prevAssign = null;
$lastLine = $tokens[$stackPtr]['line'];
@@ -140,11 +145,25 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null)
break;
}
- if (isset($scopes[$tokens[$assign]['code']]) === true
- && isset($tokens[$assign]['scope_opener']) === true
+ if (isset($tokens[$assign]['scope_opener']) === true
&& $tokens[$assign]['level'] === $tokens[$stackPtr]['level']
) {
- break;
+ if (isset($scopes[$tokens[$assign]['code']]) === true) {
+ // This type of scope indicates that the assignment block is over.
+ break;
+ }
+
+ // Skip over the scope block because it is seen as part of the assignment block,
+ // but also process any assignment blocks that are inside as well.
+ $nextAssign = $phpcsFile->findNext($find, ($assign + 1), ($tokens[$assign]['scope_closer'] - 1));
+ if ($nextAssign !== false) {
+ $assign = $this->checkAlignment($phpcsFile, $nextAssign);
+ } else {
+ $assign = $tokens[$assign]['scope_closer'];
+ }
+
+ $lastCode = $assign;
+ continue;
}
if ($assign === $arrayEnd) {
@@ -253,6 +272,10 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null)
// padding length if they aligned with us.
$varEnd = $tokens[($var + 1)]['column'];
$assignLen = $tokens[$assign]['length'];
+ if ($this->alignAtEnd !== true) {
+ $assignLen = 1;
+ }
+
if ($assign !== $stackPtr) {
if ($prevAssign === null) {
// Processing an inner block but no assignments found.
diff --git a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php
index ca9223814b..39fdae9526 100644
--- a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php
+++ b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php
@@ -30,6 +30,7 @@ public function register()
T_UNSET,
T_SELF,
T_STATIC,
+ T_PARENT,
T_VARIABLE,
T_CLOSE_CURLY_BRACKET,
T_CLOSE_PARENTHESIS,
@@ -155,10 +156,13 @@ public function checkSpacing(File $phpcsFile, $stackPtr, $openBracket)
}//end if
if ($tokens[($nextSeparator + 1)]['code'] !== T_WHITESPACE) {
- $error = 'No space found after comma in argument list';
- $fix = $phpcsFile->addFixableError($error, $nextSeparator, 'NoSpaceAfterComma');
- if ($fix === true) {
- $phpcsFile->fixer->addContent($nextSeparator, ' ');
+ // Ignore trailing comma's after last argument as that's outside the scope of this sniff.
+ if (($nextSeparator + 1) !== $closeBracket) {
+ $error = 'No space found after comma in argument list';
+ $fix = $phpcsFile->addFixableError($error, $nextSeparator, 'NoSpaceAfterComma');
+ if ($fix === true) {
+ $phpcsFile->fixer->addContent($nextSeparator, ' ');
+ }
}
} else {
// If there is a newline in the space, then they must be formatting
diff --git a/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php b/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php
index be1464d7ce..ff19526a9b 100644
--- a/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php
+++ b/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php
@@ -139,17 +139,31 @@ public function process(File $phpcsFile, $stackPtr)
} else if ($lineDifference > 1) {
$error = 'Opening brace should be on the line after the declaration; found %s blank line(s)';
$data = [($lineDifference - 1)];
- $fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceSpacing', $data);
- if ($fix === true) {
- for ($i = ($tokens[$stackPtr]['parenthesis_closer'] + 1); $i < $openingBrace; $i++) {
- if ($tokens[$i]['line'] === $braceLine) {
- $phpcsFile->fixer->addNewLineBefore($i);
- break;
+
+ $prevNonWs = $phpcsFile->findPrevious(T_WHITESPACE, ($openingBrace - 1), $closeBracket, true);
+ if ($prevNonWs !== $prev) {
+ // There must be a comment between the end of the function declaration and the open brace.
+ // Report, but don't fix.
+ $phpcsFile->addError($error, $openingBrace, 'BraceSpacing', $data);
+ } else {
+ $fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceSpacing', $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+ for ($i = $openingBrace; $i > $prev; $i--) {
+ if ($tokens[$i]['line'] === $tokens[$openingBrace]['line']) {
+ if ($tokens[$i]['column'] === 1) {
+ $phpcsFile->fixer->addNewLineBefore($i);
+ }
+
+ continue;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
}
- $phpcsFile->fixer->replaceToken($i, '');
+ $phpcsFile->fixer->endChangeset();
}
- }
+ }//end if
}//end if
$ignore = Tokens::$phpcsCommentTokens;
diff --git a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
index df70df221f..9bd0dff3de 100644
--- a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
+++ b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
@@ -71,15 +71,19 @@ public function process(File $phpcsFile, $stackPtr)
// Predicate nodes for PHP.
$find = [
- T_CASE => true,
- T_DEFAULT => true,
- T_CATCH => true,
- T_IF => true,
- T_FOR => true,
- T_FOREACH => true,
- T_WHILE => true,
- T_DO => true,
- T_ELSEIF => true,
+ T_CASE => true,
+ T_DEFAULT => true,
+ T_CATCH => true,
+ T_IF => true,
+ T_FOR => true,
+ T_FOREACH => true,
+ T_WHILE => true,
+ T_ELSEIF => true,
+ T_INLINE_THEN => true,
+ T_COALESCE => true,
+ T_COALESCE_EQUAL => true,
+ T_MATCH_ARROW => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
];
$complexity = 1;
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php
new file mode 100644
index 0000000000..3e3af830d3
--- /dev/null
+++ b/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php
@@ -0,0 +1,60 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
+
+use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Sniffs\Sniff;
+
+class AbstractClassNamePrefixSniff implements Sniff
+{
+
+
+ /**
+ * Registers the tokens that this sniff wants to listen for.
+ *
+ * @return int[]
+ */
+ public function register()
+ {
+ return [T_CLASS];
+
+ }//end register()
+
+
+ /**
+ * Processes this sniff, when one of its tokens is encountered.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ if ($phpcsFile->getClassProperties($stackPtr)['is_abstract'] === false) {
+ // This class is not abstract so we don't need to check it.
+ return;
+ }
+
+ $className = $phpcsFile->getDeclarationName($stackPtr);
+ if ($className === null) {
+ // We are not interested in anonymous classes.
+ return;
+ }
+
+ $prefix = substr($className, 0, 8);
+ if (strtolower($prefix) !== 'abstract') {
+ $phpcsFile->addError('Abstract class names must be prefixed with "Abstract"; found "%s"', $stackPtr, 'Missing', [$className]);
+ }
+
+ }//end process()
+
+
+}//end class
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php
index 64e12689a9..b45112034a 100644
--- a/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php
+++ b/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php
@@ -23,21 +23,23 @@ class CamelCapsFunctionNameSniff extends AbstractScopeSniff
* @var array
*/
protected $magicMethods = [
- 'construct' => true,
- 'destruct' => true,
- 'call' => true,
- 'callstatic' => true,
- 'get' => true,
- 'set' => true,
- 'isset' => true,
- 'unset' => true,
- 'sleep' => true,
- 'wakeup' => true,
- 'tostring' => true,
- 'set_state' => true,
- 'clone' => true,
- 'invoke' => true,
- 'debuginfo' => true,
+ 'construct' => true,
+ 'destruct' => true,
+ 'call' => true,
+ 'callstatic' => true,
+ 'get' => true,
+ 'set' => true,
+ 'isset' => true,
+ 'unset' => true,
+ 'sleep' => true,
+ 'wakeup' => true,
+ 'serialize' => true,
+ 'unserialize' => true,
+ 'tostring' => true,
+ 'invoke' => true,
+ 'set_state' => true,
+ 'clone' => true,
+ 'debuginfo' => true,
];
/**
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php
index b7f5427a22..a41960637c 100644
--- a/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php
+++ b/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php
@@ -66,7 +66,12 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop
return;
}
- $className = strtolower($phpcsFile->getDeclarationName($currScope));
+ $className = $phpcsFile->getDeclarationName($currScope);
+ if (empty($className) === false) {
+ // Not an anonymous class.
+ $className = strtolower($className);
+ }
+
if ($className !== $this->currentClass) {
$this->loadFunctionNamesInScope($phpcsFile, $currScope);
$this->currentClass = $className;
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php
new file mode 100644
index 0000000000..c5dc34d489
--- /dev/null
+++ b/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php
@@ -0,0 +1,54 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
+
+use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Sniffs\Sniff;
+
+class InterfaceNameSuffixSniff implements Sniff
+{
+
+
+ /**
+ * Registers the tokens that this sniff wants to listen for.
+ *
+ * @return int[]
+ */
+ public function register()
+ {
+ return [T_INTERFACE];
+
+ }//end register()
+
+
+ /**
+ * Processes this sniff, when one of its tokens is encountered.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $interfaceName = $phpcsFile->getDeclarationName($stackPtr);
+ if ($interfaceName === null) {
+ return;
+ }
+
+ $suffix = substr($interfaceName, -9);
+ if (strtolower($suffix) !== 'interface') {
+ $phpcsFile->addError('Interface names must be suffixed with "Interface"; found "%s"', $stackPtr, 'Missing', [$interfaceName]);
+ }
+
+ }//end process()
+
+
+}//end class
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php
new file mode 100644
index 0000000000..4e3b211dff
--- /dev/null
+++ b/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php
@@ -0,0 +1,54 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions;
+
+use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Sniffs\Sniff;
+
+class TraitNameSuffixSniff implements Sniff
+{
+
+
+ /**
+ * Registers the tokens that this sniff wants to listen for.
+ *
+ * @return int[]
+ */
+ public function register()
+ {
+ return [T_TRAIT];
+
+ }//end register()
+
+
+ /**
+ * Processes this sniff, when one of its tokens is encountered.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $traitName = $phpcsFile->getDeclarationName($stackPtr);
+ if ($traitName === null) {
+ return;
+ }
+
+ $suffix = substr($traitName, -5);
+ if (strtolower($suffix) !== 'trait') {
+ $phpcsFile->addError('Trait names must be suffixed with "Trait"; found "%s"', $stackPtr, 'Missing', [$traitName]);
+ }
+
+ }//end process()
+
+
+}//end class
diff --git a/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php
index e4173d98d0..db50eb56bb 100644
--- a/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php
+++ b/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php
@@ -83,6 +83,7 @@ public function process(File $phpcsFile, $stackPtr)
$prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR
|| $tokens[$prev]['code'] === T_DOUBLE_COLON
+ || $tokens[$prev]['code'] === T_NULLSAFE_OBJECT_OPERATOR
) {
return;
}
diff --git a/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php b/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php
index e59ed5ffa7..d03bf8abe1 100644
--- a/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php
@@ -23,7 +23,10 @@ class ClosingPHPTagSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php
index d5c7ddb724..1c2b1aee93 100644
--- a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php
@@ -31,8 +31,9 @@ public function register()
/**
* Processes this sniff, when one of its tokens is encountered.
*
- * @param File $phpcsFile The file being scanned.
- * @param int $stackPtr The position of the current token in the stack passed in $tokens.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the stack
+ * passed in $tokens.
*
* @return void
*/
diff --git a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php
index d8b8bcf498..8717189f0e 100644
--- a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php
@@ -120,18 +120,19 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$ignore = [
- T_DOUBLE_COLON => true,
- T_OBJECT_OPERATOR => true,
- T_FUNCTION => true,
- T_CONST => true,
- T_PUBLIC => true,
- T_PRIVATE => true,
- T_PROTECTED => true,
- T_AS => true,
- T_NEW => true,
- T_INSTEADOF => true,
- T_NS_SEPARATOR => true,
- T_IMPLEMENTS => true,
+ T_DOUBLE_COLON => true,
+ T_OBJECT_OPERATOR => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
+ T_FUNCTION => true,
+ T_CONST => true,
+ T_PUBLIC => true,
+ T_PRIVATE => true,
+ T_PROTECTED => true,
+ T_AS => true,
+ T_NEW => true,
+ T_INSTEADOF => true,
+ T_NS_SEPARATOR => true,
+ T_IMPLEMENTS => true,
];
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
@@ -162,6 +163,11 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
+ if (empty($tokens[$stackPtr]['nested_attributes']) === false) {
+ // Class instantiation in attribute, not function call.
+ return;
+ }
+
$function = strtolower($tokens[$stackPtr]['content']);
$pattern = null;
diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php
index 8b8c76e922..4376daa9dd 100644
--- a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php
@@ -11,6 +11,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
class LowerCaseConstantSniff implements Sniff
{
@@ -25,6 +26,17 @@ class LowerCaseConstantSniff implements Sniff
'JS',
];
+ /**
+ * The tokens this sniff is targetting.
+ *
+ * @var array
+ */
+ private $targets = [
+ T_TRUE => T_TRUE,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
+ ];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -33,11 +45,14 @@ class LowerCaseConstantSniff implements Sniff
*/
public function register()
{
- return [
- T_TRUE,
- T_FALSE,
- T_NULL,
- ];
+ $targets = $this->targets;
+
+ // Register function keywords to filter out type declarations.
+ $targets[] = T_FUNCTION;
+ $targets[] = T_CLOSURE;
+ $targets[] = T_FN;
+
+ return $targets;
}//end register()
@@ -52,10 +67,89 @@ public function register()
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ // Handle function declarations separately as they may contain the keywords in type declarations.
+ if ($tokens[$stackPtr]['code'] === T_FUNCTION
+ || $tokens[$stackPtr]['code'] === T_CLOSURE
+ || $tokens[$stackPtr]['code'] === T_FN
+ ) {
+ if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) {
+ return;
+ }
+
+ $end = $tokens[$stackPtr]['parenthesis_closer'];
+ if (isset($tokens[$stackPtr]['scope_opener']) === true) {
+ $end = $tokens[$stackPtr]['scope_opener'];
+ }
+
+ // Do a quick check if any of the targets exist in the declaration.
+ $found = $phpcsFile->findNext($this->targets, $tokens[$stackPtr]['parenthesis_opener'], $end);
+ if ($found === false) {
+ // Skip forward, no need to examine these tokens again.
+ return $end;
+ }
+
+ // Handle the whole function declaration in one go.
+ $params = $phpcsFile->getMethodParameters($stackPtr);
+ foreach ($params as $param) {
+ if (isset($param['default_token']) === false) {
+ continue;
+ }
+
+ $paramEnd = $param['comma_token'];
+ if ($param['comma_token'] === false) {
+ $paramEnd = $tokens[$stackPtr]['parenthesis_closer'];
+ }
+
+ for ($i = $param['default_token']; $i < $paramEnd; $i++) {
+ if (isset($this->targets[$tokens[$i]['code']]) === true) {
+ $this->processConstant($phpcsFile, $i);
+ }
+ }
+ }
+
+ // Skip over return type declarations.
+ return $end;
+ }//end if
+
+ // Handle property declarations separately as they may contain the keywords in type declarations.
+ if (isset($tokens[$stackPtr]['conditions']) === true) {
+ $conditions = $tokens[$stackPtr]['conditions'];
+ $lastCondition = end($conditions);
+ if (isset(Tokens::$ooScopeTokens[$lastCondition]) === true) {
+ // This can only be an OO constant or property declaration as methods are handled above.
+ $equals = $phpcsFile->findPrevious(T_EQUAL, ($stackPtr - 1), null, false, null, true);
+ if ($equals !== false) {
+ $this->processConstant($phpcsFile, $stackPtr);
+ }
+
+ return;
+ }
+ }
+
+ // Handle everything else.
+ $this->processConstant($phpcsFile, $stackPtr);
+
+ }//end process()
+
+
+ /**
+ * Processes a non-type declaration constant.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the
+ * stack passed in $tokens.
+ *
+ * @return void
+ */
+ protected function processConstant(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$keyword = $tokens[$stackPtr]['content'];
$expected = strtolower($keyword);
+
if ($keyword !== $expected) {
if ($keyword === strtoupper($keyword)) {
$phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper');
@@ -77,7 +171,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower');
}
- }//end process()
+ }//end processConstant()
}//end class
diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
index 26522b9a2a..e6b4917834 100644
--- a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
@@ -11,7 +11,8 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
-use PHP_CodeSniffer\Util;
+use PHP_CodeSniffer\Util\Common;
+use PHP_CodeSniffer\Util\Tokens;
class LowerCaseKeywordSniff implements Sniff
{
@@ -24,79 +25,21 @@ class LowerCaseKeywordSniff implements Sniff
*/
public function register()
{
- return [
- T_ABSTRACT,
- T_ARRAY,
- T_AS,
- T_BREAK,
- T_CALLABLE,
- T_CASE,
- T_CATCH,
- T_CLASS,
- T_CLONE,
- T_CLOSURE,
- T_CONST,
- T_CONTINUE,
- T_DECLARE,
- T_DEFAULT,
- T_DO,
- T_ECHO,
- T_ELSE,
- T_ELSEIF,
- T_EMPTY,
- T_ENDDECLARE,
- T_ENDFOR,
- T_ENDFOREACH,
- T_ENDIF,
- T_ENDSWITCH,
- T_ENDWHILE,
- T_EVAL,
- T_EXIT,
- T_EXTENDS,
- T_FINAL,
- T_FINALLY,
- T_FN,
- T_FOR,
- T_FOREACH,
- T_FUNCTION,
- T_GLOBAL,
- T_GOTO,
- T_IF,
- T_IMPLEMENTS,
- T_INCLUDE,
- T_INCLUDE_ONCE,
- T_INSTANCEOF,
- T_INSTEADOF,
- T_INTERFACE,
- T_ISSET,
- T_LIST,
- T_LOGICAL_AND,
- T_LOGICAL_OR,
- T_LOGICAL_XOR,
- T_NAMESPACE,
- T_NEW,
- T_PARENT,
- T_PRINT,
- T_PRIVATE,
- T_PROTECTED,
- T_PUBLIC,
- T_REQUIRE,
- T_REQUIRE_ONCE,
- T_RETURN,
- T_SELF,
- T_STATIC,
- T_SWITCH,
- T_THROW,
- T_TRAIT,
- T_TRY,
- T_UNSET,
- T_USE,
- T_VAR,
- T_WHILE,
- T_YIELD,
- T_YIELD_FROM,
+ $targets = Tokens::$contextSensitiveKeywords;
+ $targets += [
+ T_CLOSURE => T_CLOSURE,
+ T_EMPTY => T_EMPTY,
+ T_ENUM_CASE => T_ENUM_CASE,
+ T_EVAL => T_EVAL,
+ T_ISSET => T_ISSET,
+ T_MATCH_DEFAULT => T_MATCH_DEFAULT,
+ T_PARENT => T_PARENT,
+ T_SELF => T_SELF,
+ T_UNSET => T_UNSET,
];
+ return $targets;
+
}//end register()
@@ -120,7 +63,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->recordMetric($stackPtr, 'PHP keyword case', 'mixed');
}
- $messageKeyword = Util\Common::prepareForOutput($keyword);
+ $messageKeyword = Common::prepareForOutput($keyword);
$error = 'PHP keywords must be lowercase; expected "%s" but found "%s"';
$data = [
diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
index 27ef5ecac9..4d463f28fb 100644
--- a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php
@@ -9,6 +9,7 @@
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP;
+use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
@@ -16,6 +17,30 @@
class LowerCaseTypeSniff implements Sniff
{
+ /**
+ * Native types supported by PHP.
+ *
+ * @var array
+ */
+ private $phpTypes = [
+ 'self' => true,
+ 'parent' => true,
+ 'array' => true,
+ 'callable' => true,
+ 'bool' => true,
+ 'float' => true,
+ 'int' => true,
+ 'string' => true,
+ 'iterable' => true,
+ 'void' => true,
+ 'object' => true,
+ 'mixed' => true,
+ 'static' => true,
+ 'false' => true,
+ 'null' => true,
+ 'never' => true,
+ ];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -27,6 +52,8 @@ public function register()
$tokens = Tokens::$castTokens;
$tokens[] = T_FUNCTION;
$tokens[] = T_CLOSURE;
+ $tokens[] = T_FN;
+ $tokens[] = T_VARIABLE;
return $tokens;
}//end register()
@@ -47,76 +74,85 @@ public function process(File $phpcsFile, $stackPtr)
if (isset(Tokens::$castTokens[$tokens[$stackPtr]['code']]) === true) {
// A cast token.
- if (strtolower($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) {
- if ($tokens[$stackPtr]['content'] === strtoupper($tokens[$stackPtr]['content'])) {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper');
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed');
- }
+ $this->processType(
+ $phpcsFile,
+ $stackPtr,
+ $tokens[$stackPtr]['content'],
+ 'PHP type casts must be lowercase; expected "%s" but found "%s"',
+ 'TypeCastFound'
+ );
+
+ return;
+ }
+
+ /*
+ * Check property types.
+ */
- $error = 'PHP type casts must be lowercase; expected "%s" but found "%s"';
- $data = [
- strtolower($tokens[$stackPtr]['content']),
- $tokens[$stackPtr]['content'],
- ];
+ if ($tokens[$stackPtr]['code'] === T_VARIABLE) {
+ try {
+ $props = $phpcsFile->getMemberProperties($stackPtr);
+ } catch (RuntimeException $e) {
+ // Not an OO property.
+ return;
+ }
+
+ // Strip off potential nullable indication.
+ $type = ltrim($props['type'], '?');
- $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TypeCastFound', $data);
- if ($fix === true) {
- $phpcsFile->fixer->replaceToken($stackPtr, strtolower($tokens[$stackPtr]['content']));
+ if ($type !== '') {
+ $error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"';
+ $errorCode = 'PropertyTypeFound';
+
+ if ($props['type_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($type, '|') !== false) {
+ $this->processUnionType(
+ $phpcsFile,
+ $props['type_token'],
+ $props['type_end_token'],
+ $error,
+ $errorCode
+ );
+ } else if (isset($this->phpTypes[strtolower($type)]) === true) {
+ $this->processType($phpcsFile, $props['type_token'], $type, $error, $errorCode);
}
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower');
- }//end if
+ }
return;
}//end if
- $phpTypes = [
- 'self' => true,
- 'parent' => true,
- 'array' => true,
- 'callable' => true,
- 'bool' => true,
- 'float' => true,
- 'int' => true,
- 'string' => true,
- 'iterable' => true,
- 'void' => true,
- 'object' => true,
- ];
+ /*
+ * Check function return type.
+ */
$props = $phpcsFile->getMethodProperties($stackPtr);
// Strip off potential nullable indication.
- $returnType = ltrim($props['return_type'], '?');
- $returnTypeLower = strtolower($returnType);
-
- if ($returnType !== ''
- && isset($phpTypes[$returnTypeLower]) === true
- ) {
- // A function return type.
- if ($returnTypeLower !== $returnType) {
- if ($returnType === strtoupper($returnType)) {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper');
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed');
- }
+ $returnType = ltrim($props['return_type'], '?');
- $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"';
- $token = $props['return_type_token'];
- $data = [
- $returnTypeLower,
- $returnType,
- ];
+ if ($returnType !== '') {
+ $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"';
+ $errorCode = 'ReturnTypeFound';
- $fix = $phpcsFile->addFixableError($error, $token, 'ReturnTypeFound', $data);
- if ($fix === true) {
- $phpcsFile->fixer->replaceToken($token, $returnTypeLower);
- }
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower');
- }//end if
- }//end if
+ if ($props['return_type_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($returnType, '|') !== false) {
+ $this->processUnionType(
+ $phpcsFile,
+ $props['return_type_token'],
+ $props['return_type_end_token'],
+ $error,
+ $errorCode
+ );
+ } else if (isset($this->phpTypes[strtolower($returnType)]) === true) {
+ $this->processType($phpcsFile, $props['return_type_token'], $returnType, $error, $errorCode);
+ }
+ }
+
+ /*
+ * Check function parameter types.
+ */
$params = $phpcsFile->getMethodParameters($stackPtr);
if (empty($params) === true) {
@@ -125,38 +161,116 @@ public function process(File $phpcsFile, $stackPtr)
foreach ($params as $param) {
// Strip off potential nullable indication.
- $typeHint = ltrim($param['type_hint'], '?');
- $typeHintLower = strtolower($typeHint);
-
- if ($typeHint !== ''
- && isset($phpTypes[$typeHintLower]) === true
- ) {
- // A function return type.
- if ($typeHintLower !== $typeHint) {
- if ($typeHint === strtoupper($typeHint)) {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper');
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed');
- }
-
- $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"';
- $token = $param['type_hint_token'];
- $data = [
- $typeHintLower,
- $typeHint,
- ];
-
- $fix = $phpcsFile->addFixableError($error, $token, 'ParamTypeFound', $data);
- if ($fix === true) {
- $phpcsFile->fixer->replaceToken($token, $typeHintLower);
- }
- } else {
- $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower');
- }//end if
- }//end if
+ $typeHint = ltrim($param['type_hint'], '?');
+
+ if ($typeHint !== '') {
+ $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"';
+ $errorCode = 'ParamTypeFound';
+
+ if ($param['type_hint_token'] === T_TYPE_INTERSECTION) {
+ // Intersection types don't support simple types.
+ } else if (strpos($typeHint, '|') !== false) {
+ $this->processUnionType(
+ $phpcsFile,
+ $param['type_hint_token'],
+ $param['type_hint_end_token'],
+ $error,
+ $errorCode
+ );
+ } else if (isset($this->phpTypes[strtolower($typeHint)]) === true) {
+ $this->processType($phpcsFile, $param['type_hint_token'], $typeHint, $error, $errorCode);
+ }
+ }
}//end foreach
}//end process()
+ /**
+ * Processes a union type declaration.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $typeDeclStart The position of the start of the type token.
+ * @param int $typeDeclEnd The position of the end of the type token.
+ * @param string $error Error message template.
+ * @param string $errorCode The error code.
+ *
+ * @return void
+ */
+ protected function processUnionType(File $phpcsFile, $typeDeclStart, $typeDeclEnd, $error, $errorCode)
+ {
+ $tokens = $phpcsFile->getTokens();
+ $current = $typeDeclStart;
+
+ do {
+ $endOfType = $phpcsFile->findNext(T_TYPE_UNION, $current, $typeDeclEnd);
+ if ($endOfType === false) {
+ // This must be the last type in the union.
+ $endOfType = ($typeDeclEnd + 1);
+ }
+
+ $hasNsSep = $phpcsFile->findNext(T_NS_SEPARATOR, $current, $endOfType);
+ if ($hasNsSep !== false) {
+ // Multi-token class based type. Ignore.
+ $current = ($endOfType + 1);
+ continue;
+ }
+
+ // Type consisting of a single token.
+ $startOfType = $phpcsFile->findNext(Tokens::$emptyTokens, $current, $endOfType, true);
+ if ($startOfType === false) {
+ // Parse error.
+ return;
+ }
+
+ $type = $tokens[$startOfType]['content'];
+ if (isset($this->phpTypes[strtolower($type)]) === true) {
+ $this->processType($phpcsFile, $startOfType, $type, $error, $errorCode);
+ }
+
+ $current = ($endOfType + 1);
+ } while ($current <= $typeDeclEnd);
+
+ }//end processUnionType()
+
+
+ /**
+ * Processes a type cast or a singular type declaration.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the type token.
+ * @param string $type The type found.
+ * @param string $error Error message template.
+ * @param string $errorCode The error code.
+ *
+ * @return void
+ */
+ protected function processType(File $phpcsFile, $stackPtr, $type, $error, $errorCode)
+ {
+ $typeLower = strtolower($type);
+
+ if ($typeLower === $type) {
+ $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower');
+ return;
+ }
+
+ if ($type === strtoupper($type)) {
+ $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper');
+ } else {
+ $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed');
+ }
+
+ $data = [
+ $typeLower,
+ $type,
+ ];
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->replaceToken($stackPtr, $typeLower);
+ }
+
+ }//end processType()
+
+
}//end class
diff --git a/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php b/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php
index de1912d221..14dfaa7216 100644
--- a/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php
@@ -55,7 +55,7 @@ public function process(File $phpcsFile, $stackPtr)
{
// Prepare the "Found" string to display.
$contextLength = 4;
- $endOfStatement = $phpcsFile->findEndOfStatement($stackPtr, T_COMMA);
+ $endOfStatement = $phpcsFile->findEndOfStatement($stackPtr, [T_COMMA, T_COLON]);
if (($endOfStatement - $stackPtr) < $contextLength) {
$contextLength = ($endOfStatement - $stackPtr);
}
diff --git a/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php b/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php
index b768c4b147..19d58572c6 100644
--- a/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php
@@ -42,10 +42,11 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$ignore = [
- T_DOUBLE_COLON => true,
- T_OBJECT_OPERATOR => true,
- T_FUNCTION => true,
- T_CONST => true,
+ T_DOUBLE_COLON => true,
+ T_OBJECT_OPERATOR => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
+ T_FUNCTION => true,
+ T_CONST => true,
];
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
diff --git a/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php b/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php
index 9ef8c2ea66..85e717bc99 100644
--- a/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class SyntaxSniff implements Sniff
{
@@ -32,7 +33,10 @@ class SyntaxSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
@@ -53,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr)
}
$fileName = escapeshellarg($phpcsFile->getFilename());
- $cmd = escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1";
+ $cmd = Common::escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1";
$output = shell_exec($cmd);
$matches = [];
if (preg_match('/^.*error:(.*) in .* on line ([0-9]+)/m', trim($output), $matches) === 1) {
diff --git a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php
index 54aa07f22a..2740884bb4 100644
--- a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php
@@ -10,30 +10,13 @@
namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP;
use PHP_CodeSniffer\Files\File;
-use PHP_CodeSniffer\Sniffs\Sniff;
-class UpperCaseConstantSniff implements Sniff
+class UpperCaseConstantSniff extends LowerCaseConstantSniff
{
/**
- * Returns an array of tokens this test wants to listen for.
- *
- * @return array
- */
- public function register()
- {
- return [
- T_TRUE,
- T_FALSE,
- T_NULL,
- ];
-
- }//end register()
-
-
- /**
- * Processes this sniff, when one of its tokens is encountered.
+ * Processes a non-type declaration constant.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
@@ -41,11 +24,12 @@ public function register()
*
* @return void
*/
- public function process(File $phpcsFile, $stackPtr)
+ protected function processConstant(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$keyword = $tokens[$stackPtr]['content'];
$expected = strtoupper($keyword);
+
if ($keyword !== $expected) {
if ($keyword === strtolower($keyword)) {
$phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower');
@@ -67,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper');
}
- }//end process()
+ }//end processConstant()
}//end class
diff --git a/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php b/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php
index e25d556b8b..83265b2e47 100644
--- a/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php
+++ b/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php
@@ -34,7 +34,10 @@ class GitMergeConflictSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php
index 7a7c5d8eee..09aea5436e 100644
--- a/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php
+++ b/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php
@@ -58,7 +58,6 @@ public function register()
$this->ignoreTokens[T_CLOSE_SHORT_ARRAY] = T_CLOSE_SHORT_ARRAY;
$this->ignoreTokens[T_USE] = T_USE;
- $this->ignoreTokens[T_DECLARE] = T_DECLARE;
$this->ignoreTokens[T_THROW] = T_THROW;
$this->ignoreTokens[T_YIELD] = T_YIELD;
$this->ignoreTokens[T_YIELD_FROM] = T_YIELD_FROM;
diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php
index 4fe89e767a..bbdbfe0a9a 100644
--- a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php
+++ b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php
@@ -41,7 +41,10 @@ class DisallowSpaceIndentSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php
index a50e9f92bb..2140e55ef5 100644
--- a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php
+++ b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php
@@ -41,7 +41,10 @@ class DisallowTabIndentSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
@@ -73,6 +76,8 @@ public function process(File $phpcsFile, $stackPtr)
T_DOC_COMMENT_WHITESPACE => true,
T_DOC_COMMENT_STRING => true,
T_COMMENT => true,
+ T_END_HEREDOC => true,
+ T_END_NOWDOC => true,
];
for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php
index f231090827..7fd604849d 100644
--- a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php
+++ b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php
@@ -142,12 +142,13 @@ public function process(File $phpcsFile, $stackPtr)
}
}
- $lastOpenTag = $stackPtr;
- $lastCloseTag = null;
- $openScopes = [];
- $adjustments = [];
- $setIndents = [];
- $disableExactEnd = 0;
+ $lastOpenTag = $stackPtr;
+ $lastCloseTag = null;
+ $openScopes = [];
+ $adjustments = [];
+ $setIndents = [];
+ $disableExactStack = [];
+ $disableExactEnd = 0;
$tokens = $phpcsFile->getTokens();
$first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr);
@@ -232,6 +233,7 @@ public function process(File $phpcsFile, $stackPtr)
if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS
&& isset($tokens[$i]['parenthesis_closer']) === true
) {
+ $disableExactStack[$tokens[$i]['parenthesis_closer']] = $tokens[$i]['parenthesis_closer'];
$disableExactEnd = max($disableExactEnd, $tokens[$i]['parenthesis_closer']);
if ($this->debug === true) {
$line = $tokens[$i]['line'];
@@ -337,7 +339,14 @@ public function process(File $phpcsFile, $stackPtr)
echo "\t* open tag is inside condition; using open tag *".PHP_EOL;
}
- $checkIndent = ($tokens[$lastOpenTag]['column'] - 1);
+ $first = $phpcsFile->findFirstOnLine([T_WHITESPACE, T_INLINE_HTML], $lastOpenTag, true);
+ if ($this->debug === true) {
+ $line = $tokens[$first]['line'];
+ $type = $tokens[$first]['type'];
+ echo "\t* first token on line $line is $first ($type) *".PHP_EOL;
+ }
+
+ $checkIndent = ($tokens[$first]['column'] - 1);
if (isset($adjustments[$condition]) === true) {
$checkIndent += $adjustments[$condition];
}
@@ -609,11 +618,11 @@ public function process(File $phpcsFile, $stackPtr)
// Scope closers reset the required indent to the same level as the opening condition.
if (($checkToken !== null
- && isset($openScopes[$checkToken]) === true
+ && (isset($openScopes[$checkToken]) === true
|| (isset($tokens[$checkToken]['scope_condition']) === true
&& isset($tokens[$checkToken]['scope_closer']) === true
&& $tokens[$checkToken]['scope_closer'] === $checkToken
- && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line']))
+ && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])))
|| ($checkToken === null
&& isset($openScopes[$i]) === true)
) {
@@ -796,6 +805,19 @@ public function process(File $phpcsFile, $stackPtr)
) {
$exact = true;
+ if ($disableExactEnd > $checkToken) {
+ foreach ($disableExactStack as $disableExactStackEnd) {
+ if ($disableExactStackEnd < $checkToken) {
+ continue;
+ }
+
+ if ($tokens[$checkToken]['conditions'] === $tokens[$disableExactStackEnd]['conditions']) {
+ $exact = false;
+ break;
+ }
+ }
+ }
+
$lastOpener = null;
if (empty($openScopes) === false) {
end($openScopes);
@@ -849,15 +871,29 @@ public function process(File $phpcsFile, $stackPtr)
&& $tokens[$next]['code'] !== T_VARIABLE
&& $tokens[$next]['code'] !== T_FN)
) {
- if ($this->debug === true) {
- $line = $tokens[$checkToken]['line'];
- $type = $tokens[$checkToken]['type'];
- echo "\t* method prefix ($type) found on line $line; indent set to exact *".PHP_EOL;
+ $isMethodPrefix = true;
+ if (isset($tokens[$checkToken]['nested_parenthesis']) === true) {
+ $parenthesis = array_keys($tokens[$checkToken]['nested_parenthesis']);
+ $deepestOpen = array_pop($parenthesis);
+ if (isset($tokens[$deepestOpen]['parenthesis_owner']) === true
+ && $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
+ ) {
+ // This is constructor property promotion and not a method prefix.
+ $isMethodPrefix = false;
+ }
}
- $exact = true;
- }
- }
+ if ($isMethodPrefix === true) {
+ if ($this->debug === true) {
+ $line = $tokens[$checkToken]['line'];
+ $type = $tokens[$checkToken]['type'];
+ echo "\t* method prefix ($type) found on line $line; indent set to exact *".PHP_EOL;
+ }
+
+ $exact = true;
+ }
+ }//end if
+ }//end if
// JS property indentation has to be exact or else if will break
// things like function and object indentation.
@@ -913,7 +949,8 @@ public function process(File $phpcsFile, $stackPtr)
// Don't perform strict checking on chained method calls since they
// are often covered by custom rules.
if ($checkToken !== null
- && $tokens[$checkToken]['code'] === T_OBJECT_OPERATOR
+ && ($tokens[$checkToken]['code'] === T_OBJECT_OPERATOR
+ || $tokens[$checkToken]['code'] === T_NULLSAFE_OBJECT_OPERATOR)
&& $exact === true
) {
$exact = false;
@@ -946,18 +983,38 @@ public function process(File $phpcsFile, $stackPtr)
}
if ($this->tabIndent === true) {
- $error .= '%s tabs, found %s';
- $data = [
- floor($checkIndent / $this->tabWidth),
- floor($tokenIndent / $this->tabWidth),
- ];
+ $expectedTabs = floor($checkIndent / $this->tabWidth);
+ $foundTabs = floor($tokenIndent / $this->tabWidth);
+ $foundSpaces = ($tokenIndent - ($foundTabs * $this->tabWidth));
+ if ($foundSpaces > 0) {
+ if ($foundTabs > 0) {
+ $error .= '%s tabs, found %s tabs and %s spaces';
+ $data = [
+ $expectedTabs,
+ $foundTabs,
+ $foundSpaces,
+ ];
+ } else {
+ $error .= '%s tabs, found %s spaces';
+ $data = [
+ $expectedTabs,
+ $foundSpaces,
+ ];
+ }
+ } else {
+ $error .= '%s tabs, found %s';
+ $data = [
+ $expectedTabs,
+ $foundTabs,
+ ];
+ }//end if
} else {
$error .= '%s spaces, found %s';
$data = [
$checkIndent,
$tokenIndent,
];
- }
+ }//end if
if ($this->debug === true) {
$line = $tokens[$checkToken]['line'];
@@ -988,15 +1045,17 @@ public function process(File $phpcsFile, $stackPtr)
// Don't check indents exactly between arrays as they tend to have custom rules.
if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) {
+ $disableExactStack[$tokens[$i]['bracket_closer']] = $tokens[$i]['bracket_closer'];
$disableExactEnd = max($disableExactEnd, $tokens[$i]['bracket_closer']);
if ($this->debug === true) {
- $line = $tokens[$i]['line'];
- $type = $tokens[$disableExactEnd]['type'];
+ $line = $tokens[$i]['line'];
+ $type = $tokens[$disableExactEnd]['type'];
+ $endLine = $tokens[$disableExactEnd]['line'];
echo "Opening short array bracket found on line $line".PHP_EOL;
if ($disableExactEnd === $tokens[$i]['bracket_closer']) {
- echo "\t=> disabling exact indent checking until $disableExactEnd ($type)".PHP_EOL;
+ echo "\t=> disabling exact indent checking until $disableExactEnd ($type) on line $endLine".PHP_EOL;
} else {
- echo "\t=> continuing to disable exact indent checking until $disableExactEnd ($type)".PHP_EOL;
+ echo "\t=> continuing to disable exact indent checking until $disableExactEnd ($type) on line $endLine".PHP_EOL;
}
}
}
@@ -1008,7 +1067,6 @@ public function process(File $phpcsFile, $stackPtr)
) {
if ($this->debug === true) {
$line = $tokens[$i]['line'];
- $type = $tokens[$disableExactEnd]['type'];
echo "Here/nowdoc found on line $line".PHP_EOL;
}
@@ -1277,11 +1335,14 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}//end if
- // Closing an anon class or function.
+ // Closing an anon class, closure, or match.
+ // Each may be returned, which can confuse control structures that
+ // use return as a closer, like CASE statements.
if (isset($tokens[$i]['scope_condition']) === true
&& $tokens[$i]['scope_closer'] === $i
&& ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE
- || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS)
+ || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS
+ || $tokens[$tokens[$i]['scope_condition']]['code'] === T_MATCH)
) {
if ($this->debug === true) {
$type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2)));
diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php
index 3146717e84..070bbcd1fb 100644
--- a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php
+++ b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php
@@ -62,6 +62,11 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
+ if ($tokens[$nextNonEmpty]['code'] === T_CLOSE_PARENTHESIS) {
+ // Ignore PHP 8.1 first class callable syntax.
+ return;
+ }
+
if ($this->ignoreNewlines === true
&& $tokens[$stackPtr]['line'] !== $tokens[$nextNonEmpty]['line']
) {
diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc
index 044e4a1727..075fc34c5b 100644
--- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc
+++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc
@@ -63,6 +63,22 @@ $array = [
$c ? $d : $e,
];
+$foo =
+[
+ 'bar' =>
+ [
+ ],
+];
+
+$foo = [
+ 'foo'
+ . 'bar',
+ [
+ 'baz',
+ 'qux',
+ ],
+];
+
// phpcs:set Generic.Arrays.ArrayIndent indent 2
$var = [
@@ -70,3 +86,28 @@ $var = [
2 => 'two',
/* three */ 3 => 'three',
];
+
+// phpcs:set Generic.Arrays.ArrayIndent indent 4
+
+$array = array(
+ match ($test) { 1 => 'a', 2 => 'b' }
+ => 'dynamic keys, woho!',
+);
+
+$array = [
+ match ($test) { 1 => 'a', 2 => 'b' }
+ => 'dynamic keys, woho!',
+];
+
+// Ensure that PHP 8.0 named parameters don't affect the sniff.
+$array = [
+ functionCall(
+ name: $value
+ ),
+];
+
+$array = [
+ functionCall(
+ name: $value
+ ),
+];
diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed
index 404313a4d1..505de5f780 100644
--- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed
@@ -64,6 +64,22 @@ $array = [
$c ? $d : $e,
];
+$foo =
+[
+ 'bar' =>
+ [
+ ],
+];
+
+$foo = [
+ 'foo'
+ . 'bar',
+ [
+ 'baz',
+ 'qux',
+ ],
+];
+
// phpcs:set Generic.Arrays.ArrayIndent indent 2
$var = [
@@ -71,3 +87,28 @@ $var = [
2 => 'two',
/* three */ 3 => 'three',
];
+
+// phpcs:set Generic.Arrays.ArrayIndent indent 4
+
+$array = array(
+ match ($test) { 1 => 'a', 2 => 'b' }
+ => 'dynamic keys, woho!',
+);
+
+$array = [
+ match ($test) { 1 => 'a', 2 => 'b' }
+ => 'dynamic keys, woho!',
+];
+
+// Ensure that PHP 8.0 named parameters don't affect the sniff.
+$array = [
+ functionCall(
+ name: $value
+ ),
+];
+
+$array = [
+ functionCall(
+ name: $value
+ ),
+];
diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php
index 6c94bd1a8f..4861041f62 100644
--- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php
+++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php
@@ -26,19 +26,25 @@ class ArrayIndentUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 14 => 1,
- 15 => 1,
- 17 => 1,
- 30 => 1,
- 31 => 1,
- 33 => 1,
- 41 => 1,
- 62 => 1,
- 63 => 1,
- 69 => 1,
- 70 => 1,
- 71 => 1,
- 72 => 1,
+ 14 => 1,
+ 15 => 1,
+ 17 => 1,
+ 30 => 1,
+ 31 => 1,
+ 33 => 1,
+ 41 => 1,
+ 62 => 1,
+ 63 => 1,
+ 69 => 1,
+ 77 => 1,
+ 78 => 1,
+ 79 => 1,
+ 85 => 1,
+ 86 => 1,
+ 87 => 1,
+ 88 => 1,
+ 98 => 1,
+ 110 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php b/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
index 0297681061..af1d9c9a86 100644
--- a/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
+++ b/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
@@ -35,7 +35,6 @@ public function getErrorList($testFile='')
6 => 1,
7 => 1,
12 => 1,
- 13 => 1,
];
case 'DisallowLongArraySyntaxUnitTest.2.inc':
return [
diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc
index d0c136cbfa..91ab9d3975 100644
--- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc
+++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc
@@ -5,7 +5,10 @@ interface MyInterface {}
interface YourInterface {}
trait MyTrait {}
trait YourTrait {}
+enum MyEnum {}
+enum YourEnum {}
class MyClass {}
interface MyInterface {}
trait MyTrait {}
-?>
\ No newline at end of file
+enum MyEnum {}
+?>
diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc
index e6f92eb130..6829748a5f 100644
--- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc
+++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc
@@ -2,4 +2,5 @@
class MyClass {}
interface MyInterface {}
trait MyTrait {}
+enum MyEnum {}
?>
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php
index b3b3edb502..68982f8686 100644
--- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php
+++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php
@@ -45,9 +45,10 @@ public function getWarningList($testFile='')
switch ($testFile) {
case 'DuplicateClassNameUnitTest.1.inc':
return [
- 8 => 1,
- 9 => 1,
10 => 1,
+ 11 => 1,
+ 12 => 1,
+ 13 => 1,
];
break;
case 'DuplicateClassNameUnitTest.2.inc':
@@ -55,6 +56,7 @@ public function getWarningList($testFile='')
2 => 1,
3 => 1,
4 => 1,
+ 5 => 1,
];
break;
case 'DuplicateClassNameUnitTest.5.inc':
diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc
index 8147b4c73e..fd3abc030e 100644
--- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc
+++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc
@@ -89,3 +89,7 @@ class Test_Class_Bad_G
/*some comment*/
{
}
+
+enum Test_Enum
+{
+}
diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed
index 406eb230fd..a755ae4725 100644
--- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed
@@ -89,3 +89,7 @@ class Test_Class_Bad_G
/*some comment*/ {
}
+
+enum Test_Enum {
+
+}
diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php
index 61cd4c9e8f..c357afc927 100644
--- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php
+++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php
@@ -38,6 +38,7 @@ public function getErrorList()
70 => 1,
79 => 1,
90 => 1,
+ 94 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc
index 49c6cfa77e..9560abbbb1 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc
+++ b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc
@@ -91,3 +91,5 @@ while ( $sample = false ) {}
if ($a = 123) :
endif;
+
+match ($a[0] = 123) {};
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php
index e489f1c35e..3ed57d2207 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php
+++ b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php
@@ -74,6 +74,7 @@ public function getWarningList()
88 => 1,
90 => 1,
92 => 1,
+ 95 => 1,
];
}//end getWarningList()
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc
index e627fd3edf..464b0e3d8c 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc
+++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc
@@ -79,3 +79,8 @@ echo $a{0};
if ($foo) {
;
}
+
+// Do not remove semicolon after match
+$c = match ($a) {
+ 1 => true,
+};
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed
index 2773f97b85..cc0f85e067 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed
@@ -73,3 +73,8 @@ echo $a{0};
if ($foo) {
}
+
+// Do not remove semicolon after match
+$c = match ($a) {
+ 1 => true,
+};
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc
index 83780bce24..210788a1de 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc
+++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc
@@ -69,4 +69,6 @@ try {
// TODO: Handle this exception later :-)
}
-if (true) {} elseif (false) {}
\ No newline at end of file
+if (true) {} elseif (false) {}
+
+match($foo) {};
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php
index 9d89ce2456..4e00ada1ca 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php
+++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php
@@ -39,6 +39,7 @@ public function getErrorList()
64 => 1,
68 => 1,
72 => 2,
+ 74 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc
index 0ba5df778f..026cf48424 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc
+++ b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc
@@ -27,3 +27,8 @@ final class Bar_Foo {
protected function foo() {}
private function Bar() {}
}
+
+final readonly class Foo_Bar {
+ public final function fooBar() {}
+ final protected function fool() {}
+}
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php
index 732f30318c..33bb160a04 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php
+++ b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php
@@ -45,6 +45,8 @@ public function getWarningList()
12 => 1,
15 => 1,
18 => 1,
+ 32 => 1,
+ 33 => 1,
];
}//end getWarningList()
diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc
index 9997e787c5..d800d690fd 100644
--- a/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc
+++ b/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc
@@ -123,3 +123,32 @@ function myCallback($a, $b, $c, $d) {
}
fn ($a, $b, $c) => $b;
+
+// phpcs:set Generic.CodeAnalysis.UnusedFunctionParameter ignoreTypeHints[] Exception
+
+function oneParam(Exception $foo) {
+ return 'foobar';
+}
+
+function moreParamFirst(Exception $foo, LogicException $bar) {
+ return 'foobar' . $bar;
+}
+
+function moreParamSecond(LogicException $bar, Exception $foo) {
+ return 'foobar' . $bar;
+}
+// phpcs:set Generic.CodeAnalysis.UnusedFunctionParameter ignoreTypeHints[]
+
+class ConstructorPropertyPromotionNoContentInMethod {
+ public function __construct(protected int $id) {}
+}
+
+class ConstructorPropertyPromotionWithContentInMethod {
+ public function __construct(protected int $id, $toggle = true) {
+ if ($toggle === true) {
+ doSomething();
+ }
+ }
+}
+
+$found = in_array_cb($needle, $haystack, fn($array, $needle) => $array[2] === $needle);
diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc
index 725e15613f..ce458d8429 100644
--- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc
+++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc
@@ -164,3 +164,12 @@ if ([function() { echo 'hi'; }] === [$foo]
) {
}
+echo match (5 == $num) {
+ true => "true\n",
+ false => "false\n"
+};
+
+echo match ($text) {
+ 'foo' => 10 === $y,
+ 10 === $y => 'bar',
+};
diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php
index e69f3b88b1..c558ae8f6f 100644
--- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php
+++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php
@@ -59,6 +59,9 @@ public function getErrorList()
142 => 1,
156 => 1,
160 => 1,
+ 167 => 1,
+ 173 => 1,
+ 174 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc
index 1a6ab6900a..fb4a7380fb 100644
--- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc
+++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc
@@ -253,3 +253,22 @@ for ($i = 1, $j = 0; $i <= 10; $j += $i, print $i, $i++);
if ($this->valid(fn(): bool => 2 > 1)) {
}
+
+// Issue 3345.
+function testMultiCatch()
+{
+ if (true)
+ try {
+ } catch (\LogicException $e) {
+ } catch (\Exception $e) {
+ }
+}
+
+function testFinally()
+{
+ if (true)
+ try {
+ } catch (\LogicException $e) {
+ } finally {
+ }
+}
diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed
index 4cd8ee4d6c..d80a32652a 100644
--- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed
+++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed
@@ -286,3 +286,24 @@ for ($i = 1, $j = 0; $i <= 10; $j += $i, print $i, $i++);
if ($this->valid(fn(): bool => 2 > 1)) {
}
+
+// Issue 3345.
+function testMultiCatch()
+{
+ if (true) {
+ try {
+ } catch (\LogicException $e) {
+ } catch (\Exception $e) {
+ }
+ }
+}
+
+function testFinally()
+{
+ if (true) {
+ try {
+ } catch (\LogicException $e) {
+ } finally {
+ }
+ }
+}
diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php
index 92888b8581..a9c4f4f8ed 100644
--- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php
+++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php
@@ -71,6 +71,8 @@ public function getErrorList($testFile='InlineControlStructureUnitTest.1.inc')
236 => 1,
238 => 1,
242 => 1,
+ 260 => 1,
+ 269 => 1,
];
case 'InlineControlStructureUnitTest.js':
diff --git a/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php b/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php
index 618465a916..6a10e2ad8b 100644
--- a/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php
+++ b/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php
@@ -25,7 +25,7 @@ protected function shouldSkipTest()
{
$rhinoPath = Config::getExecutablePath('rhino');
$jshintPath = Config::getExecutablePath('jshint');
- if ($rhinoPath === null && $jshintPath === null) {
+ if ($jshintPath === null) {
return true;
}
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc
new file mode 100644
index 0000000000..4f2e47af9c
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc
@@ -0,0 +1 @@
+= 'foo';
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc.fixed
new file mode 100644
index 0000000000..1f87609292
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc.fixed
@@ -0,0 +1 @@
+= 'foo';
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc
new file mode 100644
index 0000000000..db6d1b8931
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc
@@ -0,0 +1 @@
+= 'foo' ?>
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed
new file mode 100644
index 0000000000..d3c19feeb2
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed
@@ -0,0 +1 @@
+= 'foo' ?>
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc
new file mode 100644
index 0000000000..d3c19feeb2
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc
@@ -0,0 +1 @@
+= 'foo' ?>
diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php
index b97e8ab295..f5ab9ffa6f 100644
--- a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php
+++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php
@@ -33,6 +33,9 @@ public function getErrorList($testFile='')
case 'EndFileNewlineUnitTest.3.css':
case 'EndFileNewlineUnitTest.4.inc':
return [2 => 1];
+ case 'EndFileNewlineUnitTest.6.inc':
+ case 'EndFileNewlineUnitTest.7.inc':
+ return [1 => 1];
default:
return [];
}//end switch
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc
new file mode 100644
index 0000000000..884aad1b30
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc
@@ -0,0 +1 @@
+= 'foo'; ?>
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc
new file mode 100644
index 0000000000..1f87609292
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc
@@ -0,0 +1 @@
+= 'foo';
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc.fixed
new file mode 100644
index 0000000000..4f2e47af9c
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc.fixed
@@ -0,0 +1 @@
+= 'foo';
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc
new file mode 100644
index 0000000000..95de52c990
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc
@@ -0,0 +1 @@
+= 'foo'; ?>
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed
new file mode 100644
index 0000000000..884aad1b30
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed
@@ -0,0 +1 @@
+= 'foo'; ?>
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php
index 23c9980b44..88dc2f88a1 100644
--- a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php
+++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php
@@ -37,6 +37,9 @@ public function getErrorList($testFile='')
case 'EndFileNoNewlineUnitTest.2.js':
case 'EndFileNoNewlineUnitTest.6.inc':
return [2 => 1];
+ case 'EndFileNoNewlineUnitTest.8.inc':
+ case 'EndFileNoNewlineUnitTest.9.inc':
+ return [1 => 1];
default:
return [];
}//end switch
diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc
new file mode 100644
index 0000000000..ee44f5118f
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc
@@ -0,0 +1 @@
+= 'text' ?>
diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc
new file mode 100755
index 0000000000..ee44f5118f
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc
@@ -0,0 +1 @@
+= 'text' ?>
diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php
index f47fef6d36..269102dd3e 100644
--- a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php
+++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php
@@ -25,7 +25,7 @@ protected function shouldSkipTest()
// PEAR doesn't preserve the executable flag, so skip
// tests when running in a PEAR install.
// Also skip on Windows which doesn't have the concept of executable files.
- return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'));
+ return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || stripos(PHP_OS, 'WIN') === 0);
}//end shouldSkipTest()
@@ -44,6 +44,7 @@ public function getErrorList($testFile='')
{
switch ($testFile) {
case 'ExecutableFileUnitTest.2.inc':
+ case 'ExecutableFileUnitTest.4.inc':
return [1 => 1];
default:
return [];
diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc.fixed b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc
similarity index 100%
rename from src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc.fixed
rename to src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc
diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed
similarity index 88%
rename from src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc
rename to src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed
index a2d3ed2093..b143fb4cf0 100644
--- a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc
+++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed
@@ -1,18 +1,18 @@
-
-
-
-
-
-group('a.id,
- uc.name,
- ag.title,
- ua.name'
- );
-}
+
+
+
+
+
+group('a.id,
+ uc.name,
+ ag.title,
+ ua.name'
+ );
+}
diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc
new file mode 100644
index 0000000000..260d7fd2fe
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc
@@ -0,0 +1,5 @@
+
+= 'hi'; ?>
+
+...more HTML...
+= 'hi'; ?>
diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed
new file mode 100644
index 0000000000..260d7fd2fe
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed
@@ -0,0 +1,5 @@
+
+= 'hi'; ?>
+
+...more HTML...
+= 'hi'; ?>
diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.inc b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.1.inc
similarity index 100%
rename from src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.inc
rename to src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.1.inc
diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc
new file mode 100644
index 0000000000..708d19c6d6
--- /dev/null
+++ b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc
@@ -0,0 +1,7 @@
+=
+
+?>
+
+=
+
+?>
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php
index 4b4bc97427..f67a40a3be 100644
--- a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php
+++ b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php
@@ -21,11 +21,19 @@ class LowercasedFilenameUnitTest extends AbstractSniffUnitTest
* The key of the array should represent the line number and the value
* should represent the number of errors that should occur on that line.
*
+ * @param string $testFile The name of the file being tested.
+ *
* @return array
*/
- public function getErrorList()
+ public function getErrorList($testFile='')
{
- return [1 => 1];
+ switch ($testFile) {
+ case 'LowercasedFilenameUnitTest.1.inc':
+ case 'LowercasedFilenameUnitTest.2.inc':
+ return [1 => 1];
+ default:
+ return [];
+ }
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc
index 94cebb4ee2..9f76d5d8d0 100644
--- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc
+++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc
@@ -12,10 +12,15 @@ class baz {
}
trait barTrait {
-
+
}
interface barInterface {
}
-?>
\ No newline at end of file
+
+enum barEnum {
+
+}
+
+?>
diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php
index f429f98095..36a4bb9385 100644
--- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php
+++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php
@@ -30,6 +30,7 @@ public function getErrorList()
10 => 1,
14 => 1,
18 => 1,
+ 22 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc b/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc
index 5dd11cfdb7..ed316cc219 100644
--- a/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc
+++ b/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc
@@ -14,3 +14,7 @@ $this->wizardid = 10; $this->paint(); echo 'x';
1,
399 => 1,
401 => 1,
+ 420 => 1,
+ 422 => 1,
+ 436 => 1,
+ 438 => 1,
+ 442 => 1,
+ 443 => 1,
+ 454 => 1,
+ 487 => 1,
+ 499 => 1,
+ 500 => 1,
];
break;
case 'MultipleStatementAlignmentUnitTest.js':
diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc
index 2747553cdb..8bd067b3f9 100644
--- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc
+++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc
@@ -149,3 +149,26 @@ $foobar = functionCallAnonClassParam(
},
$args=array(),
);
+
+$result = myFunction(param1: $arg1, param2: $arg2);
+$result = myFunction(param1: $arg1 , param2:$arg2);
+$result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3,param4:$arg4, param5:$arg5);
+
+class Testing extends Bar
+{
+ public static function baz($foo, $bar)
+ {
+ $a = new parent($foo, $bar);
+ $a = new parent($foo ,$bar);
+ }
+}
+
+// Ignore spacing after PHP 7.3+ trailing comma in single-line function calls to prevent fixer conflicts.
+// This is something which should be decided by a sniff dealing with the function call parentheses.
+$foo = new MyClass($obj, 'getMethod',);
+$foo = new MyClass($obj, 'getMethod', );
+$foo = new MyClass($obj, 'getMethod', );
+$foo = new MyClass(
+ $obj,
+ 'getMethod',
+);
diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed
index 0ca70ca05f..69676524dc 100644
--- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed
@@ -149,3 +149,26 @@ $foobar = functionCallAnonClassParam(
},
$args=array(),
);
+
+$result = myFunction(param1: $arg1, param2: $arg2);
+$result = myFunction(param1: $arg1, param2:$arg2);
+$result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3, param4:$arg4, param5:$arg5);
+
+class Testing extends Bar
+{
+ public static function baz($foo, $bar)
+ {
+ $a = new parent($foo, $bar);
+ $a = new parent($foo, $bar);
+ }
+}
+
+// Ignore spacing after PHP 7.3+ trailing comma in single-line function calls to prevent fixer conflicts.
+// This is something which should be decided by a sniff dealing with the function call parentheses.
+$foo = new MyClass($obj, 'getMethod',);
+$foo = new MyClass($obj, 'getMethod', );
+$foo = new MyClass($obj, 'getMethod', );
+$foo = new MyClass(
+ $obj,
+ 'getMethod',
+);
diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php
index cb1c2e0867..5f87962e62 100644
--- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php
+++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php
@@ -52,6 +52,10 @@ public function getErrorList()
132 => 2,
133 => 2,
134 => 1,
+ 154 => 2,
+ 155 => 1,
+ 162 => 2,
+ 170 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc
index 854c38886e..3ae3b1ed0b 100644
--- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc
+++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc
@@ -236,3 +236,28 @@ function myFunction($a, $lot, $of, $params)
: array /* comment */ {
return null;
}
+
+class Issue3357
+{
+ public function extraLine(string: $a): void
+
+ {
+ // code here.
+ }
+}
+
+function issue3357WithoutIndent(string: $a): void
+
+
+{
+ // code here.
+}
+
+class Issue3357WithComment
+{
+ public function extraLine(string: $a): void
+ // Comment.
+ {
+ // code here.
+ }
+}
diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed
index c1f904b218..f164c4934e 100644
--- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed
@@ -256,3 +256,25 @@ function myFunction($a, $lot, $of, $params)
{
return null;
}
+
+class Issue3357
+{
+ public function extraLine(string: $a): void
+ {
+ // code here.
+ }
+}
+
+function issue3357WithoutIndent(string: $a): void
+{
+ // code here.
+}
+
+class Issue3357WithComment
+{
+ public function extraLine(string: $a): void
+ // Comment.
+ {
+ // code here.
+ }
+}
diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php
index 27eaa4fd77..10af4fd55a 100644
--- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php
+++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php
@@ -58,6 +58,9 @@ public function getErrorList()
220 => 1,
231 => 1,
236 => 1,
+ 244 => 1,
+ 252 => 1,
+ 260 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
index 151ffed6a1..494dcc7694 100644
--- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
+++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
@@ -79,9 +79,11 @@ function complexityTwenty()
switch ($condition) {
case '1':
- if ($condition) {
- } else if ($cond) {
- }
+ do {
+ if ($condition) {
+ } else if ($cond) {
+ }
+ } while ($cond);
break;
case '2':
while ($cond) {
@@ -116,9 +118,11 @@ function complexityTwenty()
function complexityTwentyOne()
{
while ($condition === true) {
- if ($condition) {
- } else if ($cond) {
- }
+ do {
+ if ($condition) {
+ } else if ($cond) {
+ }
+ } while ($cond);
}
switch ($condition) {
@@ -157,4 +161,294 @@ function complexityTwentyOne()
}
}
+
+function complexityTenWithTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = (empty($condition2)) ? $value2A : $value2B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = (empty($condition2)) ? $value2A : $value2B;
+ $value3 = (empty($condition3)) ? $value3A : $value3B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNestedTernaries()
+{
+ $value1 = true ? $value1A : false ? $value1B : $value1C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNestedTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = true ? $value2A : false ? $value2B : $value2C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B;
+ $value3 = $value3A ?? $value3B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNestedNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B ?? $value1C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNestedNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B ?? $value2C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNullCoalescenceAssignment()
+{
+ $value1 ??= $default1;
+ $value2 ??= $default2;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNullCoalescenceAssignment()
+{
+ $value1 ??= $default1;
+ $value2 ??= $default2;
+ $value3 ??= $default3;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityFiveWithMatch()
+{
+ return match(strtolower(substr($monthName, 0, 3))){
+ 'apr', 'jun', 'sep', 'nov' => 30,
+ 'jan', 'mar', 'may', 'jul', 'aug', 'oct', 'dec' => 31,
+ 'feb' => is_leap_year($year) ? 29 : 28,
+ default => throw new InvalidArgumentException("Invalid month"),
+ }
+}
+
+
+function complexityFourteenWithMatch()
+{
+ return match(strtolower(substr($monthName, 0, 3))) {
+ 'jan' => 31,
+ 'feb' => is_leap_year($year) ? 29 : 28,
+ 'mar' => 31,
+ 'apr' => 30,
+ 'may' => 31,
+ 'jun' => 30,
+ 'jul' => 31,
+ 'aug' => 31,
+ 'sep' => 30,
+ 'oct' => 31,
+ 'nov' => 30,
+ 'dec' => 31,
+ default => throw new InvalidArgumentException("Invalid month"),
+ };
+}
+
+
+function complexitySevenWithNullSafeOperator()
+{
+ $foo = $object1->getX()?->getY()?->getZ();
+ $bar = $object2->getX()?->getY()?->getZ();
+ $baz = $object3->getX()?->getY()?->getZ();
+}
+
+
+function complexityElevenWithNullSafeOperator()
+{
+ $foo = $object1->getX()?->getY()?->getZ();
+ $bar = $object2->getX()?->getY()?->getZ();
+ $baz = $object3->getX()?->getY()?->getZ();
+ $bacon = $object4->getX()?->getY()?->getZ();
+ $bits = $object5->getX()?->getY()?->getZ();
+}
+
?>
diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
index e8099317cd..d3860dff9e 100644
--- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
+++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
@@ -25,7 +25,7 @@ class CyclomaticComplexityUnitTest extends AbstractSniffUnitTest
*/
public function getErrorList()
{
- return [116 => 1];
+ return [118 => 1];
}//end getErrorList()
@@ -41,8 +41,15 @@ public function getErrorList()
public function getWarningList()
{
return [
- 45 => 1,
- 72 => 1,
+ 45 => 1,
+ 72 => 1,
+ 189 => 1,
+ 237 => 1,
+ 285 => 1,
+ 333 => 1,
+ 381 => 1,
+ 417 => 1,
+ 445 => 1,
];
}//end getWarningList()
diff --git a/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc
new file mode 100644
index 0000000000..27b9d818da
--- /dev/null
+++ b/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc
@@ -0,0 +1,59 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions;
+
+use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
+
+class AbstractClassNamePrefixUnitTest extends AbstractSniffUnitTest
+{
+
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of errors that should occur on that line.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [
+ 3 => 1,
+ 13 => 1,
+ 18 => 1,
+ 23 => 1,
+ 42 => 1,
+ ];
+
+ }//end getErrorList()
+
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+
+ }//end getWarningList()
+
+
+}//end class
diff --git a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc
index 9bda11472e..8441060d27 100644
--- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc
+++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc
@@ -165,3 +165,21 @@ abstract class My_Class {
public function my_class() {}
public function _MY_CLASS() {}
}
+
+enum Suit: string implements Colorful, CardGame {
+ // Magic methods.
+ function __call($name, $args) {}
+ static function __callStatic($name, $args) {}
+ function __invoke() {}
+
+ // Valid Method Name.
+ public function getSomeValue() {}
+
+ // Double underscore non-magic methods not allowed.
+ function __myFunction() {}
+ function __my_function() {}
+
+ // Non-camelcase.
+ public function parseMyDSN() {}
+ public function get_some_value() {}
+}
diff --git a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php
index 502498d8bc..3d0320c3ae 100644
--- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php
+++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php
@@ -63,6 +63,10 @@ public function getErrorList()
147 => 2,
158 => 1,
159 => 1,
+ 179 => 1,
+ 180 => 2,
+ 183 => 1,
+ 184 => 1,
];
return $errors;
diff --git a/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc
new file mode 100644
index 0000000000..d562d3eaeb
--- /dev/null
+++ b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc
@@ -0,0 +1,27 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions;
+
+use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
+
+class InterfaceNameSuffixUnitTest extends AbstractSniffUnitTest
+{
+
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of errors that should occur on that line.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [8 => 1];
+
+ }//end getErrorList()
+
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+
+ }//end getWarningList()
+
+
+}//end class
diff --git a/src/Standards/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc
new file mode 100644
index 0000000000..e807697185
--- /dev/null
+++ b/src/Standards/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc
@@ -0,0 +1,13 @@
+
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions;
+
+use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
+
+class TraitNameSuffixUnitTest extends AbstractSniffUnitTest
+{
+
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of errors that should occur on that line.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [
+ 3 => 1,
+ 9 => 1,
+ ];
+
+ }//end getErrorList()
+
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+
+ }//end getWarningList()
+
+
+}//end class
diff --git a/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc
index 7c33bbf119..6bf96f251c 100644
--- a/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc
+++ b/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc
@@ -29,3 +29,5 @@ class ClassConstBowOutTest {
const // phpcs:ignore Standard.Category.Sniff
some_constant = 2;
}
+
+$foo->getBar()?->define('foo');
diff --git a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.inc b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.1.inc
similarity index 100%
rename from src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.inc
rename to src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.1.inc
diff --git a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc
new file mode 100644
index 0000000000..fc896ed197
--- /dev/null
+++ b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc
@@ -0,0 +1,5 @@
+= 'foo'; ?>
+Bold text
+= 'bar'; ?>
+Italic text
+= 'baz';
\ No newline at end of file
diff --git a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.php b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.php
index bcadc38655..9c567b7bad 100644
--- a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.php
@@ -21,11 +21,22 @@ class ClosingPHPTagUnitTest extends AbstractSniffUnitTest
* The key of the array should represent the line number and the value
* should represent the number of errors that should occur on that line.
*
+ * @param string $testFile The name of the file being tested.
+ *
* @return array
*/
- public function getErrorList()
+ public function getErrorList($testFile='')
{
- return [9 => 1];
+ switch ($testFile) {
+ case 'ClosingPHPTagUnitTest.1.inc':
+ return [9 => 1];
+ case 'ClosingPHPTagUnitTest.2.inc':
+ return [5 => 1];
+ break;
+ default:
+ return [];
+ break;
+ }
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc b/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc
index 4659b73289..060da6128c 100644
--- a/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc
@@ -54,4 +54,7 @@ class SizeOf implements Something {}
function mymodule_form_callback(SizeOf $sizeof) {
}
-?>
+$size = $class?->sizeof($array);
+
+#[SizeOf(10)]
+function doSomething() {}
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc
index 63aea51872..0307a0559d 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc
@@ -81,3 +81,20 @@ var_dump(MyClass::TRUE);
function tRUE() {}
$input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]);
+
+// Issue #3332 - ignore type declarations, but not default values.
+class TypedThings {
+ const MYCONST = FALSE;
+
+ public int|FALSE $int = FALSE;
+ public Type|NULL $int = new MyObj(NULL);
+
+ private function typed(int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL
+ {
+ if (TRUE === FALSE) {
+ return NULL;
+ }
+ }
+}
+
+$cl = function (int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL {};
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed
index b3c3d8a136..3a6b094c86 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed
@@ -81,3 +81,20 @@ var_dump(MyClass::TRUE);
function tRUE() {}
$input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]);
+
+// Issue #3332 - ignore type declarations, but not default values.
+class TypedThings {
+ const MYCONST = false;
+
+ public int|FALSE $int = false;
+ public Type|NULL $int = new MyObj(null);
+
+ private function typed(int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL
+ {
+ if (true === false) {
+ return null;
+ }
+ }
+}
+
+$cl = function (int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL {};
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php
index 85e8f7011d..2fb2d6f6dc 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php
@@ -30,20 +30,27 @@ public function getErrorList($testFile='LowerCaseConstantUnitTest.inc')
switch ($testFile) {
case 'LowerCaseConstantUnitTest.inc':
return [
- 7 => 1,
- 10 => 1,
- 15 => 1,
- 16 => 1,
- 23 => 1,
- 26 => 1,
- 31 => 1,
- 32 => 1,
- 39 => 1,
- 42 => 1,
- 47 => 1,
- 48 => 1,
- 70 => 1,
- 71 => 1,
+ 7 => 1,
+ 10 => 1,
+ 15 => 1,
+ 16 => 1,
+ 23 => 1,
+ 26 => 1,
+ 31 => 1,
+ 32 => 1,
+ 39 => 1,
+ 42 => 1,
+ 47 => 1,
+ 48 => 1,
+ 70 => 1,
+ 71 => 1,
+ 87 => 1,
+ 89 => 1,
+ 90 => 1,
+ 92 => 2,
+ 94 => 2,
+ 95 => 1,
+ 100 => 2,
];
break;
case 'LowerCaseConstantUnitTest.js':
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
index 96c2062256..37579d3217 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
@@ -29,5 +29,20 @@ class X extends Y {
}
}
FN ($x) => $x;
+$r = Match ($x) {
+ 1 => 1,
+ 2 => 2,
+ DEFAULT, => 3,
+};
+
+class Reading {
+ Public READOnly int $var;
+}
+
+EnuM ENUM: string
+{
+ Case HEARTS;
+}
+
__HALT_COMPILER(); // An exception due to phar support.
function
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed
index f7d3b397fe..7063327ae8 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed
@@ -29,5 +29,20 @@ class X extends Y {
}
}
fn ($x) => $x;
+$r = match ($x) {
+ 1 => 1,
+ 2 => 2,
+ default, => 3,
+};
+
+class Reading {
+ public readonly int $var;
+}
+
+enum ENUM: string
+{
+ case HEARTS;
+}
+
__HALT_COMPILER(); // An exception due to phar support.
function
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php
index 2b9239062e..6d08e12751 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php
@@ -38,6 +38,11 @@ public function getErrorList()
25 => 1,
28 => 1,
31 => 1,
+ 32 => 1,
+ 35 => 1,
+ 39 => 2,
+ 42 => 1,
+ 44 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc
index f4e0dd4628..011adcd530 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc
@@ -15,7 +15,7 @@ $foo = (Int) $bar;
$foo = (INTEGER) $bar;
$foo = (BOOL) $bar;
$foo = (String) $bar;
-$foo = (Array) $bar;
+$foo = ( Array ) $bar;
function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {}
function foo(Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Foo\Bar {}
@@ -45,3 +45,50 @@ $foo = function (?Int $a, ? Callable $b)
$var = (BInARY) $string;
$var = (binary)$string;
+
+function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ NULL $var) {}
+
+function unionParamTypesB (\Package\ClassName | Int | \Package\Other_Class | FALSE $var) {}
+
+function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ NULL {}
+
+function unionReturnTypesB ($var): \Package\ClassName | Int | \Package\Other_Class | FALSE {}
+
+class TypedProperties
+{
+ protected ClassName $class;
+ public Int $int;
+ private ?BOOL $bool;
+ public Self $self;
+ protected PaRenT $parent;
+ private ARRAY $array;
+ public Float $float;
+ protected ?STRING $string;
+ private IterablE $iterable;
+ public Object $object;
+ protected Mixed $mixed;
+
+ public Iterable|FALSE|NULL $unionTypeA;
+ protected SELF|Parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB;
+ private ClassName|/*comment*/Float|STRING|False $unionTypeC;
+ public sTRing | aRRaY | FaLSe $unionTypeD;
+}
+
+class ConstructorPropertyPromotionWithTypes {
+ public function __construct(protected Float|Int $x, public ?STRING &$y = 'test', private mixed $z) {}
+}
+
+class ConstructorPropertyPromotionAndNormalParams {
+ public function __construct(public Int $promotedProp, ?Int $normalArg) {}
+}
+
+function (): NeVeR {
+ exit;
+};
+
+function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {}
+
+function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {}
+
+$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b;
+$arrow = fn (Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Float => $a * $b;
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed
index a64a4400d7..d866101b6f 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed
@@ -15,7 +15,7 @@ $foo = (int) $bar;
$foo = (integer) $bar;
$foo = (bool) $bar;
$foo = (string) $bar;
-$foo = (array) $bar;
+$foo = ( array ) $bar;
function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {}
function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : Foo\Bar {}
@@ -45,3 +45,50 @@ $foo = function (?int $a, ? callable $b)
$var = (binary) $string;
$var = (binary)$string;
+
+function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ null $var) {}
+
+function unionParamTypesB (\Package\ClassName | int | \Package\Other_Class | false $var) {}
+
+function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ null {}
+
+function unionReturnTypesB ($var): \Package\ClassName | int | \Package\Other_Class | false {}
+
+class TypedProperties
+{
+ protected ClassName $class;
+ public int $int;
+ private ?bool $bool;
+ public self $self;
+ protected parent $parent;
+ private array $array;
+ public float $float;
+ protected ?string $string;
+ private iterable $iterable;
+ public object $object;
+ protected mixed $mixed;
+
+ public iterable|false|null $unionTypeA;
+ protected self|parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB;
+ private ClassName|/*comment*/float|string|false $unionTypeC;
+ public string | array | false $unionTypeD;
+}
+
+class ConstructorPropertyPromotionWithTypes {
+ public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {}
+}
+
+class ConstructorPropertyPromotionAndNormalParams {
+ public function __construct(public int $promotedProp, ?int $normalArg) {}
+}
+
+function (): never {
+ exit;
+};
+
+function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {}
+
+function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {}
+
+$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b;
+$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : float => $a * $b;
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php
index 0a5f5e03ea..fa05aba6a3 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php
@@ -45,6 +45,28 @@ public function getErrorList()
43 => 2,
44 => 1,
46 => 1,
+ 49 => 1,
+ 51 => 2,
+ 53 => 1,
+ 55 => 2,
+ 60 => 1,
+ 61 => 1,
+ 62 => 1,
+ 63 => 1,
+ 64 => 1,
+ 65 => 1,
+ 66 => 1,
+ 67 => 1,
+ 68 => 1,
+ 69 => 1,
+ 71 => 3,
+ 72 => 2,
+ 73 => 3,
+ 74 => 3,
+ 78 => 3,
+ 82 => 2,
+ 85 => 1,
+ 94 => 5,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc
index 7ff31ba704..72bffe2c4c 100644
--- a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc
@@ -6,4 +6,5 @@ if (@in_array($array, $needle))
{
echo '@';
}
-?>
+
+$hasValue = @in_array(haystack: $array, needle: $needle);
diff --git a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php
index 40b509bced..0e7a4eaa40 100644
--- a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php
@@ -40,7 +40,10 @@ public function getErrorList()
*/
public function getWarningList()
{
- return [5 => 1];
+ return [
+ 5 => 1,
+ 10 => 1,
+ ];
}//end getWarningList()
diff --git a/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc b/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc
index 8f378c8db1..f0f350f374 100644
--- a/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc
@@ -2,3 +2,4 @@
if (php_sapi_name() !== 'cli') {}
if (PHP_SAPI !== 'cli') {}
if ($object->php_sapi_name() === true) {}
+if ($object?->php_sapi_name() === true) {}
diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.inc b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.1.inc
similarity index 100%
rename from src/Standards/Generic/Tests/PHP/SyntaxUnitTest.inc
rename to src/Standards/Generic/Tests/PHP/SyntaxUnitTest.1.inc
diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc
new file mode 100644
index 0000000000..11f8a58988
--- /dev/null
+++ b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc
@@ -0,0 +1,3 @@
+= 'text' ?>
+text
+= if($x) { $y = 'text'; } ?>
diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php
index d8cd7efaab..98d205ce43 100644
--- a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php
@@ -22,11 +22,21 @@ class SyntaxUnitTest extends AbstractSniffUnitTest
* The key of the array should represent the line number and the value
* should represent the number of errors that should occur on that line.
*
+ * @param string $testFile The name of the file being tested.
+ *
* @return array
*/
- public function getErrorList()
+ public function getErrorList($testFile='')
{
- return [3 => 1];
+ switch ($testFile) {
+ case 'SyntaxUnitTest.1.inc':
+ case 'SyntaxUnitTest.2.inc':
+ return [3 => 1];
+ break;
+ default:
+ return [];
+ break;
+ }
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc
index 965bf3b511..30c6d2980d 100644
--- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc
@@ -78,4 +78,21 @@ class MyClass
var_dump(MyClass::true);
-function true() {}
\ No newline at end of file
+function true() {}
+
+// Issue #3332 - ignore type declarations, but not default values.
+class TypedThings {
+ const MYCONST = false;
+
+ public int|false $int = false;
+ public Type|null $int = new MyObj(null);
+
+ private function typed(int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null
+ {
+ if (true === false) {
+ return null;
+ }
+ }
+}
+
+$cl = function (int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null {};
diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed
index ae83dc91de..7705198c81 100644
--- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed
@@ -78,4 +78,21 @@ class MyClass
var_dump(MyClass::true);
-function true() {}
\ No newline at end of file
+function true() {}
+
+// Issue #3332 - ignore type declarations, but not default values.
+class TypedThings {
+ const MYCONST = FALSE;
+
+ public int|false $int = FALSE;
+ public Type|null $int = new MyObj(NULL);
+
+ private function typed(int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null
+ {
+ if (TRUE === FALSE) {
+ return NULL;
+ }
+ }
+}
+
+$cl = function (int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null {};
diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php
index 0bdafe6a43..30e5776337 100644
--- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php
@@ -40,6 +40,13 @@ public function getErrorList()
48 => 1,
70 => 1,
71 => 1,
+ 85 => 1,
+ 87 => 1,
+ 88 => 1,
+ 90 => 2,
+ 92 => 2,
+ 93 => 1,
+ 98 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc
new file mode 100644
index 0000000000..85cae1fdc8
--- /dev/null
+++ b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc
@@ -0,0 +1,19 @@
+
+
+<<<<<<< HEAD
+ Testing a merge conflict.
+=======
+ Another text string.
+>>>>>>> ref/heads/feature-branch
+
+
+
+
+<<<<<<< HEAD
+ = 'Testing a merge conflict.'; ?>
+=======
+ = 'Another text string.'; ?>
+>>>>>>> ref/heads/feature-branch
+
+
+= $text; ?>
diff --git a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php
index a36caafbd2..50986f48bb 100644
--- a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php
+++ b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php
@@ -99,6 +99,16 @@ public function getErrorList($testFile='GitMergeConflictUnitTest.1.inc')
32 => 1,
];
+ case 'GitMergeConflictUnitTest.7.inc':
+ return [
+ 3 => 1,
+ 5 => 1,
+ 7 => 1,
+ 12 => 1,
+ 14 => 1,
+ 16 => 1,
+ ];
+
case 'GitMergeConflictUnitTest.1.css':
return [
3 => 1,
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc
index bad3998f97..2399a387e1 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc
+++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc
@@ -163,3 +163,15 @@ $a = (
if (true) {} ( 1+2) === 3 ? $a = 1 : $a = 2;
class A {} ( 1+2) === 3 ? $a = 1 : $a = 2;
function foo() {} ( 1+2) === 3 ? $a = 1 : $a = 2;
+
+// Issue #3618.
+class NonArbitraryParenthesesWithKeywords {
+ public static function baz( $foo, $bar ) {
+ $a = new self();
+ $b = new parent();
+ $c = new static();
+
+ // self/static are already tested above, round line 45.
+ $d = new parent( $foo,$bar );
+ }
+}
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed
index 08fcd6244a..9162728e17 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed
@@ -151,3 +151,15 @@ $a = (
if (true) {} (1+2) === 3 ? $a = 1 : $a = 2;
class A {} (1+2) === 3 ? $a = 1 : $a = 2;
function foo() {} (1+2) === 3 ? $a = 1 : $a = 2;
+
+// Issue #3618.
+class NonArbitraryParenthesesWithKeywords {
+ public static function baz( $foo, $bar ) {
+ $a = new self();
+ $b = new parent();
+ $c = new static();
+
+ // self/static are already tested above, round line 45.
+ $d = new parent( $foo,$bar );
+ }
+}
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc
new file mode 100644
index 0000000000..4df731a3c0
--- /dev/null
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc
@@ -0,0 +1,19 @@
+=
+ "$hello $there";
+?>
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
+=
+ "$hello $there";
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc.fixed
new file mode 100644
index 0000000000..064567954e
--- /dev/null
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc.fixed
@@ -0,0 +1,19 @@
+=
+ "$hello $there";
+?>
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
+=
+ "$hello $there";
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.php b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.php
index 5c68c381d1..f915cf4aa9 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.php
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.php
@@ -86,6 +86,17 @@ public function getErrorList($testFile='DisallowSpaceIndentUnitTest.1.inc')
118 => 1,
];
break;
+ case 'DisallowSpaceIndentUnitTest.3.inc':
+ return [
+ 2 => 1,
+ 5 => 1,
+ 10 => 1,
+ 12 => 1,
+ 13 => 1,
+ 14 => 1,
+ 15 => 1,
+ ];
+ break;
case 'DisallowSpaceIndentUnitTest.js':
return [3 => 1];
break;
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc
similarity index 100%
rename from src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc
rename to src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc.fixed
similarity index 100%
rename from src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc.fixed
rename to src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc.fixed
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc
new file mode 100644
index 0000000000..4df731a3c0
--- /dev/null
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc
@@ -0,0 +1,19 @@
+=
+ "$hello $there";
+?>
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
+=
+ "$hello $there";
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc.fixed
new file mode 100644
index 0000000000..04a764ac2d
--- /dev/null
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc.fixed
@@ -0,0 +1,19 @@
+=
+ "$hello $there";
+?>
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
+=
+ "$hello $there";
diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc
new file mode 100644
index 0000000000..68b18938d0
--- /dev/null
+++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc
@@ -0,0 +1,13 @@
+
*/
- public function getErrorList($testFile='DisallowTabIndentUnitTest.inc')
+ public function getErrorList($testFile='')
{
switch ($testFile) {
- case 'DisallowTabIndentUnitTest.inc':
+ case 'DisallowTabIndentUnitTest.1.inc':
return [
5 => 2,
9 => 1,
@@ -83,23 +83,46 @@ public function getErrorList($testFile='DisallowTabIndentUnitTest.inc')
92 => 1,
93 => 1,
];
- break;
+
+ case 'DisallowTabIndentUnitTest.2.inc':
+ return [
+ 6 => 1,
+ 7 => 1,
+ 8 => 1,
+ 9 => 1,
+ 10 => 1,
+ 11 => 1,
+ 12 => 1,
+ 13 => 1,
+ 19 => 1,
+ ];
+
+ case 'DisallowTabIndentUnitTest.3.inc':
+ if (\PHP_VERSION_ID >= 70300) {
+ return [
+ 7 => 1,
+ 13 => 1,
+ ];
+ }
+
+ // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet.
+ return [];
+
case 'DisallowTabIndentUnitTest.js':
return [
3 => 1,
5 => 1,
6 => 1,
];
- break;
+
case 'DisallowTabIndentUnitTest.css':
return [
1 => 1,
2 => 1,
];
- break;
+
default:
return [];
- break;
}//end switch
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc
index bcce855e31..4061aff567 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc
+++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc
@@ -1454,6 +1454,132 @@ return [
]),
];
+echo $string?->append('foo')
+ ?->outputUsing();
+
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact true
+echo $string?->append('foo')
+ ?->outputUsing();
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact false
+
+if (true) {
+ ?> null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match ($value) {
+ '' => null,
+false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match (
+ $value
+ ) {
+ '' => null,
+ false
+ => false,
+ 1,
+ 2,
+ 3 => true,
+ default =>
+$value,
+};
+
+function toString(): string
+{
+ return sprintf(
+ '%s',
+ match ($type) {
+ 'foo' => 'bar',
+ },
+ );
+}
+
+$list = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ },
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ $list2 = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ }
+ ];
+ }
+ }
+];
+
+$foo = match ($type) {
+ 'a' => [
+ 'aa' => 'DESC',
+ 'ab' => 'DESC',
+ ],
+ 'b' => [
+ 'ba' => 'DESC',
+ 'bb' => 'DESC',
+ ],
+ default => [
+ 'da' => 'DESC',
+ ],
+};
+
+$a = [
+ 'a' => [
+ 'a' => fn () => foo()
+ ],
+ 'a' => [
+ 'a' => 'a',
+ ]
+];
+
+switch ($foo) {
+ case 'a':
+ $foo = match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'a'
+ };
+ return $foo;
+ case 'b':
+ return match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'b'
+ };
+ default:
+ return 'default';
+}
+
+foo(function ($foo) {
+ return [
+ match ($foo) {
+ }
+ ];
+});
+
+/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */
?>
@@ -1461,7 +1587,7 @@ return [
+ <<<'INTRO'
+ lorem ipsum
+ INTRO,
+ 'em' => [
+ [
+ '',
+ ],
+ ],
+ 'abc' => [
+ 'a' => 'wop wop',
+ 'b' => 'ola ola.',
+ ],
+];
+
echo ""
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed
index 3ad7b793b8..7b5efea36a 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed
+++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed
@@ -1454,6 +1454,132 @@ return [
]),
];
+echo $string?->append('foo')
+ ?->outputUsing();
+
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact true
+echo $string?->append('foo')
+ ?->outputUsing();
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact false
+
+if (true) {
+ ?> null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match ($value) {
+ '' => null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match (
+ $value
+ ) {
+ '' => null,
+ false
+ => false,
+ 1,
+ 2,
+ 3 => true,
+ default =>
+ $value,
+};
+
+function toString(): string
+{
+ return sprintf(
+ '%s',
+ match ($type) {
+ 'foo' => 'bar',
+ },
+ );
+}
+
+$list = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ },
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ $list2 = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ }
+ ];
+ }
+ }
+];
+
+$foo = match ($type) {
+ 'a' => [
+ 'aa' => 'DESC',
+ 'ab' => 'DESC',
+ ],
+ 'b' => [
+ 'ba' => 'DESC',
+ 'bb' => 'DESC',
+ ],
+ default => [
+ 'da' => 'DESC',
+ ],
+};
+
+$a = [
+ 'a' => [
+ 'a' => fn () => foo()
+ ],
+ 'a' => [
+ 'a' => 'a',
+ ]
+];
+
+switch ($foo) {
+ case 'a':
+ $foo = match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'a'
+ };
+ return $foo;
+ case 'b':
+ return match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'b'
+ };
+ default:
+ return 'default';
+}
+
+foo(function ($foo) {
+ return [
+ match ($foo) {
+ }
+ ];
+});
+
+/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */
?>
@@ -1461,7 +1587,7 @@ return [
+ <<<'INTRO'
+ lorem ipsum
+ INTRO,
+ 'em' => [
+ [
+ '',
+ ],
+ ],
+ 'abc' => [
+ 'a' => 'wop wop',
+ 'b' => 'ola ola.',
+ ],
+];
+
echo ""
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc
index d128111de0..e7253141d4 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc
+++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc
@@ -1454,6 +1454,132 @@ return [
]),
];
+echo $string?->append('foo')
+ ?->outputUsing();
+
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact true
+echo $string?->append('foo')
+ ?->outputUsing();
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact false
+
+if (true) {
+ ?> null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match ($value) {
+ '' => null,
+false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match (
+ $value
+ ) {
+ '' => null,
+ false
+ => false,
+ 1,
+ 2,
+ 3 => true,
+ default =>
+$value,
+};
+
+function toString(): string
+{
+ return sprintf(
+ '%s',
+ match ($type) {
+ 'foo' => 'bar',
+ },
+ );
+}
+
+$list = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ },
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ $list2 = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ }
+ ];
+ }
+ }
+];
+
+$foo = match ($type) {
+ 'a' => [
+ 'aa' => 'DESC',
+ 'ab' => 'DESC',
+ ],
+ 'b' => [
+ 'ba' => 'DESC',
+ 'bb' => 'DESC',
+ ],
+ default => [
+ 'da' => 'DESC',
+ ],
+};
+
+$a = [
+ 'a' => [
+ 'a' => fn () => foo()
+ ],
+ 'a' => [
+ 'a' => 'a',
+ ]
+];
+
+switch ($foo) {
+ case 'a':
+ $foo = match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'a'
+ };
+ return $foo;
+ case 'b':
+ return match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'b'
+ };
+ default:
+ return 'default';
+}
+
+foo(function ($foo) {
+ return [
+ match ($foo) {
+ }
+ ];
+});
+
+/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */
?>
@@ -1461,7 +1587,7 @@ return [
+ <<<'INTRO'
+ lorem ipsum
+ INTRO,
+ 'em' => [
+ [
+ '',
+ ],
+ ],
+ 'abc' => [
+ 'a' => 'wop wop',
+ 'b' => 'ola ola.',
+ ],
+];
+
echo ""
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed
index d6505e0ced..57caa29175 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed
+++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed
@@ -1454,6 +1454,132 @@ return [
]),
];
+echo $string?->append('foo')
+ ?->outputUsing();
+
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact true
+echo $string?->append('foo')
+ ?->outputUsing();
+// phpcs:set Generic.WhiteSpace.ScopeIndent exact false
+
+if (true) {
+ ?> null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match ($value) {
+ '' => null,
+ false => false,
+ 1, 2, 3 => true,
+ default => $value,
+};
+
+$value = match (
+ $value
+ ) {
+ '' => null,
+ false
+ => false,
+ 1,
+ 2,
+ 3 => true,
+ default =>
+ $value,
+};
+
+function toString(): string
+{
+ return sprintf(
+ '%s',
+ match ($type) {
+ 'foo' => 'bar',
+ },
+ );
+}
+
+$list = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ },
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ $list2 = [
+ 'fn' => function ($a) {
+ if ($a === true) {
+ echo 'hi';
+ }
+ }
+ ];
+ }
+ }
+];
+
+$foo = match ($type) {
+ 'a' => [
+ 'aa' => 'DESC',
+ 'ab' => 'DESC',
+ ],
+ 'b' => [
+ 'ba' => 'DESC',
+ 'bb' => 'DESC',
+ ],
+ default => [
+ 'da' => 'DESC',
+ ],
+};
+
+$a = [
+ 'a' => [
+ 'a' => fn () => foo()
+ ],
+ 'a' => [
+ 'a' => 'a',
+ ]
+];
+
+switch ($foo) {
+ case 'a':
+ $foo = match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'a'
+ };
+ return $foo;
+ case 'b':
+ return match ($foo) {
+ 'bar' => 'custom_1',
+ default => 'b'
+ };
+ default:
+ return 'default';
+}
+
+foo(function ($foo) {
+ return [
+ match ($foo) {
+ }
+ ];
+});
+
+/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */
?>
@@ -1461,7 +1587,7 @@ return [
+ <<<'INTRO'
+ lorem ipsum
+ INTRO,
+ 'em' => [
+ [
+ '',
+ ],
+ ],
+ 'abc' => [
+ 'a' => 'wop wop',
+ 'b' => 'ola ola.',
+ ],
+];
+
echo ""
diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php
index f347678cac..6b0a71028e 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php
+++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php
@@ -178,10 +178,19 @@ public function getErrorList($testFile='ScopeIndentUnitTest.inc')
1340 => 1,
1342 => 1,
1345 => 1,
- 1464 => 1,
- 1465 => 1,
- 1466 => 1,
- 1467 => 1,
+ 1488 => 1,
+ 1489 => 1,
+ 1500 => 1,
+ 1503 => 1,
+ 1518 => 1,
+ 1520 => 1,
+ 1527 => 1,
+ 1529 => 1,
+ 1530 => 1,
+ 1590 => 1,
+ 1591 => 1,
+ 1592 => 1,
+ 1593 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc
index edce4dcc69..fb5c181429 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc
+++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc
@@ -67,7 +67,13 @@ function bar( & ... $spread ) {
);
}
+// Ignore PHP 8.1 first class callable declarations.
+$map = array_map(strtolower(...), $map);
+
// phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0
+// Ignore PHP 8.1 first class callable declarations.
+$map = array_map(strtolower( ... ), $map);
+
// Intentional parse error. This has to be the last test in the file.
function bar( ...
diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed
index efec7ac15c..9388acfc63 100644
--- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed
@@ -62,7 +62,13 @@ function bar( & ... $spread ) {
);
}
+// Ignore PHP 8.1 first class callable declarations.
+$map = array_map(strtolower(...), $map);
+
// phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0
+// Ignore PHP 8.1 first class callable declarations.
+$map = array_map(strtolower( ... ), $map);
+
// Intentional parse error. This has to be the last test in the file.
function bar( ...
diff --git a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php
index a5b07a9e47..dbd61f3d23 100644
--- a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php
+++ b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
diff --git a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php
index 90e9eb8cab..2e9b59b7bf 100644
--- a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php
+++ b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php
@@ -10,7 +10,6 @@
namespace PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
-use PHP_CodeSniffer\Util\Tokens;
class ClassCommentSniff extends FileCommentSniff
{
@@ -27,6 +26,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
@@ -47,10 +47,28 @@ public function process(File $phpcsFile, $stackPtr)
$type = strtolower($tokens[$stackPtr]['content']);
$errorData = [$type];
- $find = Tokens::$methodPrefixes;
- $find[] = T_WHITESPACE;
+ $find = [
+ T_ABSTRACT => T_ABSTRACT,
+ T_FINAL => T_FINAL,
+ T_READONLY => T_READONLY,
+ T_WHITESPACE => T_WHITESPACE,
+ ];
+
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($find[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
- $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
diff --git a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php
index 86f5b0044b..0009f804a6 100644
--- a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php
+++ b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php
@@ -138,17 +138,30 @@ public function process(File $phpcsFile, $stackPtr)
$commentEnd = $tokens[$commentStart]['comment_closer'];
- $nextToken = $phpcsFile->findNext(
- T_WHITESPACE,
- ($commentEnd + 1),
- null,
- true
- );
+ for ($nextToken = ($commentEnd + 1); $nextToken < $phpcsFile->numTokens; $nextToken++) {
+ if ($tokens[$nextToken]['code'] === T_WHITESPACE) {
+ continue;
+ }
+
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE
+ && isset($tokens[$nextToken]['attribute_closer']) === true
+ ) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($nextToken === $phpcsFile->numTokens) {
+ $nextToken--;
+ }
$ignore = [
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
T_FUNCTION,
T_CLOSURE,
T_PUBLIC,
@@ -157,6 +170,7 @@ public function process(File $phpcsFile, $stackPtr)
T_FINAL,
T_STATIC,
T_ABSTRACT,
+ T_READONLY,
T_CONST,
T_PROPERTY,
];
diff --git a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
index a6a51f6b08..1bf9444784 100644
--- a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
+++ b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
@@ -16,6 +16,25 @@
class FunctionCommentSniff implements Sniff
{
+ /**
+ * Disable the check for functions with a lower visibility than the value given.
+ *
+ * Allowed values are public, protected, and private.
+ *
+ * @var string
+ */
+ public $minimumVisibility = 'private';
+
+ /**
+ * Array of methods which do not require a return type.
+ *
+ * @var array
+ */
+ public $specialMethods = [
+ '__construct',
+ '__destruct',
+ ];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -40,17 +59,40 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
+ $scopeModifier = $phpcsFile->getMethodProperties($stackPtr)['scope'];
+ if ($scopeModifier === 'protected'
+ && $this->minimumVisibility === 'public'
+ || $scopeModifier === 'private'
+ && ($this->minimumVisibility === 'public' || $this->minimumVisibility === 'protected')
+ ) {
+ return;
+ }
+
$tokens = $phpcsFile->getTokens();
- $find = Tokens::$methodPrefixes;
- $find[] = T_WHITESPACE;
+ $ignore = Tokens::$methodPrefixes;
+ $ignore[T_WHITESPACE] = T_WHITESPACE;
+
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
- $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
// Inline comments might just be closing comments for
// control structures or functions instead of function comments
// using the wrong comment type. If there is other code on the line,
// assume they relate to that code.
- $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true);
+ $prev = $phpcsFile->findPrevious($ignore, ($commentEnd - 1), null, true);
if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
$commentEnd = $prev;
}
@@ -78,9 +120,34 @@ public function process(File $phpcsFile, $stackPtr)
}
if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
- $error = 'There must be no blank lines after the function comment';
- $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
- }
+ for ($i = ($commentEnd + 1); $i < $stackPtr; $i++) {
+ if ($tokens[$i]['column'] !== 1) {
+ continue;
+ }
+
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ $error = 'There must be no blank lines after the function comment';
+ $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ while ($i < $stackPtr
+ && $tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ $phpcsFile->fixer->replaceToken($i++, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ break;
+ }
+ }//end for
+ }//end if
$commentStart = $tokens[$commentEnd]['comment_opener'];
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
@@ -117,7 +184,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
// Skip constructor and destructor.
$methodName = $phpcsFile->getDeclarationName($stackPtr);
- $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');
+ $isSpecialMethod = in_array($methodName, $this->specialMethods, true);
$return = null;
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
@@ -132,10 +199,6 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
}
}
- if ($isSpecialMethod === true) {
- return;
- }
-
if ($return !== null) {
$content = $tokens[($return + 2)]['content'];
if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
@@ -143,6 +206,10 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
$phpcsFile->addError($error, $return, 'MissingReturnType');
}
} else {
+ if ($isSpecialMethod === true) {
+ return;
+ }
+
$error = 'Missing @return tag in function comment';
$phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
}//end if
diff --git a/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php b/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php
index 5724592c69..edd2d6beb9 100644
--- a/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php
+++ b/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php
@@ -39,6 +39,7 @@ protected function getPatterns()
'} elseif (...) {EOL',
'} else {EOL',
'do {EOL',
+ 'match (...) {EOL',
];
}//end getPatterns()
diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php
index a4131fe65c..b5e8695c15 100644
--- a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php
+++ b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php
@@ -272,7 +272,7 @@ public function processSingleLineCall(File $phpcsFile, $stackPtr, $openBracket,
$requiredSpacesBeforeClose,
$spaceBeforeClose,
];
- $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeCloseBracket', $data);
+ $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeCloseBracket', $data);
if ($fix === true) {
$padding = str_repeat(' ', $requiredSpacesBeforeClose);
@@ -390,11 +390,14 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $
$padding = str_repeat(' ', $functionIndent);
if ($foundFunctionIndent === 0) {
$phpcsFile->fixer->addContentBefore($first, $padding);
+ } else if ($tokens[$first]['code'] === T_INLINE_HTML) {
+ $newContent = $padding.ltrim($tokens[$first]['content']);
+ $phpcsFile->fixer->replaceToken($first, $newContent);
} else {
$phpcsFile->fixer->replaceToken(($first - 1), $padding);
}
}
- }
+ }//end if
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true);
if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) {
@@ -581,7 +584,7 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $
if ($inArg === false) {
$argStart = $nextCode;
- $argEnd = $phpcsFile->findEndOfStatement($nextCode);
+ $argEnd = $phpcsFile->findEndOfStatement($nextCode, [T_COLON]);
}
}//end if
@@ -618,7 +621,7 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $
}//end if
$argStart = $next;
- $argEnd = $phpcsFile->findEndOfStatement($next);
+ $argEnd = $phpcsFile->findEndOfStatement($next, [T_COLON]);
}//end if
}//end for
diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
index 9843629454..14479e2c0a 100644
--- a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
+++ b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
@@ -456,19 +456,23 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct
}
// We changed lines, so this should be a whitespace indent token.
- if ($tokens[$i]['code'] !== T_WHITESPACE) {
- $foundIndent = 0;
- } else if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) {
- // This is an empty line, so don't check the indent.
- $foundIndent = $expectedIndent;
-
+ $foundIndent = 0;
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
$error = 'Blank lines are not allowed in a multi-line '.$type.' declaration';
$fix = $phpcsFile->addFixableError($error, $i, 'EmptyLine');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($i, '');
}
- } else {
+
+ // This is an empty line, so don't check the indent.
+ continue;
+ } else if ($tokens[$i]['code'] === T_WHITESPACE) {
+ $foundIndent = $tokens[$i]['length'];
+ } else if ($tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE) {
$foundIndent = $tokens[$i]['length'];
+ ++$expectedIndent;
}
if ($expectedIndent !== $foundIndent) {
@@ -492,6 +496,19 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct
$lastLine = $tokens[$i]['line'];
}//end if
+ if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS
+ && isset($tokens[$i]['parenthesis_closer']) === true
+ ) {
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true);
+ if ($tokens[$prevNonEmpty]['code'] !== T_USE) {
+ // Since PHP 8.1, a default value can contain a class instantiation.
+ // Skip over these "function calls" as they have their own indentation rules.
+ $i = $tokens[$i]['parenthesis_closer'];
+ $lastLine = $tokens[$i]['line'];
+ continue;
+ }
+ }
+
if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) {
// Skip arrays as they have their own indentation rules.
if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) {
@@ -503,6 +520,13 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct
$lastLine = $tokens[$i]['line'];
continue;
}
+
+ if ($tokens[$i]['code'] === T_ATTRIBUTE) {
+ // Skip attributes as they have their own indentation rules.
+ $i = $tokens[$i]['attribute_closer'];
+ $lastLine = $tokens[$i]['line'];
+ continue;
+ }
}//end for
}//end processArgumentList()
diff --git a/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php
index 34ca2830b5..00e68bfec5 100644
--- a/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php
+++ b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
diff --git a/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php
index 49486a54a3..e7f87d44dd 100644
--- a/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php
+++ b/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php
@@ -23,21 +23,23 @@ class ValidFunctionNameSniff extends AbstractScopeSniff
* @var array
*/
protected $magicMethods = [
- 'construct' => true,
- 'destruct' => true,
- 'call' => true,
- 'callstatic' => true,
- 'get' => true,
- 'set' => true,
- 'isset' => true,
- 'unset' => true,
- 'sleep' => true,
- 'wakeup' => true,
- 'tostring' => true,
- 'set_state' => true,
- 'clone' => true,
- 'invoke' => true,
- 'debuginfo' => true,
+ 'construct' => true,
+ 'destruct' => true,
+ 'call' => true,
+ 'callstatic' => true,
+ 'get' => true,
+ 'set' => true,
+ 'isset' => true,
+ 'unset' => true,
+ 'sleep' => true,
+ 'wakeup' => true,
+ 'serialize' => true,
+ 'unserialize' => true,
+ 'tostring' => true,
+ 'invoke' => true,
+ 'set_state' => true,
+ 'clone' => true,
+ 'debuginfo' => true,
];
/**
diff --git a/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php b/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php
index 1929b3bbe8..fb1b79a329 100644
--- a/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php
+++ b/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php
@@ -29,6 +29,16 @@ class ObjectOperatorIndentSniff implements Sniff
*/
public $multilevel = false;
+ /**
+ * Tokens to listen for.
+ *
+ * @var array
+ */
+ private $targets = [
+ T_OBJECT_OPERATOR,
+ T_NULLSAFE_OBJECT_OPERATOR,
+ ];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -37,7 +47,7 @@ class ObjectOperatorIndentSniff implements Sniff
*/
public function register()
{
- return [T_OBJECT_OPERATOR];
+ return $this->targets;
}//end register()
@@ -57,14 +67,14 @@ public function process(File $phpcsFile, $stackPtr)
// Make sure this is the first object operator in a chain of them.
$start = $phpcsFile->findStartOfStatement($stackPtr);
- $prev = $phpcsFile->findPrevious(T_OBJECT_OPERATOR, ($stackPtr - 1), $start);
+ $prev = $phpcsFile->findPrevious($this->targets, ($stackPtr - 1), $start);
if ($prev !== false) {
return;
}
// Make sure this is a chained call.
$end = $phpcsFile->findEndOfStatement($stackPtr);
- $next = $phpcsFile->findNext(T_OBJECT_OPERATOR, ($stackPtr + 1), $end);
+ $next = $phpcsFile->findNext($this->targets, ($stackPtr + 1), $end);
if ($next === false) {
// Not a chained call.
return;
@@ -179,7 +189,7 @@ public function process(File $phpcsFile, $stackPtr)
}//end if
$next = $phpcsFile->findNext(
- T_OBJECT_OPERATOR,
+ $this->targets,
($next + 1),
null,
false,
diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc
index d97ef4d29d..6942944b5c 100644
--- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc
+++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc
@@ -110,3 +110,5 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) {
{
}
}
+
+enum IncorrectBracePlacement {}
diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed
index 5b0a2f93fb..26688b1527 100644
--- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed
+++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed
@@ -119,3 +119,7 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) {
{
}
}
+
+enum IncorrectBracePlacement
+{
+}
diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php
index 1514a70378..4c1d28e733 100644
--- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php
+++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php
@@ -61,6 +61,7 @@ public function getErrorList($testFile='')
99 => 1,
104 => 1,
110 => 1,
+ 114 => 1,
];
default:
diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc
index 0ca87512f8..e340ff0041 100644
--- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc
@@ -118,3 +118,46 @@ trait Empty_Trait_Doc
{
}//end trait
+
+
+/**
+ *
+ *
+ */
+enum Empty_Enum_Doc
+{
+
+}//end enum
+
+
+/**
+ * Sample class comment
+ *
+ * @category PHP
+ * @package PHP_CodeSniffer
+ * @author Greg Sherwood
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ * @link http://pear.php.net/package/PHP_CodeSniffer
+ */
+#[Authenticate('admin_logged_in')]
+class TodoController extends AbstractController implements MustBeLoggedInInterface
+{
+}
+
+/**
+ * Docblock
+ *
+ * @category PHP
+ * @package PHP_CodeSniffer
+ * @author Greg Sherwood
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ * @link http://pear.php.net/package/PHP_CodeSniffer
+ */
+abstract readonly class AbstractReadonlyWithDocblock {}
+
+/*
+ * Docblock
+ */
+readonly class ReadonlyWrongStyle {}
+
+readonly final class ReadonlyFinalWithoutDocblock {}
diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php
index 9a4bcf7dd6..73465fadc9 100644
--- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php
+++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php
@@ -44,6 +44,9 @@ public function getErrorList()
96 => 5,
106 => 5,
116 => 5,
+ 126 => 5,
+ 161 => 1,
+ 163 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.1.inc
similarity index 100%
rename from src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.inc
rename to src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.1.inc
diff --git a/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc
new file mode 100644
index 0000000000..8845eb1906
--- /dev/null
+++ b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc
@@ -0,0 +1,9 @@
+
*/
- public function getErrorList()
+ public function getErrorList($testFile='FileCommentUnitTest.inc')
{
- return [
- 21 => 1,
- 23 => 2,
- 24 => 1,
- 26 => 1,
- 28 => 1,
- 29 => 1,
- 30 => 1,
- 31 => 1,
- 32 => 2,
- 33 => 1,
- 34 => 1,
- 35 => 1,
- 40 => 2,
- 41 => 2,
- 43 => 1,
- ];
+ switch ($testFile) {
+ case 'FileCommentUnitTest.1.inc':
+ return [
+ 21 => 1,
+ 23 => 2,
+ 24 => 1,
+ 26 => 1,
+ 28 => 1,
+ 29 => 1,
+ 30 => 1,
+ 31 => 1,
+ 32 => 2,
+ 33 => 1,
+ 34 => 1,
+ 35 => 1,
+ 40 => 2,
+ 41 => 2,
+ 43 => 1,
+ ];
+
+ case 'FileCommentUnitTest.2.inc':
+ case 'FileCommentUnitTest.3.inc':
+ case 'FileCommentUnitTest.4.inc':
+ return [1 => 1];
+
+ default:
+ return [];
+ }//end switch
}//end getErrorList()
@@ -52,16 +65,24 @@ public function getErrorList()
* The key of the array should represent the line number and the value
* should represent the number of warnings that should occur on that line.
*
+ * @param string $testFile The name of the file being tested.
+ *
* @return array
*/
- public function getWarningList()
+ public function getWarningList($testFile='FileCommentUnitTest.inc')
{
- return [
- 29 => 1,
- 30 => 1,
- 34 => 1,
- 43 => 1,
- ];
+ switch ($testFile) {
+ case 'FileCommentUnitTest.1.inc':
+ return [
+ 29 => 1,
+ 30 => 1,
+ 34 => 1,
+ 43 => 1,
+ ];
+
+ default:
+ return [];
+ }//end switch
}//end getWarningList()
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
index 461b490775..a20ba3a720 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
@@ -381,3 +381,132 @@ public function setTranslator($a, &$b): void
{
$this->translator = $translator;
}
+
+// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility protected
+private function setTranslator2($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility public
+protected function setTranslator3($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+private function setTranslator4($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+class Bar {
+ /**
+ * The PHP5 constructor
+ *
+ * @return
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[]
+class Bar {
+ /**
+ * The PHP5 constructor
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] ignored
+/**
+ * Should be ok
+ */
+public function ignored() {
+
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+class Something implements JsonSerializable {
+ /**
+ * Single attribute.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function jsonSerialize() {}
+
+ /**
+ * Multiple attributes.
+ *
+ * @return Something
+ */
+ #[AttributeA]
+ #[AttributeB]
+ public function methodName() {}
+
+ /**
+ * Blank line between docblock and attribute.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionA() {}
+
+ /**
+ * Blank line between attribute and function declaration.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionB() {}
+
+ /**
+ * Blank line between both docblock and attribute and attribute and function declaration.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionC() {}
+}
+
+class SpacingAfter {
+ /**
+ * There are multiple blank lines between this comment and the next function.
+ *
+ * @return void
+ */
+
+
+
+
+
+
+
+
+ public function multipleBlankLines() {}
+
+ /**
+ * There are multiple blank lines, and some "empty" lines with only
+ * spaces/tabs between this comment and the next function.
+ *
+ * @return void
+ */
+
+
+
+
+
+
+
+
+
+ public function multipleLinesSomeEmpty() {}
+}
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
index 80bf63f7be..fc6d4f7e71 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
@@ -381,3 +381,111 @@ public function setTranslator($a, &$b): void
{
$this->translator = $translator;
}
+
+// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility protected
+private function setTranslator2($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility public
+protected function setTranslator3($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+private function setTranslator4($a, &$b): void
+{
+ $this->translator = $translator;
+}
+
+class Bar {
+ /**
+ * The PHP5 constructor
+ *
+ * @return
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[]
+class Bar {
+ /**
+ * The PHP5 constructor
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] ignored
+/**
+ * Should be ok
+ */
+public function ignored() {
+
+}
+
+// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+class Something implements JsonSerializable {
+ /**
+ * Single attribute.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function jsonSerialize() {}
+
+ /**
+ * Multiple attributes.
+ *
+ * @return Something
+ */
+ #[AttributeA]
+ #[AttributeB]
+ public function methodName() {}
+
+ /**
+ * Blank line between docblock and attribute.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionA() {}
+
+ /**
+ * Blank line between attribute and function declaration.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionB() {}
+
+ /**
+ * Blank line between both docblock and attribute and attribute and function declaration.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionC() {}
+}
+
+class SpacingAfter {
+ /**
+ * There are multiple blank lines between this comment and the next function.
+ *
+ * @return void
+ */
+ public function multipleBlankLines() {}
+
+ /**
+ * There are multiple blank lines, and some "empty" lines with only
+ * spaces/tabs between this comment and the next function.
+ *
+ * @return void
+ */
+ public function multipleLinesSomeEmpty() {}
+}
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
index 2f27c18710..e7ec800d02 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
@@ -68,6 +68,13 @@ public function getErrorList()
361 => 1,
363 => 1,
364 => 1,
+ 406 => 1,
+ 417 => 1,
+ 455 => 1,
+ 464 => 1,
+ 473 => 1,
+ 485 => 1,
+ 501 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc
index 66ab925535..cc9903aa13 100644
--- a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc
+++ b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc
@@ -156,4 +156,10 @@ if ($i == 0) {
}
else {
}
-?>
\ No newline at end of file
+
+// match
+$r = match ($x) {
+ 1 => 1,
+};
+
+$r = match( $x ){ 1 => 1 };
diff --git a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php
index 5cb267672b..98c3463b7b 100644
--- a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php
+++ b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php
@@ -48,6 +48,7 @@ public function getErrorList()
133 => 2,
147 => 1,
157 => 1,
+ 165 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc
index 6b2c898c42..612748fedf 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc
@@ -525,3 +525,45 @@ return trim(preg_replace_callback(
$a = ['a' => function ($b) { return $b; }];
$a['a']( 1 );
+
+// PHP 8.0 named parameters.
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+
+array_fill_keys(
+ keys: range( 1,
+ 12,
+ ), value: true,
+);
+
+// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments false
+array_fill_keys(
+ keys: range( 1,
+ 12,
+ ), value: true,
+);
+// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true
+
+?>
+
+
+
+
+
+
+
+content
+
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed
index 7396441878..00226de562 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed
@@ -537,3 +537,48 @@ return trim(
$a = ['a' => function ($b) { return $b; }];
$a['a'](1);
+
+// PHP 8.0 named parameters.
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ), value: true,
+);
+
+// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments false
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true
+
+?>
+
+
+
+
+
+
+
+content
+
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php
index c6db5b08e5..4984de2bf6 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php
+++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php
@@ -126,6 +126,14 @@ public function getErrorList($testFile='FunctionCallSignatureUnitTest.inc')
523 => 1,
524 => 3,
527 => 2,
+ 539 => 1,
+ 540 => 1,
+ 546 => 1,
+ 547 => 1,
+ 548 => 1,
+ 559 => 1,
+ 567 => 1,
+ 568 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
index 7af7200017..fb7cb52f4d 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
@@ -314,3 +314,154 @@ if(true) {
;
}
}
+
+class ConstructorPropertyPromotionSingleLineDocblockIndentOK
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+/**
+ * @var string
+ * @Assert\NotBlank()
+ */
+#[NotBlank]
+private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesOK
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesIncorrectIndent
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration.
+function usingNewInInitializersCallParamsIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA(),
+ new InjectedDependencyB
+ )
+) {}
+
+function usingNewInInitializersCallParamsNotIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA,
+ new InjectedDependencyB()
+ )
+) {}
+
+function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+new InjectedDependencyA(), new InjectedDependencyB()
+)
+) {}
+
+class UsingNewInInitializers {
+ public function doSomething(
+ object $paramA,
+ stdClass $paramB = new stdClass(),
+ Exception $paramC = new Exception(
+ new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+
+ public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ Exception $param = new Exception(
+new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+}
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
index b993e3f079..f82b5665a5 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
@@ -312,3 +312,154 @@ if(true) {
abstract function baz();
}
}
+
+class ConstructorPropertyPromotionSingleLineDocblockIndentOK
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesOK
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesIncorrectIndent
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration.
+function usingNewInInitializersCallParamsIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA(),
+ new InjectedDependencyB
+ )
+) {}
+
+function usingNewInInitializersCallParamsNotIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA,
+ new InjectedDependencyB()
+ )
+) {}
+
+function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+new InjectedDependencyA(), new InjectedDependencyB()
+)
+) {}
+
+class UsingNewInInitializers {
+ public function doSomething(
+ object $paramA,
+ stdClass $paramB = new stdClass(),
+ Exception $paramC = new Exception(
+ new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+
+ public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ Exception $param = new Exception(
+new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+}
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
index 7a8a53a1c3..01ab3e8482 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
@@ -82,6 +82,23 @@ public function getErrorList($testFile='FunctionDeclarationUnitTest.inc')
309 => 1,
313 => 1,
314 => 1,
+ 350 => 1,
+ 351 => 1,
+ 352 => 1,
+ 353 => 1,
+ 361 => 1,
+ 362 => 1,
+ 363 => 1,
+ 364 => 1,
+ 365 => 1,
+ 366 => 1,
+ 367 => 1,
+ 368 => 1,
+ 369 => 1,
+ 370 => 1,
+ 371 => 1,
+ 402 => 1,
+ 406 => 1,
];
} else {
$errors = [
diff --git a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc
index c9233734fc..8f8d64ab73 100644
--- a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc
@@ -100,5 +100,20 @@ $closure = function(array $arg2=array(), array $arg1) {}
$fn = fn($a = [], $b) => $a[] = $b;
+class OnlyConstructorPropertyPromotion {
+ public function __construct(
+ public string $name = '',
+ protected $bar
+ ) {}
+}
+
+class ConstructorPropertyPromotionMixedWithNormalParams {
+ public function __construct(
+ public string $name = '',
+ ?int $optionalParam = 0,
+ mixed $requiredParam,
+ ) {}
+}
+
// Intentional syntax error. Must be last thing in the file.
function
diff --git a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php
index 73489a35c0..60d261cbbb 100644
--- a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php
+++ b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php
@@ -35,6 +35,8 @@ public function getErrorList()
91 => 1,
99 => 1,
101 => 1,
+ 106 => 1,
+ 114 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc
index c6d15df7f0..053a4fee2f 100644
--- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc
+++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc
@@ -66,3 +66,25 @@ trait _Invalid_Name {}
trait ___ {}
trait Invalid__Name {}
+
+enum Valid_Name: string {}
+
+enum invalid_Name : String {}
+
+enum invalid_name {}
+
+enum Invalid_name: Int {}
+
+enum VALID_Name {}
+
+enum VALID_NAME {}
+
+enum VALID_Name : int {}
+
+enum ValidName {}
+
+enum _Invalid_Name {}
+
+enum ___ {}
+
+enum Invalid__Name {}
diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php
index 8ff9c674b2..54ee74aac1 100644
--- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php
+++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php
@@ -44,6 +44,12 @@ public function getErrorList()
64 => 1,
66 => 2,
68 => 1,
+ 72 => 1,
+ 74 => 2,
+ 76 => 1,
+ 86 => 1,
+ 88 => 2,
+ 90 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc
index 78280cdd90..18b1a48176 100644
--- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc
+++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc
@@ -220,3 +220,24 @@ abstract class My_Class {
public function my_class() {}
public function _MY_CLASS() {}
}
+
+enum Suit: string implements Colorful, CardGame {
+ // Magic methods.
+ function __call($name, $args) {}
+ static function __callStatic($name, $args) {}
+ function __invoke() {}
+
+ // Valid Method Name.
+ public function parseMyDSN() {}
+ private function _getAnotherValue() {}
+
+ // Double underscore non-magic methods not allowed.
+ function __myFunction() {}
+ function __my_function() {}
+
+ // Non-camelcase.
+ public function get_some_value() {}
+
+ // Private without underscore prefix.
+ private function getMe() {}
+}
diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php
index 9bb6de0d84..4639a1e2ab 100644
--- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php
+++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php
@@ -122,6 +122,10 @@ public function getErrorList()
212 => 1,
213 => 1,
214 => 1,
+ 235 => 1,
+ 236 => 2,
+ 239 => 1,
+ 242 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc
index 70fd3d8ccb..b1b09d9323 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc
@@ -110,3 +110,33 @@ $rootNode
->five();
// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false
+
+$object
+ ?->setBar($foo)
+ ?->setFoo($bar);
+
+$someObject?->someFunction("some", "parameter")
+->someOtherFunc(23, 42)?->
+ someOtherFunc2($one, $two)
+
+->someOtherFunc3(23, 42)
+ ?->andAThirdFunction();
+
+// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel true
+$object
+ ?->setBar($foo)
+ ?->setFoo($bar);
+
+$someObject?->someFunction("some", "parameter")
+->someOtherFunc(23, 42)
+ ?->someOtherFunc2($one, $two)
+
+->someOtherFunc3(23, 42)
+ ?->andAThirdFunction();
+// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false
+
+$someObject
+ ->startSomething(paramName: $value)
+ ->someOtherFunc(nameA: 23, nameB: 42)
+->endSomething($value, name: $value)
+->endEverything();
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed
index dfa642f46d..5d5b77bef6 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed
@@ -110,3 +110,33 @@ $rootNode
->five();
// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false
+
+$object
+ ?->setBar($foo)
+ ?->setFoo($bar);
+
+$someObject?->someFunction("some", "parameter")
+ ->someOtherFunc(23, 42)
+ ?->someOtherFunc2($one, $two)
+
+ ->someOtherFunc3(23, 42)
+ ?->andAThirdFunction();
+
+// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel true
+$object
+ ?->setBar($foo)
+ ?->setFoo($bar);
+
+$someObject?->someFunction("some", "parameter")
+ ->someOtherFunc(23, 42)
+ ?->someOtherFunc2($one, $two)
+
+ ->someOtherFunc3(23, 42)
+ ?->andAThirdFunction();
+// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false
+
+$someObject
+ ->startSomething(paramName: $value)
+ ->someOtherFunc(nameA: 23, nameB: 42)
+ ->endSomething($value, name: $value)
+ ->endEverything();
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php
index 0ae7b72ef4..0cad3efc15 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php
@@ -44,6 +44,13 @@ public function getErrorList()
82 => 1,
95 => 1,
103 => 1,
+ 119 => 2,
+ 122 => 1,
+ 131 => 1,
+ 134 => 1,
+ 140 => 1,
+ 141 => 1,
+ 142 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
index ce211da453..3f90067920 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
@@ -144,3 +144,21 @@ switch ( $a ) {
?>
getSummaryCount(); ?>
class="empty">
+
+ 'a', 2 => 'b' };
+
+$match = match ($test) {
+ 1 => 'a',
+ 2 => 'b'
+ };
+
+enum Enum
+{
+}
+
+enum Suits {}
+
+enum Cards
+{
+ }
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
index 05c8e8a258..f369c2b2f6 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
@@ -148,3 +148,23 @@ switch ( $a ) {
getSummaryCount(); ?>
class="empty">
+
+ 'a', 2 => 'b'
+};
+
+$match = match ($test) {
+ 1 => 'a',
+ 2 => 'b'
+};
+
+enum Enum
+{
+}
+
+enum Suits {
+}
+
+enum Cards
+{
+}
diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
index 9de3a69ea7..265932549a 100644
--- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
+++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
@@ -40,6 +40,10 @@ public function getErrorList()
135 => 1,
141 => 1,
146 => 1,
+ 149 => 1,
+ 154 => 1,
+ 160 => 1,
+ 164 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php
index ac6407d6f1..3db26f6fd1 100644
--- a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php
+++ b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
@@ -50,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr)
$errorData = [strtolower($tokens[$stackPtr]['content'])];
- $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($tokens[$stackPtr]['scope_closer'] + 1));
+ $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], ($tokens[$stackPtr]['scope_closer'] + 1));
if ($nextClass !== false) {
$error = 'Each %s must be in a file by itself';
$phpcsFile->addError($error, $nextClass, 'MultipleClasses', $errorData);
@@ -59,7 +60,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->recordMetric($stackPtr, 'One class per file', 'yes');
}
- $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT], 0);
+ $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], 0);
if ($tokens[$namespace]['code'] !== T_NAMESPACE) {
$error = 'Each %s must be in a namespace of at least one level (a top-level vendor name)';
$phpcsFile->addError($error, $stackPtr, 'MissingNamespace', $errorData);
diff --git a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php
index 12508fad4c..7ad52fa708 100644
--- a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php
+++ b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php
@@ -82,6 +82,7 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens)
T_CLASS => T_CLASS,
T_INTERFACE => T_INTERFACE,
T_TRAIT => T_TRAIT,
+ T_ENUM => T_ENUM,
T_FUNCTION => T_FUNCTION,
];
@@ -168,7 +169,9 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens)
}
// Ignore function/class prefixes.
- if (isset(Tokens::$methodPrefixes[$tokens[$i]['code']]) === true) {
+ if (isset(Tokens::$methodPrefixes[$tokens[$i]['code']]) === true
+ || $tokens[$i]['code'] === T_READONLY
+ ) {
continue;
}
@@ -178,6 +181,14 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens)
continue;
}
+ // Ignore attributes.
+ if ($tokens[$i]['code'] === T_ATTRIBUTE
+ && isset($tokens[$i]['attribute_closer']) === true
+ ) {
+ $i = $tokens[$i]['attribute_closer'];
+ continue;
+ }
+
// Detect and skip over symbols.
if (isset($symbols[$tokens[$i]['code']]) === true
&& isset($tokens[$i]['scope_closer']) === true
@@ -193,6 +204,7 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens)
) {
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true);
if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR
+ && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR
&& $tokens[$prev]['code'] !== T_DOUBLE_COLON
&& $tokens[$prev]['code'] !== T_FUNCTION
) {
@@ -216,11 +228,13 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens)
&& strtolower($tokens[$i]['content']) === 'defined'
) {
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
- if ($tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS
+ if ($openBracket !== false
+ && $tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS
&& isset($tokens[$openBracket]['parenthesis_closer']) === true
) {
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true);
if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR
+ && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR
&& $tokens[$prev]['code'] !== T_DOUBLE_COLON
&& $tokens[$prev]['code'] !== T_FUNCTION
) {
diff --git a/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc
new file mode 100644
index 0000000000..302908eb12
--- /dev/null
+++ b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc
@@ -0,0 +1,3 @@
+
diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc
new file mode 100644
index 0000000000..9c1de6267b
--- /dev/null
+++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc
@@ -0,0 +1,2 @@
+define("MAXSIZE", 100);
diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc
new file mode 100644
index 0000000000..9499885b69
--- /dev/null
+++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc
@@ -0,0 +1,2 @@
+define("MAXSIZE", 100);
diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc
new file mode 100644
index 0000000000..0500d10e6b
--- /dev/null
+++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc
@@ -0,0 +1,2 @@
+defined('MINSIZE') or define("MAXSIZE", 100);
diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc
new file mode 100644
index 0000000000..588ece5840
--- /dev/null
+++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc
@@ -0,0 +1,2 @@
+defined('MINSIZE') or define("MAXSIZE", 100);
diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php
index b7728d1074..013bf7ce5a 100644
--- a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php
+++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php
@@ -67,6 +67,8 @@ public function getWarningList($testFile='')
case 'SideEffectsUnitTest.5.inc':
case 'SideEffectsUnitTest.10.inc':
case 'SideEffectsUnitTest.12.inc':
+ case 'SideEffectsUnitTest.15.inc':
+ case 'SideEffectsUnitTest.16.inc':
return [1 => 1];
default:
return [];
diff --git a/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php b/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php
index 7a33bd9471..db67fb9428 100644
--- a/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php
+++ b/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php
@@ -21,14 +21,14 @@ class AnonClassDeclarationSniff extends ClassDeclarationSniff
/**
* The PSR2 MultiLineFunctionDeclarations sniff.
*
- * @var MultiLineFunctionDeclarationSniff
+ * @var \PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\MultiLineFunctionDeclarationSniff
*/
private $multiLineSniff = null;
/**
* The Generic FunctionCallArgumentSpacing sniff.
*
- * @var FunctionCallArgumentSpacingSniff
+ * @var \PHP_CodeSniffer\Standards\Generic\Sniffs\Functions\FunctionCallArgumentSpacingSniff
*/
private $functionCallSniff = null;
diff --git a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
index f474a26b8d..2298da39f4 100644
--- a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
+++ b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
@@ -44,14 +44,16 @@ public function process(File $phpcsFile, $stackPtr)
// Find the class name.
$allowed = [
- T_STRING => T_STRING,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
- T_SELF => T_SELF,
- T_STATIC => T_STATIC,
- T_VARIABLE => T_VARIABLE,
- T_DOLLAR => T_DOLLAR,
- T_OBJECT_OPERATOR => T_OBJECT_OPERATOR,
- T_DOUBLE_COLON => T_DOUBLE_COLON,
+ T_STRING => T_STRING,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_SELF => T_SELF,
+ T_STATIC => T_STATIC,
+ T_PARENT => T_PARENT,
+ T_VARIABLE => T_VARIABLE,
+ T_DOLLAR => T_DOLLAR,
+ T_OBJECT_OPERATOR => T_OBJECT_OPERATOR,
+ T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR,
+ T_DOUBLE_COLON => T_DOUBLE_COLON,
];
$allowed += Tokens::$emptyTokens;
@@ -62,6 +64,14 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}
+ // Skip over potential attributes for anonymous classes.
+ if ($tokens[$i]['code'] === T_ATTRIBUTE
+ && isset($tokens[$i]['attribute_closer']) === true
+ ) {
+ $i = $tokens[$i]['attribute_closer'];
+ continue;
+ }
+
if ($tokens[$i]['code'] === T_OPEN_SQUARE_BRACKET
|| $tokens[$i]['code'] === T_OPEN_CURLY_BRACKET
) {
@@ -71,7 +81,7 @@ public function process(File $phpcsFile, $stackPtr)
$classNameEnd = $i;
break;
- }
+ }//end for
if ($classNameEnd === null) {
return;
@@ -87,6 +97,11 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
+ if ($classNameEnd === $stackPtr) {
+ // Failed to find the class name.
+ return;
+ }
+
$error = 'Parentheses must be used when instantiating a new class';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingParentheses');
if ($fix === true) {
diff --git a/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php
index 0f9752b1f4..fa1f8d60ed 100644
--- a/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php
+++ b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
T_FUNCTION,
];
diff --git a/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php b/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php
new file mode 100644
index 0000000000..83ffda4df0
--- /dev/null
+++ b/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php
@@ -0,0 +1,80 @@
+
+ * @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\PSR12\Sniffs\Classes;
+
+use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
+
+class OpeningBraceSpaceSniff implements Sniff
+{
+
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @return array
+ */
+ public function register()
+ {
+ return Tokens::$ooScopeTokens;
+
+ }//end register()
+
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the
+ * stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+ if (isset($tokens[$stackPtr]['scope_opener']) === false) {
+ return;
+ }
+
+ $opener = $tokens[$stackPtr]['scope_opener'];
+ $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true);
+ if ($next === false
+ || $tokens[$next]['line'] <= ($tokens[$opener]['line'] + 1)
+ ) {
+ return;
+ }
+
+ $error = 'Opening brace must not be followed by a blank line';
+ $fix = $phpcsFile->addFixableError($error, $opener, 'Found');
+ if ($fix === false) {
+ return;
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+ for ($i = ($opener + 1); $i < $next; $i++) {
+ if ($tokens[$i]['line'] === $tokens[$opener]['line']) {
+ continue;
+ }
+
+ if ($tokens[$i]['line'] === $tokens[$next]['line']) {
+ break;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+
+ }//end process()
+
+
+}//end class
diff --git a/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php b/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php
index ad663db986..b87c33918d 100644
--- a/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php
+++ b/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php
@@ -37,6 +37,7 @@ public function register()
T_WHILE,
T_SWITCH,
T_ELSEIF,
+ T_MATCH,
];
}//end register()
@@ -90,35 +91,56 @@ public function process(File $phpcsFile, $stackPtr)
break;
}
- $operators[] = $operator;
-
$prev = $phpcsFile->findPrevious(T_WHITESPACE, ($operator - 1), $parenOpener, true);
if ($prev === false) {
// Parse error.
return;
}
+ $next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true);
+ if ($next === false) {
+ // Parse error.
+ return;
+ }
+
+ $firstOnLine = false;
+ $lastOnLine = false;
+
if ($tokens[$prev]['line'] < $tokens[$operator]['line']) {
// The boolean operator is the first content on the line.
- if ($position === null) {
- $position = 'first';
- }
+ $firstOnLine = true;
+ }
- if ($position !== 'first') {
- $error = true;
- }
+ if ($tokens[$next]['line'] > $tokens[$operator]['line']) {
+ // The boolean operator is the last content on the line.
+ $lastOnLine = true;
+ }
+ if ($firstOnLine === true && $lastOnLine === true) {
+ // The operator is the only content on the line.
+ // Don't record it because we can't determine
+ // placement information from looking at it.
continue;
}
- $next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true);
- if ($next === false) {
- // Parse error.
- return;
+ $operators[] = $operator;
+
+ if ($firstOnLine === false && $lastOnLine === false) {
+ // It's in the middle of content, so we can't determine
+ // placement information from looking at it, but we may
+ // still need to process it.
+ continue;
}
- if ($tokens[$next]['line'] > $tokens[$operator]['line']) {
- // The boolean operator is the last content on the line.
+ if ($firstOnLine === true) {
+ if ($position === null) {
+ $position = 'first';
+ }
+
+ if ($position !== 'first') {
+ $error = true;
+ }
+ } else {
if ($position === null) {
$position = 'last';
}
@@ -126,8 +148,6 @@ public function process(File $phpcsFile, $stackPtr)
if ($position !== 'last') {
$error = true;
}
-
- continue;
}
} while ($operator !== false);
@@ -135,8 +155,18 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- $error = 'Boolean operators between conditions must be at the beginning or end of the line, but not both';
- $fix = $phpcsFile->addFixableError($error, $stackPtr, 'FoundMixed');
+ switch ($this->allowOnly) {
+ case 'first':
+ $error = 'Boolean operators between conditions must be at the beginning of the line';
+ break;
+ case 'last':
+ $error = 'Boolean operators between conditions must be at the end of the line';
+ break;
+ default:
+ $error = 'Boolean operators between conditions must be at the beginning or end of the line, but not both';
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, 'FoundMixed');
if ($fix === false) {
return;
}
diff --git a/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php b/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
index fb19f5694b..3d29c4ace1 100644
--- a/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
+++ b/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
@@ -41,6 +41,7 @@ public function register()
T_ELSE,
T_ELSEIF,
T_CATCH,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php b/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php
index c9a5955006..c3d0d0cadc 100644
--- a/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php
+++ b/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php
@@ -166,11 +166,29 @@ public function getHeaderLines(File $phpcsFile, $stackPtr)
}
// Make sure this is not a code-level docblock.
- $end = $tokens[$next]['comment_closer'];
- $docToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
+ $end = $tokens[$next]['comment_closer'];
+ for ($docToken = ($end + 1); $docToken < $phpcsFile->numTokens; $docToken++) {
+ if (isset(Tokens::$emptyTokens[$tokens[$docToken]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$docToken]['code'] === T_ATTRIBUTE
+ && isset($tokens[$docToken]['attribute_closer']) === true
+ ) {
+ $docToken = $tokens[$docToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($docToken === $phpcsFile->numTokens) {
+ $docToken--;
+ }
if (isset($commentOpeners[$tokens[$docToken]['code']]) === false
&& isset(Tokens::$methodPrefixes[$tokens[$docToken]['code']]) === false
+ && $tokens[$docToken]['code'] !== T_READONLY
) {
// Check for an @var annotation.
$annotation = false;
diff --git a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php
index f4d63d4a36..8d90734311 100644
--- a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php
+++ b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php
@@ -26,6 +26,7 @@ class NullableTypeDeclarationSniff implements Sniff
T_CALLABLE => true,
T_SELF => true,
T_PARENT => true,
+ T_STATIC => true,
];
diff --git a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php
index 8a1000f72f..7f63d1e87a 100644
--- a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php
+++ b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php
@@ -47,7 +47,10 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+ $ignore = Tokens::$emptyTokens;
+ $ignore[] = T_FINAL;
+
+ $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
if (isset(Tokens::$scopeModifiers[$tokens[$prev]['code']]) === true) {
return;
}
diff --git a/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php
index 3f65602e6e..cc162b290b 100644
--- a/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php
+++ b/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php
@@ -32,7 +32,7 @@ public function getErrorList()
32 => 1,
33 => 1,
34 => 1,
- 35 => 2,
+ 35 => 1,
36 => 1,
37 => 3,
39 => 1,
diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
index dcd849e6bd..9fd1548072 100644
--- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
@@ -32,3 +32,16 @@ $foo = new $bar['a'] [$baz['a']/* comment */ ['b']]['b'];
$a = new self::$transport[$cap_string];
$renderer = new $this->inline_diff_renderer;
$a = new ${$varHoldingClassName};
+
+$class = new $obj?->classname();
+$class = new $obj?->classname;
+$class = new ${$obj?->classname};
+
+// Issue 3456.
+// Anon classes should be skipped, even when there is an attribute between the new and the class keywords.
+$anonWithAttribute = new #[SomeAttribute('summary')] class {
+ public const SOME_STUFF = 'foo';
+};
+
+$foo = new parent();
+$foo = new parent;
diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
index d8438dac13..aa9d0c7209 100644
--- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
@@ -32,3 +32,16 @@ $foo = new $bar['a'] [$baz['a']/* comment */ ['b']]['b']();
$a = new self::$transport[$cap_string]();
$renderer = new $this->inline_diff_renderer();
$a = new ${$varHoldingClassName}();
+
+$class = new $obj?->classname();
+$class = new $obj?->classname();
+$class = new ${$obj?->classname}();
+
+// Issue 3456.
+// Anon classes should be skipped, even when there is an attribute between the new and the class keywords.
+$anonWithAttribute = new #[SomeAttribute('summary')] class {
+ public const SOME_STUFF = 'foo';
+};
+
+$foo = new parent();
+$foo = new parent();
diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php
index 40c90c8306..0a16af8fc0 100644
--- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php
+++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php
@@ -41,6 +41,9 @@ public function getErrorList()
32 => 1,
33 => 1,
34 => 1,
+ 37 => 1,
+ 38 => 1,
+ 47 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc
index 1d2e92c97e..2562d26c06 100644
--- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc
@@ -45,3 +45,8 @@ $instance = new class extends \Foo implements \HandleableInterface {
$app->get('/hello/{name}', function ($name) use ($app) {
return 'Hello ' . $app->escape($name);
});
+
+enum Foo4
+{
+
+}//end
diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php
index 1deac1cdf5..d402f1bb94 100644
--- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php
+++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php
@@ -31,6 +31,7 @@ public function getErrorList()
19 => 1,
24 => 1,
31 => 1,
+ 52 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc
new file mode 100644
index 0000000000..2c41bde994
--- /dev/null
+++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc
@@ -0,0 +1,57 @@
+
+ * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\PSR12\Tests\Classes;
+
+use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
+
+class OpeningBraceSpaceUnitTest extends AbstractSniffUnitTest
+{
+
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of errors that should occur on that line.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [
+ 10 => 1,
+ 18 => 1,
+ 24 => 1,
+ 34 => 1,
+ 41 => 1,
+ 55 => 1,
+ ];
+
+ }//end getErrorList()
+
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+
+ }//end getWarningList()
+
+
+}//end class
diff --git a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc
index 3289f7eee7..4bc2eceba8 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc
+++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc
@@ -108,3 +108,24 @@ if (
) {
// elseif body
}
+
+if (
+ ($value == 1 ||
+ $value == 2)
+ &&
+ ($value == 3 ||
+ $value == 4)
+) {
+ return 5;
+}
+
+// Reset to default.
+// phpcs:set PSR12.ControlStructures.BooleanOperatorPlacement allowOnly
+
+match (
+ $expr1
+ && $expr2 &&
+ $expr3
+) {
+ // structure body
+};
diff --git a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed
index 5e8f0c3f9b..5f4d223e84 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed
@@ -118,3 +118,24 @@ if (
) {
// elseif body
}
+
+if (
+ ($value == 1 ||
+ $value == 2)
+ &&
+ ($value == 3 ||
+ $value == 4)
+) {
+ return 5;
+}
+
+// Reset to default.
+// phpcs:set PSR12.ControlStructures.BooleanOperatorPlacement allowOnly
+
+match (
+ $expr1
+ && $expr2
+ && $expr3
+) {
+ // structure body
+};
diff --git a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php
index ffd4d68574..3eeaeebc42 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php
+++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php
@@ -35,6 +35,7 @@ public function getErrorList()
90 => 1,
98 => 1,
104 => 1,
+ 125 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
index 2cdb535650..9c037d6cd3 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
+++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
@@ -84,3 +84,17 @@ EOD
) {
break;
}
+
+match (
+ $expr1 &&
+ $expr2 &&
+ $expr3
+ ) {
+ // structure body
+};
+
+match ($expr1 &&
+$expr2 &&
+ $expr3) {
+ // structure body
+};
diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
index 7f6fbf9b06..7ea61b1e2a 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
@@ -85,3 +85,19 @@ EOD
) {
break;
}
+
+match (
+ $expr1 &&
+ $expr2 &&
+ $expr3
+) {
+ // structure body
+};
+
+match (
+ $expr1 &&
+ $expr2 &&
+ $expr3
+) {
+ // structure body
+};
diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
index c2c0ea9251..6ee076f2ea 100644
--- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
+++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
@@ -41,6 +41,10 @@ public function getErrorList()
48 => 2,
58 => 1,
59 => 1,
+ 92 => 1,
+ 96 => 1,
+ 97 => 1,
+ 98 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc b/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc
new file mode 100644
index 0000000000..0b1ebe2315
--- /dev/null
+++ b/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc
@@ -0,0 +1,13 @@
+ 2,
58 => 2,
59 => 2,
+ 87 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc
index 2a68862c32..59ab1aa785 100644
--- a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc
@@ -62,3 +62,5 @@ function functionName(?string $arg1, ?int &$arg2):
?string {}
fn (?\DateTime $arg) : ?\DateTime => $arg;
+
+return (!$a ? [ new class { public function b(): c {} } ] : []);
diff --git a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed
index 827e03c7b4..cd79f78163 100644
--- a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed
@@ -58,3 +58,5 @@ function functionName(?string $arg1, ?int &$arg2): ?string {}
function functionName(?string $arg1, ?int &$arg2): ?string {}
fn (?\DateTime $arg): ?\DateTime => $arg;
+
+return (!$a ? [ new class { public function b(): c {} } ] : []);
diff --git a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
index 82c68f2585..c067e6a2a8 100644
--- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
@@ -56,3 +56,22 @@ function name($a = -1) {}
$a =& $ref;
$a = [ 'a' => &$something ];
+
+$fn = fn(array &$one) => 1;
+$fn = fn(array & $one) => 1;
+
+$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
+
+function issue3267(string|int ...$values) {}
+
+function setDefault(#[ImportValue(
+ constraints: [
+ [
+ Assert\Type::class,
+ ['type' => 'bool'],
+ ],
+ ]
+ )] ?bool $value = null): void
+ {
+ // Do something
+ }
diff --git a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
index abab60279d..76764291fa 100644
--- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
@@ -56,3 +56,22 @@ function name($a = -1) {}
$a =& $ref;
$a = [ 'a' => &$something ];
+
+$fn = fn(array &$one) => 1;
+$fn = fn(array & $one) => 1;
+
+$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
+
+function issue3267(string|int ...$values) {}
+
+function setDefault(#[ImportValue(
+ constraints: [
+ [
+ Assert\Type::class,
+ ['type' => 'bool'],
+ ],
+ ]
+ )] ?bool $value = null): void
+ {
+ // Do something
+ }
diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc
index c07b1b91eb..84ea24b2e8 100644
--- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc
@@ -5,3 +5,18 @@ class Foo {
}
const APPLICATION_ENV = 'development';
+
+// Issue 3526, PHP 8.1 final constants.
+class SampleEnum
+{
+ final const FOO = 'SAMPLE';
+
+ public final const BAR = 'SAMPLE';
+
+ final private const BAZ = 'SAMPLE';
+}
+
+enum SomeEnum {
+ public const BAR = 'bar';
+ const BAZ = 'baz';
+}
diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php
index 3917eb6821..b738706da2 100644
--- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php
+++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php
@@ -40,7 +40,11 @@ public function getErrorList()
*/
public function getWarningList()
{
- return [4 => 1];
+ return [
+ 4 => 1,
+ 12 => 1,
+ 21 => 1,
+ ];
}//end getWarningList()
diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc
index e62489fe03..c8ad746a73 100644
--- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc
@@ -207,3 +207,15 @@ class Foo implements Bar
*/
use Baz;
}
+
+enum SomeEnum1
+{
+ use FirstTrait;
+}
+
+enum SomeEnum2
+{
+
+ use FirstTrait;
+
+}
diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed
index 0090b4e931..1c5d8185c0 100644
--- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed
@@ -201,3 +201,13 @@ class Foo implements Bar
*/
use Baz;
}
+
+enum SomeEnum1
+{
+ use FirstTrait;
+}
+
+enum SomeEnum2
+{
+ use FirstTrait;
+}
diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php
index c406805a9b..797a2912e7 100644
--- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php
+++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php
@@ -47,6 +47,7 @@ public function getErrorList()
165 => 1,
170 => 1,
208 => 1,
+ 219 => 3,
];
}//end getErrorList()
diff --git a/src/Standards/PSR12/ruleset.xml b/src/Standards/PSR12/ruleset.xml
index 312f27fc70..ce8b71a756 100644
--- a/src/Standards/PSR12/ruleset.xml
+++ b/src/Standards/PSR12/ruleset.xml
@@ -41,11 +41,7 @@
-
-
-
-
-
+
0
@@ -127,6 +123,7 @@
+
diff --git a/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml b/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml
new file mode 100644
index 0000000000..60d5e7fbaa
--- /dev/null
+++ b/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ ]]>
+
+
+ ?>
+ ]]>
+
+
+
diff --git a/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml b/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml
new file mode 100644
index 0000000000..257bcab0b2
--- /dev/null
+++ b/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+ ($bar, $baz);
+ ]]>
+
+
+ ( $bar, $baz );
+ ]]>
+
+
+
+
+ $bar,
+ $baz
+);
+ ]]>
+
+
+ $bar,
+ $baz
+);
+ ]]>
+
+
+
+
+ );
+ ]]>
+
+
+ );
+ ]]>
+
+
+
+
+ $bar,
+ $baz
+);
+ ]]>
+
+
+ $bar,
+ $baz
+);
+ ]]>
+
+
+
+
+ $baz
+);
+ ]]>
+
+
+ $baz
+);
+ ]]>
+
+
+
+
+
+
+
+
+ $baz
+);
+ ]]>
+
+
+
diff --git a/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml b/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml
new file mode 100644
index 0000000000..3b1b6555e5
--- /dev/null
+++ b/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ }
+ ]]>
+
+
+
+}
+ ]]>
+
+
+
diff --git a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php
index f96b004906..c620582d41 100644
--- a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php
+++ b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php
@@ -71,7 +71,7 @@ public function processOpen(File $phpcsFile, $stackPtr)
$blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar));
$spaces = strlen($blankSpace);
- if (in_array($tokens[($stackPtr - 2)]['code'], [T_ABSTRACT, T_FINAL], true) === true
+ if (in_array($tokens[($stackPtr - 2)]['code'], [T_ABSTRACT, T_FINAL, T_READONLY], true) === true
&& $spaces !== 1
) {
$prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
@@ -89,6 +89,7 @@ public function processOpen(File $phpcsFile, $stackPtr)
}
} else if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT
|| $tokens[($stackPtr - 2)]['code'] === T_FINAL
+ || $tokens[($stackPtr - 2)]['code'] === T_READONLY
) {
$prevContent = strtolower($tokens[($stackPtr - 2)]['content']);
$error = 'Expected 1 space between %s and %s keywords; newline found';
diff --git a/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php
index 8a158d966d..aca0be2d0d 100644
--- a/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php
+++ b/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php
@@ -41,6 +41,7 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$find = Tokens::$scopeModifiers;
$find[] = T_VARIABLE;
$find[] = T_VAR;
+ $find[] = T_READONLY;
$find[] = T_SEMICOLON;
$find[] = T_OPEN_CURLY_BRACKET;
@@ -117,30 +118,70 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$phpcsFile->addError($error, $stackPtr, 'ScopeMissing', $data);
}
+ /*
+ * Note: per PSR-PER section 4.6, the order should be:
+ * - Inheritance modifier: `abstract` or `final`.
+ * - Visibility modifier: `public`, `protected`, or `private`.
+ * - Scope modifier: `static`.
+ * - Mutation modifier: `readonly`.
+ * - Type declaration.
+ * - Name.
+ *
+ * Ref: https://www.php-fig.org/per/coding-style/#46-modifier-keywords
+ *
+ * At this time (PHP 8.2), inheritance modifiers cannot be applied to properties and
+ * the `static` and `readonly` modifiers are mutually exclusive and cannot be used together.
+ *
+ * Based on that, the below modifier keyword order checks are sufficient (for now).
+ */
+
if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_static'] === true) {
$scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1));
$staticPtr = $phpcsFile->findPrevious(T_STATIC, ($stackPtr - 1));
- if ($scopePtr < $staticPtr) {
- return;
- }
+ if ($scopePtr > $staticPtr) {
+ $error = 'The static declaration must come after the visibility declaration';
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility');
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
- $error = 'The static declaration must come after the visibility declaration';
- $fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility');
- if ($fix === true) {
- $phpcsFile->fixer->beginChangeset();
+ for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
+ if ($tokens[$i]['code'] !== T_WHITESPACE) {
+ break;
+ }
- for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
- if ($tokens[$i]['code'] !== T_WHITESPACE) {
- break;
+ $phpcsFile->fixer->replaceToken($i, '');
}
- $phpcsFile->fixer->replaceToken($i, '');
+ $phpcsFile->fixer->replaceToken($scopePtr, '');
+ $phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' ');
+
+ $phpcsFile->fixer->endChangeset();
}
+ }
+ }//end if
+
+ if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_readonly'] === true) {
+ $scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1));
+ $readonlyPtr = $phpcsFile->findPrevious(T_READONLY, ($stackPtr - 1));
+ if ($scopePtr > $readonlyPtr) {
+ $error = 'The readonly declaration must come after the visibility declaration';
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ReadonlyBeforeVisibility');
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) {
+ if ($tokens[$i]['code'] !== T_WHITESPACE) {
+ break;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
- $phpcsFile->fixer->replaceToken($scopePtr, '');
- $phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' ');
+ $phpcsFile->fixer->replaceToken($scopePtr, '');
+ $phpcsFile->fixer->addContentBefore($readonlyPtr, $propertyInfo['scope'].' ');
- $phpcsFile->fixer->endChangeset();
+ $phpcsFile->fixer->endChangeset();
+ }
}
}//end if
diff --git a/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php b/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
index 7fba165acc..09d2c14a07 100644
--- a/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
+++ b/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php
@@ -47,6 +47,7 @@ public function register()
T_ELSE,
T_ELSEIF,
T_CATCH,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php
index c38a10d2d3..de81c530ee 100644
--- a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php
+++ b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php
@@ -107,13 +107,13 @@ public function process(File $phpcsFile, $stackPtr)
}
}
- $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true);
- if ($tokens[$next]['line'] === $tokens[$opener]['line']
- && ($tokens[$next]['code'] === T_COMMENT
- || isset(Tokens::$phpcsCommentTokens[$tokens[$next]['code']]) === true)
- ) {
- // Skip comments on the same line.
- $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true);
+ for ($next = ($opener + 1); $next < $nextCloser; $next++) {
+ if (isset(Tokens::$emptyTokens[$tokens[$next]['code']]) === false
+ || (isset(Tokens::$commentTokens[$tokens[$next]['code']]) === true
+ && $tokens[$next]['line'] !== $tokens[$opener]['line'])
+ ) {
+ break;
+ }
}
if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) {
@@ -126,6 +126,11 @@ public function process(File $phpcsFile, $stackPtr)
} else {
$phpcsFile->fixer->beginChangeset();
for ($i = ($opener + 1); $i < $next; $i++) {
+ if ($tokens[$i]['line'] === $tokens[$opener]['line']) {
+ // Ignore trailing comments.
+ continue;
+ }
+
if ($tokens[$i]['line'] === $tokens[$next]['line']) {
break;
}
@@ -133,10 +138,9 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->fixer->replaceToken($i, '');
}
- $phpcsFile->fixer->addNewLineBefore($i);
$phpcsFile->fixer->endChangeset();
}
- }
+ }//end if
}//end if
if ($tokens[$nextCloser]['scope_condition'] === $nextCase) {
@@ -152,7 +156,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content']));
}
} else {
- $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']);
+ $diff = ($tokens[$nextCase]['column'] + $this->indent - $tokens[$nextCloser]['column']);
if ($diff !== 0) {
$error = 'Terminating statement must be indented to the same level as the CASE body';
$fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent');
@@ -186,7 +190,7 @@ public function process(File $phpcsFile, $stackPtr)
$nextCode = $this->findNextCase($phpcsFile, ($opener + 1), $nextCloser);
if ($nextCode !== false) {
$prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true);
- if ($tokens[$prevCode]['code'] !== T_COMMENT
+ if (isset(Tokens::$commentTokens[$tokens[$prevCode]['code']]) === false
&& $this->findNestedTerminator($phpcsFile, ($opener + 1), $nextCode) === false
) {
$error = 'There must be a comment when fall-through is intentional in a non-empty case body';
@@ -229,85 +233,159 @@ private function findNextCase($phpcsFile, $stackPtr, $end)
/**
- * Returns true if a nested terminating statement is found.
+ * Returns the position of the nested terminating statement.
+ *
+ * Returns false if no terminating statement was found.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position to start looking at.
* @param int $end The position to stop looking at.
*
- * @return bool
+ * @return int|false
*/
private function findNestedTerminator($phpcsFile, $stackPtr, $end)
{
- $tokens = $phpcsFile->getTokens();
- $terminators = [
- T_RETURN,
- T_BREAK,
- T_CONTINUE,
- T_THROW,
- T_EXIT,
- ];
-
- $lastToken = $phpcsFile->findPrevious(T_WHITESPACE, ($end - 1), $stackPtr, true);
- if ($lastToken !== false) {
- if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) {
- // We found a closing curly bracket and want to check if its
- // block belongs to an IF, ELSEIF or ELSE clause. If yes, we
- // continue searching for a terminating statement within that
- // block. Note that we have to make sure that every block of
- // the entire if/else statement has a terminating statement.
- $currentCloser = $lastToken;
- $hasElseBlock = false;
- do {
- $scopeOpener = $tokens[$currentCloser]['scope_opener'];
- $scopeCloser = $tokens[$currentCloser]['scope_closer'];
-
- $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($scopeOpener - 1), $stackPtr, true);
- if ($prevToken === false) {
+ $tokens = $phpcsFile->getTokens();
+
+ $lastToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $stackPtr, true);
+ if ($lastToken === false) {
+ return false;
+ }
+
+ if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) {
+ // We found a closing curly bracket and want to check if its block
+ // belongs to a SWITCH, IF, ELSEIF or ELSE, TRY, CATCH OR FINALLY clause.
+ // If yes, we continue searching for a terminating statement within that
+ // block. Note that we have to make sure that every block of
+ // the entire if/else/switch statement has a terminating statement.
+ // For a try/catch/finally statement, either the finally block has
+ // to have a terminating statement or every try/catch block has to have one.
+ $currentCloser = $lastToken;
+ $hasElseBlock = false;
+ $hasCatchWithoutTerminator = false;
+ do {
+ $scopeOpener = $tokens[$currentCloser]['scope_opener'];
+ $scopeCloser = $tokens[$currentCloser]['scope_closer'];
+
+ $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($scopeOpener - 1), $stackPtr, true);
+ if ($prevToken === false) {
+ return false;
+ }
+
+ // SWITCH, IF, ELSEIF, CATCH clauses possess a condition we have to account for.
+ if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) {
+ $prevToken = $tokens[$prevToken]['parenthesis_owner'];
+ }
+
+ if ($tokens[$prevToken]['code'] === T_IF) {
+ // If we have not encountered an ELSE clause by now, we cannot
+ // be sure that the whole statement terminates in every case.
+ if ($hasElseBlock === false) {
return false;
}
- // IF and ELSEIF clauses possess a condition we have to account for.
- if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) {
- $prevToken = $tokens[$prevToken]['parenthesis_owner'];
+ return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ } else if ($tokens[$prevToken]['code'] === T_ELSEIF
+ || $tokens[$prevToken]['code'] === T_ELSE
+ ) {
+ // If we find a terminating statement within this block,
+ // we continue with the previous ELSEIF or IF clause.
+ $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ if ($hasTerminator === false) {
+ return false;
}
- if ($tokens[$prevToken]['code'] === T_IF) {
- // If we have not encountered an ELSE clause by now, we cannot
- // be sure that the whole statement terminates in every case.
- if ($hasElseBlock === false) {
- return false;
+ $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
+ if ($tokens[$prevToken]['code'] === T_ELSE) {
+ $hasElseBlock = true;
+ }
+ } else if ($tokens[$prevToken]['code'] === T_FINALLY) {
+ // If we find a terminating statement within this block,
+ // the whole try/catch/finally statement is covered.
+ $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ if ($hasTerminator !== false) {
+ return $hasTerminator;
+ }
+
+ // Otherwise, we continue with the previous TRY or CATCH clause.
+ $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
+ } else if ($tokens[$prevToken]['code'] === T_TRY) {
+ // If we've seen CATCH blocks without terminator statement and
+ // have not seen a FINALLY *with* a terminator statement, we
+ // don't even need to bother checking the TRY.
+ if ($hasCatchWithoutTerminator === true) {
+ return false;
+ }
+
+ return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ } else if ($tokens[$prevToken]['code'] === T_CATCH) {
+ // Keep track of seen catch statements without terminating statement,
+ // but don't bow out yet as there may still be a FINALLY clause
+ // with a terminating statement before the CATCH.
+ $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ if ($hasTerminator === false) {
+ $hasCatchWithoutTerminator = true;
+ }
+
+ $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
+ } else if ($tokens[$prevToken]['code'] === T_SWITCH) {
+ $hasDefaultBlock = false;
+ $endOfSwitch = $tokens[$prevToken]['scope_closer'];
+ $nextCase = $prevToken;
+
+ // We look for a terminating statement within every blocks.
+ while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $endOfSwitch)) !== false) {
+ if ($tokens[$nextCase]['code'] === T_DEFAULT) {
+ $hasDefaultBlock = true;
}
- return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
- } else if ($tokens[$prevToken]['code'] === T_ELSEIF
- || $tokens[$prevToken]['code'] === T_ELSE
- ) {
- // If we find a terminating statement within this block,
- // we continue with the previous ELSEIF or IF clause.
- $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
+ $opener = $tokens[$nextCase]['scope_opener'];
+
+ $nextCode = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $endOfSwitch, true);
+ if ($tokens[$nextCode]['code'] === T_CASE || $tokens[$nextCode]['code'] === T_DEFAULT) {
+ // This case statement has no content, so skip it.
+ continue;
+ }
+
+ $endOfCase = $this->findNextCase($phpcsFile, ($opener + 1), $endOfSwitch);
+ if ($endOfCase === false) {
+ $endOfCase = $endOfSwitch;
+ }
+
+ $hasTerminator = $this->findNestedTerminator($phpcsFile, ($opener + 1), $endOfCase);
if ($hasTerminator === false) {
return false;
}
+ }//end while
- $currentCloser = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), $stackPtr, true);
- if ($tokens[$prevToken]['code'] === T_ELSE) {
- $hasElseBlock = true;
- }
- } else {
+ // If we have not encountered a DEFAULT block by now, we cannot
+ // be sure that the whole statement terminates in every case.
+ if ($hasDefaultBlock === false) {
return false;
- }//end if
- } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET);
-
- return true;
- } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) {
- // We found the last statement of the CASE. Now we want to
- // check whether it is a terminating one.
- $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1));
- if (in_array($tokens[$terminator]['code'], $terminators, true) === true) {
- return $terminator;
- }
- }//end if
+ }
+
+ return $hasTerminator;
+ } else {
+ return false;
+ }//end if
+ } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET);
+
+ return true;
+ } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) {
+ // We found the last statement of the CASE. Now we want to
+ // check whether it is a terminating one.
+ $terminators = [
+ T_RETURN => T_RETURN,
+ T_BREAK => T_BREAK,
+ T_CONTINUE => T_CONTINUE,
+ T_THROW => T_THROW,
+ T_EXIT => T_EXIT,
+ ];
+
+ $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1));
+ if (isset($terminators[$tokens[$terminator]['code']]) === true) {
+ return $terminator;
+ }
}//end if
return false;
diff --git a/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php b/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php
index 241fa5a8f4..aed461f83f 100644
--- a/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php
+++ b/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php
@@ -23,7 +23,10 @@ class EndFileNewlineSniff implements Sniff
*/
public function register()
{
- return [T_OPEN_TAG];
+ return [
+ T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
+ ];
}//end register()
diff --git a/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php b/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php
index d7a6522781..406f27948a 100644
--- a/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php
+++ b/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php
@@ -48,7 +48,7 @@ public function isMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $token
$closeBracket = $tokens[$openBracket]['parenthesis_closer'];
- $end = $phpcsFile->findEndOfStatement($openBracket + 1);
+ $end = $phpcsFile->findEndOfStatement(($openBracket + 1), [T_COLON]);
while ($tokens[$end]['code'] === T_COMMA) {
// If the next bit of code is not on the same line, this is a
// multi-line function call.
@@ -61,7 +61,7 @@ public function isMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $token
return true;
}
- $end = $phpcsFile->findEndOfStatement($next);
+ $end = $phpcsFile->findEndOfStatement($next, [T_COLON]);
}
// We've reached the last argument, so see if the next content
diff --git a/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php
index 9607dccee2..bf37af09bb 100644
--- a/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php
+++ b/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php
@@ -11,6 +11,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
class NamespaceDeclarationSniff implements Sniff
{
@@ -41,6 +42,12 @@ public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($tokens[$nextNonEmpty]['code'] === T_NS_SEPARATOR) {
+ // Namespace keyword as operator. Not a declaration.
+ return;
+ }
+
$end = $phpcsFile->findEndOfStatement($stackPtr);
for ($i = ($end + 1); $i < ($phpcsFile->numTokens - 1); $i++) {
if ($tokens[$i]['line'] === $tokens[$end]['line']) {
diff --git a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php
index ea26657b03..aba9caa717 100644
--- a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php
+++ b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php
@@ -77,6 +77,10 @@ public function process(File $phpcsFile, $stackPtr)
$baseUse = 'use';
}
+ if ($tokens[($next + 1)]['code'] !== T_WHITESPACE) {
+ $baseUse .= ' ';
+ }
+
$phpcsFile->fixer->replaceToken($next, ';'.$phpcsFile->eolChar.$baseUse);
}
} else {
@@ -281,7 +285,7 @@ private function shouldIgnoreUse($phpcsFile, $stackPtr)
}
// Ignore USE keywords for traits.
- if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT]) === true) {
+ if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ENUM]) === true) {
return true;
}
diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc
index 13e596e17a..dd78809366 100644
--- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc
+++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc
@@ -239,3 +239,12 @@ C8
foo(new class {
});
+
+readonly
+class Test
+{
+}
+
+readonly class Test
+{
+}
diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
index 3ee394e789..3be473639e 100644
--- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
@@ -232,3 +232,11 @@ class C8
foo(new class {
});
+
+readonly class Test
+{
+}
+
+readonly class Test
+{
+}
diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php
index 635ac710ba..95c225edb4 100644
--- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php
+++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php
@@ -63,6 +63,8 @@ public function getErrorList()
216 => 1,
231 => 2,
235 => 1,
+ 244 => 1,
+ 248 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc
index 031d2a8378..3e086c6f22 100644
--- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc
+++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc
@@ -71,3 +71,17 @@ class MyClass
public int $var = null;
public static int/*comment*/$var = null;
}
+
+class ReadOnlyProp {
+ public readonly int $foo,
+ $bar,
+ $var = null;
+
+ protected readonly ?string $foo;
+
+ readonly array $foo;
+
+ readonly public int $wrongOrder1;
+
+ readonly protected ?string $wrongOrder2;
+}
diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed
index aca7c2fcc3..c4e22fc18b 100644
--- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed
@@ -68,3 +68,17 @@ class MyClass
public int $var = null;
public static int /*comment*/$var = null;
}
+
+class ReadOnlyProp {
+ public readonly int $foo,
+ $bar,
+ $var = null;
+
+ protected readonly ?string $foo;
+
+ readonly array $foo;
+
+ public readonly int $wrongOrder1;
+
+ protected readonly ?string $wrongOrder2;
+}
diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php
index 20da24d976..6a1778b904 100644
--- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php
+++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php
@@ -46,6 +46,11 @@ public function getErrorList()
69 => 1,
71 => 1,
72 => 1,
+ 76 => 1,
+ 80 => 1,
+ 82 => 1,
+ 84 => 1,
+ 86 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
index 24de0dc1ea..542ab3cf2c 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
+++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc
@@ -68,3 +68,14 @@ if ($expr1
&& $expr2
/* comment */ ) {
}
+
+$r = match ($x) {};
+$r = match ( $x ) {};
+
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 1
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 1
+$r = match ($x) {};
+$r = match ( $x ) {};
+$r = match ( $x ) {};
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 0
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 0
diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
index f57e792d1d..a29534beed 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed
@@ -67,3 +67,14 @@ if ($expr1
&& $expr2
/* comment */) {
}
+
+$r = match ($x) {};
+$r = match ($x) {};
+
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 1
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 1
+$r = match ( $x ) {};
+$r = match ( $x ) {};
+$r = match ( $x ) {};
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 0
+// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 0
diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
index b7b251992d..35d5e51508 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
+++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php
@@ -36,6 +36,9 @@ public function getErrorList()
60 => 1,
64 => 1,
69 => 1,
+ 73 => 2,
+ 77 => 2,
+ 79 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc
index af605cd71d..2ca60a93e8 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc
+++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc
@@ -271,3 +271,328 @@ $foo = $foo ?
}
} :
null;
+
+switch ($foo) {
+case Foo::INTERFACE:
+ echo '1';
+ return self::INTERFACE;
+case Foo::TRAIT:
+case Foo::ARRAY:
+ echo '1';
+ return self::VALUE;
+}
+
+// OK: Every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return 1;
+ default:
+ return 3;
+ }
+ case 2:
+ return 2;
+}
+
+// KO: Not every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return;
+ }
+ case 2:
+ return 2;
+}
+
+// KO: Not every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return;
+ default:
+ $a = 1;
+ }
+ case 2:
+ return 2;
+}
+
+// OK: Every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return 1;
+ default:
+ throw new \Exception();
+ }
+ case 2:
+ return 2;
+}
+
+switch ($foo) {
+ case 1:
+ // phpcs:ignore
+ case 2:
+ return 1;
+ case 3:
+ return 2;
+}
+
+// Issue 3352.
+switch ( $test ) {
+ case 2: // comment followed by empty line
+
+ break;
+
+ case 3: /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments. */
+
+
+
+ break;
+
+ case 4: /** inline docblock */
+
+
+
+ break;
+
+ case 5: /* checking how it handles */ /* two trailing comments */
+
+ break;
+
+ case 6:
+ // Comment as first content of the body.
+
+ break;
+
+ case 7:
+ /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments at start of body. */
+
+ break;
+
+ case 8:
+ /** inline docblock */
+
+ break;
+}
+
+// Handle comments correctly.
+switch ($foo) {
+ case 1:
+ if ($bar > 0) {
+ doSomething();
+ }
+ // Comment
+ else {
+ return 1;
+ }
+ case 2:
+ return 2;
+}
+
+switch ($foo) {
+ case 1:
+ if ($bar > 0) /*comment*/ {
+ return doSomething();
+ }
+ else {
+ return 1;
+ }
+ case 2:
+ return 2;
+}
+
+// Issue #3297.
+// Okay - finally will always be executed, so all branches are covered by the `return` in finally.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ doSomething();
+ } finally {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - all - non-finally - branches have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) /*comment*/ {
+ return true;
+ }
+ // Comment
+ catch (AnotherException $e) {
+ return true;
+ } finally {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - finally will always be executed, so all branches are covered by the `return` in finally.
+// Non-standard structure order.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } catch (Exception $e) {
+ doSomething();
+ } finally {
+ return true;
+ } catch (AnotherException $e) {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - all - non-finally - branches have a terminating statement.
+// Non-standard structure order.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } finally {
+ doSomething();
+ } catch (MyException $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// All okay, no finally. Any exception still uncaught will terminate the case anyhow, so we're good.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (MyException $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// All okay, no catch
+switch ( $a ) {
+ case 1:
+ try {
+ return true;
+ } finally {
+ doSomething();
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// All okay, try-catch nested in if.
+switch ( $a ) {
+ case 1:
+ if ($a) {
+ try {
+ return true; // Comment.
+ } catch (MyException $e) {
+ throw new Exception($e->getMessage());
+ }
+ } else {
+ return true;
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } finally {
+ doSomething();
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. One of the catches does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ return true;
+ } finally {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. Try does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } finally {
+ doSomething();
+ } catch (Exception $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. One of the catches does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Issue 3550 - comment after terminating statement.
+switch (rand()) {
+ case 1:
+ if (rand() === 1) {
+ break;
+ } else {
+ break; // comment
+ }
+ default:
+ break;
+}
diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed
index d5671feea3..bbc8b7c48e 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed
@@ -274,3 +274,320 @@ $foo = $foo ?
}
} :
null;
+
+switch ($foo) {
+case Foo::INTERFACE:
+ echo '1';
+ return self::INTERFACE;
+case Foo::TRAIT:
+case Foo::ARRAY:
+ echo '1';
+ return self::VALUE;
+}
+
+// OK: Every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return 1;
+ default:
+ return 3;
+ }
+ case 2:
+ return 2;
+}
+
+// KO: Not every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return;
+ }
+ case 2:
+ return 2;
+}
+
+// KO: Not every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return;
+ default:
+ $a = 1;
+ }
+ case 2:
+ return 2;
+}
+
+// OK: Every clause terminates
+switch ($foo) {
+ case 1:
+ switch ($bar) {
+ case 1:
+ return 1;
+ default:
+ throw new \Exception();
+ }
+ case 2:
+ return 2;
+}
+
+switch ($foo) {
+ case 1:
+ // phpcs:ignore
+ case 2:
+ return 1;
+ case 3:
+ return 2;
+}
+
+// Issue 3352.
+switch ( $test ) {
+ case 2: // comment followed by empty line
+ break;
+
+ case 3: /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments. */
+ break;
+
+ case 4: /** inline docblock */
+ break;
+
+ case 5: /* checking how it handles */ /* two trailing comments */
+ break;
+
+ case 6:
+ // Comment as first content of the body.
+
+ break;
+
+ case 7:
+ /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments at start of body. */
+
+ break;
+
+ case 8:
+ /** inline docblock */
+
+ break;
+}
+
+// Handle comments correctly.
+switch ($foo) {
+ case 1:
+ if ($bar > 0) {
+ doSomething();
+ }
+ // Comment
+ else {
+ return 1;
+ }
+ case 2:
+ return 2;
+}
+
+switch ($foo) {
+ case 1:
+ if ($bar > 0) /*comment*/ {
+ return doSomething();
+ }
+ else {
+ return 1;
+ }
+ case 2:
+ return 2;
+}
+
+// Issue #3297.
+// Okay - finally will always be executed, so all branches are covered by the `return` in finally.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ doSomething();
+ } finally {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - all - non-finally - branches have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) /*comment*/ {
+ return true;
+ }
+ // Comment
+ catch (AnotherException $e) {
+ return true;
+ } finally {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - finally will always be executed, so all branches are covered by the `return` in finally.
+// Non-standard structure order.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } catch (Exception $e) {
+ doSomething();
+ } finally {
+ return true;
+ } catch (AnotherException $e) {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Okay - all - non-finally - branches have a terminating statement.
+// Non-standard structure order.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } finally {
+ doSomething();
+ } catch (MyException $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// All okay, no finally. Any exception still uncaught will terminate the case anyhow, so we're good.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (MyException $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// All okay, no catch
+switch ( $a ) {
+ case 1:
+ try {
+ return true;
+ } finally {
+ doSomething();
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// All okay, try-catch nested in if.
+switch ( $a ) {
+ case 1:
+ if ($a) {
+ try {
+ return true; // Comment.
+ } catch (MyException $e) {
+ throw new Exception($e->getMessage());
+ }
+ } else {
+ return true;
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } finally {
+ doSomething();
+ }
+ case 2:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. One of the catches does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ return true;
+ } finally {
+ doSomething();
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. Try does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ doSomething();
+ } finally {
+ doSomething();
+ } catch (Exception $e) {
+ return true;
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Missing fall-through comment. One of the catches does not have a terminating statement.
+switch ( $a ) {
+ case 1:
+ try {
+ return false;
+ } catch (Exception $e) {
+ doSomething();
+ } catch (AnotherException $e) {
+ return true;
+ }
+ default:
+ $other = $code;
+ break;
+}
+
+// Issue 3550 - comment after terminating statement.
+switch (rand()) {
+ case 1:
+ if (rand() === 1) {
+ break;
+ } else {
+ break; // comment
+ }
+ default:
+ break;
+}
diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php
index 9c1735563f..0cd946d888 100644
--- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php
+++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php
@@ -47,6 +47,17 @@ public function getErrorList()
224 => 1,
236 => 1,
260 => 1,
+ 300 => 1,
+ 311 => 1,
+ 346 => 1,
+ 350 => 1,
+ 356 => 1,
+ 362 => 1,
+ 384 => 1,
+ 528 => 1,
+ 541 => 1,
+ 558 => 1,
+ 575 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc
new file mode 100644
index 0000000000..4f2e47af9c
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc
@@ -0,0 +1 @@
+= 'foo';
\ No newline at end of file
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc.fixed b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc.fixed
new file mode 100644
index 0000000000..1f87609292
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc.fixed
@@ -0,0 +1 @@
+= 'foo';
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc
new file mode 100644
index 0000000000..db6d1b8931
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc
@@ -0,0 +1 @@
+= 'foo' ?>
\ No newline at end of file
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed
new file mode 100644
index 0000000000..d3c19feeb2
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed
@@ -0,0 +1 @@
+= 'foo' ?>
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc
new file mode 100644
index 0000000000..fa2f476a92
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc
@@ -0,0 +1,5 @@
+= 'foo'
+
+
+
+
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc.fixed b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc.fixed
new file mode 100644
index 0000000000..e4016b081a
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc.fixed
@@ -0,0 +1 @@
+= 'foo'
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.14.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.14.inc
new file mode 100644
index 0000000000..d3c19feeb2
--- /dev/null
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.14.inc
@@ -0,0 +1 @@
+= 'foo' ?>
diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php
index a80c16dbed..456106fe5a 100644
--- a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php
+++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php
@@ -35,6 +35,10 @@ public function getErrorList($testFile='')
case 'EndFileNewlineUnitTest.9.inc':
case 'EndFileNewlineUnitTest.10.inc':
return [2 => 1];
+ case 'EndFileNewlineUnitTest.11.inc':
+ case 'EndFileNewlineUnitTest.12.inc':
+ case 'EndFileNewlineUnitTest.13.inc':
+ return [1 => 1];
default:
return [];
}//end switch
diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc
index dcaf00bd6a..1ca477d054 100644
--- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc
+++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc
@@ -242,3 +242,26 @@ function (array $term) use ($mode): string {
},
$search
));
+
+// PHP 8.0 named parameters.
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+
+array_fill_keys(
+ keys: range( 1,
+ 12,
+ ), value: true,
+);
+
+// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments true
+array_fill_keys(
+ keys: range( 1,
+ 12,
+ ), value: true,
+);
+// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments false
diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed
index f61b479c50..dc383ed2a7 100644
--- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed
@@ -255,3 +255,29 @@ return trim(preg_replace_callback(
},
$search
));
+
+// PHP 8.0 named parameters.
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ),
+ value: true,
+);
+
+// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments true
+array_fill_keys(
+ keys: range(
+ 1,
+ 12,
+ ), value: true,
+);
+// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments false
diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php
index c6862ca17f..1d87825891 100644
--- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php
+++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php
@@ -35,20 +35,19 @@ public function getErrorList()
103 => 1,
111 => 1,
117 => 4,
- 121 => 1,
- 125 => 1,
- 129 => 1,
- 133 => 1,
- 138 => 1,
- 146 => 1,
- 150 => 1,
- 154 => 1,
- 158 => 1,
- 162 => 1,
- 167 => 1,
- 172 => 1,
+ 123 => 1,
+ 127 => 1,
+ 131 => 1,
+ 136 => 1,
+ 143 => 1,
+ 148 => 1,
+ 152 => 1,
+ 156 => 1,
+ 160 => 1,
+ 165 => 1,
+ 170 => 1,
175 => 1,
- 178 => 1,
+ 178 => 2,
186 => 1,
187 => 1,
194 => 3,
@@ -67,6 +66,11 @@ public function getErrorList()
234 => 1,
242 => 1,
243 => 1,
+ 256 => 1,
+ 257 => 1,
+ 258 => 1,
+ 263 => 1,
+ 264 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc
index 21c03119dd..096b44bc8a 100644
--- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc
+++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc
@@ -64,3 +64,12 @@ class Nested_Function {
};
}
}
+
+enum MyEnum
+{
+ function _myFunction() {}
+ function __myFunction() {}
+ public static function myFunction() {}
+ static public function myFunction() {}
+ public function _() {}
+}
diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed
index 5fb88613ed..eae8d28f5c 100644
--- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed
@@ -64,3 +64,12 @@ class Nested_Function {
};
}
}
+
+enum MyEnum
+{
+ function _myFunction() {}
+ function __myFunction() {}
+ public static function myFunction() {}
+ public static function myFunction() {}
+ public function _() {}
+}
diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php
index 2d508d4c62..a8dcdfea58 100644
--- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php
+++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php
@@ -40,6 +40,7 @@ public function getErrorList()
54 => 1,
56 => 3,
63 => 2,
+ 73 => 1,
];
}//end getErrorList()
@@ -61,6 +62,7 @@ public function getWarningList()
30 => 1,
46 => 1,
63 => 1,
+ 70 => 1,
];
}//end getWarningList()
diff --git a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc
index 2500b6448e..703393396f 100644
--- a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc
+++ b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc
@@ -20,3 +20,7 @@ namespace Vendor\
Package;
namespace Vendor\Package;
+
+$call = namespace\function_name();
+echo namespace\CONSTANT_NAME;
+// Something which is not a blank line.
diff --git a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed
index a98985d117..0b39bfa086 100644
--- a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed
@@ -22,3 +22,7 @@ namespace Vendor\
Package;
namespace Vendor\Package;
+
+$call = namespace\function_name();
+echo namespace\CONSTANT_NAME;
+// Something which is not a blank line.
diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc
index c4e83da440..61befc9e73 100644
--- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc
+++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc
@@ -30,9 +30,14 @@ trait HelloWorld
use Hello, World;
}
+enum SomeEnum
+{
+ use Hello, World;
+}
+
$x = $foo ? function ($foo) use /* comment */ ($bar): int {
return 1;
} : $bar;
// Testcase must be on last line in the file.
-use
\ No newline at end of file
+use
diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc
index 068b5233f1..bc0d7a961e 100644
--- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc
+++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc
@@ -6,6 +6,10 @@ use My\Full\Classname as Another, My\Full\NSname;
use function My\Full\functionname as somefunction, My\Full\otherfunction;
use const My\Full\constantname as someconstant, My\Full\otherconstant;
+use BarClass as Bar,FooClass,BazClass as Baz;
+use function My\Full\functionname as somefunction,My\Full\otherfunction;
+use const My\Full\constantname as someconstant,My\Full\otherconstant;
+
namespace AnotherProject;
diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed
index 21e574ddb2..6579613b12 100644
--- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed
+++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed
@@ -9,6 +9,14 @@ use function My\Full\otherfunction;
use const My\Full\constantname as someconstant;
use const My\Full\otherconstant;
+use BarClass as Bar;
+use FooClass;
+use BazClass as Baz;
+use function My\Full\functionname as somefunction;
+use function My\Full\otherfunction;
+use const My\Full\constantname as someconstant;
+use const My\Full\otherconstant;
+
namespace AnotherProject;
diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php
index b0b47e3530..b424a2c113 100644
--- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php
+++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php
@@ -34,7 +34,10 @@ public function getErrorList($testFile='')
5 => 1,
6 => 1,
7 => 1,
- 12 => 1,
+ 9 => 1,
+ 10 => 1,
+ 11 => 1,
+ 16 => 1,
];
case 'UseDeclarationUnitTest.3.inc':
return [
diff --git a/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php
index de5ee8535d..64d5d64da2 100644
--- a/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php
+++ b/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php
@@ -374,6 +374,7 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
|| $tokens[$nextToken]['code'] === T_OPEN_SHORT_ARRAY
|| $tokens[$nextToken]['code'] === T_CLOSURE
|| $tokens[$nextToken]['code'] === T_FN
+ || $tokens[$nextToken]['code'] === T_MATCH
) {
// Let subsequent calls of this test handle nested arrays.
if ($tokens[$lastToken]['code'] !== T_DOUBLE_ARROW) {
@@ -429,9 +430,13 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
}
if ($keyUsed === true && $tokens[$lastToken]['code'] === T_COMMA) {
- $error = 'No key specified for array entry; first entry specifies key';
- $phpcsFile->addError($error, $nextToken, 'NoKeySpecified');
- return;
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($lastToken + 1), null, true);
+ // Allow for PHP 7.4+ array unpacking within an array declaration.
+ if ($tokens[$nextToken]['code'] !== T_ELLIPSIS) {
+ $error = 'No key specified for array entry; first entry specifies key';
+ $phpcsFile->addError($error, $nextToken, 'NoKeySpecified');
+ return;
+ }
}
if ($keyUsed === false) {
@@ -450,9 +455,14 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
$error = 'Expected 0 spaces before comma; %s found';
$data = [$spaceLength];
- $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeComma', $data);
- if ($fix === true) {
- $phpcsFile->fixer->replaceToken(($nextToken - 1), '');
+ // The error is only fixable if there is only whitespace between the tokens.
+ if ($prev === $phpcsFile->findPrevious(T_WHITESPACE, ($nextToken - 1), null, true)) {
+ $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeComma', $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->replaceToken(($nextToken - 1), '');
+ }
+ } else {
+ $phpcsFile->addError($error, $nextToken, 'SpaceBeforeComma', $data);
}
}
}//end if
@@ -464,8 +474,17 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
true
);
- $indices[] = ['value' => $valueContent];
- $singleUsed = true;
+ $indices[] = ['value' => $valueContent];
+ $usesArrayUnpacking = $phpcsFile->findPrevious(
+ Tokens::$emptyTokens,
+ ($nextToken - 2),
+ null,
+ true
+ );
+ if ($tokens[$usesArrayUnpacking]['code'] !== T_ELLIPSIS) {
+ // Don't decide if an array is key => value indexed or not when PHP 7.4+ array unpacking is used.
+ $singleUsed = true;
+ }
}//end if
$lastToken = $nextToken;
@@ -620,7 +639,19 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
$valuePointer = $value['value'];
- $previous = $phpcsFile->findPrevious([T_WHITESPACE, T_COMMA], ($valuePointer - 1), ($arrayStart + 1), true);
+ $ignoreTokens = [
+ T_WHITESPACE => T_WHITESPACE,
+ T_COMMA => T_COMMA,
+ ];
+ $ignoreTokens += Tokens::$castTokens;
+
+ if ($tokens[$valuePointer]['code'] === T_CLOSURE
+ || $tokens[$valuePointer]['code'] === T_FN
+ ) {
+ $ignoreTokens += [T_STATIC => T_STATIC];
+ }
+
+ $previous = $phpcsFile->findPrevious($ignoreTokens, ($valuePointer - 1), ($arrayStart + 1), true);
if ($previous === false) {
$previous = $stackPtr;
}
@@ -652,12 +683,12 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array
$found,
];
- $fix = $phpcsFile->addFixableError($error, $valuePointer, 'ValueNotAligned', $data);
+ $fix = $phpcsFile->addFixableError($error, $first, 'ValueNotAligned', $data);
if ($fix === true) {
if ($found === 0) {
- $phpcsFile->fixer->addContent(($valuePointer - 1), str_repeat(' ', $expected));
+ $phpcsFile->fixer->addContent(($first - 1), str_repeat(' ', $expected));
} else {
- $phpcsFile->fixer->replaceToken(($valuePointer - 1), str_repeat(' ', $expected));
+ $phpcsFile->fixer->replaceToken(($first - 1), str_repeat(' ', $expected));
}
}
}
diff --git a/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php
index 3d2c4db1aa..69188326d9 100644
--- a/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php
+++ b/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php
@@ -65,6 +65,7 @@ public function processOpen(File $phpcsFile, $stackPtr)
if ($tokens[($stackPtr - 2)]['code'] !== T_ABSTRACT
&& $tokens[($stackPtr - 2)]['code'] !== T_FINAL
+ && $tokens[($stackPtr - 2)]['code'] !== T_READONLY
) {
if ($spaces !== 0) {
$type = strtolower($tokens[$stackPtr]['content']);
diff --git a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php
index afbec4fa7e..88a7e0daae 100644
--- a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php
+++ b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php
@@ -27,6 +27,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
diff --git a/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php b/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php
index 5f4f78c0a2..2ea5ad8bc2 100644
--- a/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php
+++ b/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php
@@ -29,6 +29,7 @@ public function register()
$targets[] = T_IMPLEMENTS;
$targets[] = T_ABSTRACT;
$targets[] = T_FINAL;
+ $targets[] = T_READONLY;
$targets[] = T_VAR;
$targets[] = T_CONST;
diff --git a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php
index 10de719dca..ffddd2cde7 100644
--- a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php
+++ b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php
@@ -28,6 +28,7 @@ public function register()
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
];
}//end register()
@@ -58,7 +59,7 @@ public function process(File $phpcsFile, $stackPtr)
// starting with the number will be multiple tokens.
$opener = $tokens[$stackPtr]['scope_opener'];
$nameStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), $opener, true);
- $nameEnd = $phpcsFile->findNext(T_WHITESPACE, $nameStart, $opener);
+ $nameEnd = $phpcsFile->findNext([T_WHITESPACE, T_COLON], $nameStart, $opener);
if ($nameEnd === false) {
$name = $tokens[$nameStart]['content'];
} else {
diff --git a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
index ae9db7944c..eb647f5fe7 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
@@ -69,11 +69,22 @@ public function process(File $phpcsFile, $stackPtr)
// If this is a function/class/interface doc block comment, skip it.
// We are only interested in inline doc block comments.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
- $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
- $ignore = [
+ $nextToken = $stackPtr;
+ do {
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextToken + 1), null, true);
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ } while (true);
+
+ $ignore = [
T_CLASS => true,
T_INTERFACE => true,
T_TRAIT => true,
+ T_ENUM => true,
T_FUNCTION => true,
T_PUBLIC => true,
T_PRIVATE => true,
@@ -83,6 +94,7 @@ public function process(File $phpcsFile, $stackPtr)
T_ABSTRACT => true,
T_CONST => true,
T_VAR => true,
+ T_READONLY => true,
];
if (isset($ignore[$tokens[$nextToken]['code']]) === true) {
return;
@@ -363,6 +375,7 @@ public function process(File $phpcsFile, $stackPtr)
if ((isset($tokens[$contentBefore]['scope_closer']) === true
&& $tokens[$contentBefore]['scope_opener'] === $contentBefore)
|| $tokens[$contentBefore]['code'] === T_OPEN_TAG
+ || $tokens[$contentBefore]['code'] === T_OPEN_TAG_WITH_ECHO
) {
if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) !== 1) {
$error = 'Empty line not required before block comment';
diff --git a/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php
index 3aa969dc78..3f30036a8d 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php
@@ -19,7 +19,6 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
-use PHP_CodeSniffer\Util\Tokens;
class ClassCommentSniff implements Sniff
{
@@ -49,10 +48,33 @@ public function register()
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
- $find = Tokens::$methodPrefixes;
- $find[] = T_WHITESPACE;
+ $find = [
+ T_ABSTRACT => T_ABSTRACT,
+ T_FINAL => T_FINAL,
+ T_READONLY => T_READONLY,
+ T_WHITESPACE => T_WHITESPACE,
+ ];
+
+ $previousContent = null;
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($find[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($previousContent === null) {
+ $previousContent = $commentEnd;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
- $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
@@ -69,7 +91,7 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
+ if ($tokens[$previousContent]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
$error = 'There must be no blank lines after the class comment';
$phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
}
diff --git a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php
index 6ab6280f60..cd509d0c89 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php
@@ -27,6 +27,7 @@ public function register()
T_FUNCTION,
T_CLASS,
T_INTERFACE,
+ T_ENUM,
];
}//end register()
@@ -69,8 +70,10 @@ public function process(File $phpcsFile, $stackPtr)
$comment = '//end '.$decName.'()';
} else if ($tokens[$stackPtr]['code'] === T_CLASS) {
$comment = '//end class';
- } else {
+ } else if ($tokens[$stackPtr]['code'] === T_INTERFACE) {
$comment = '//end interface';
+ } else {
+ $comment = '//end enum';
}//end if
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
diff --git a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
index 2624bc2246..1f49d2c0c9 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
@@ -64,6 +64,8 @@ public function process(File $phpcsFile, $stackPtr)
$ignore = [
T_CLASS => true,
T_INTERFACE => true,
+ T_ENUM => true,
+ T_ENUM_CASE => true,
T_FUNCTION => true,
T_PUBLIC => true,
T_PRIVATE => true,
@@ -74,6 +76,7 @@ public function process(File $phpcsFile, $stackPtr)
T_OBJECT => true,
T_PROTOTYPE => true,
T_VAR => true,
+ T_READONLY => true,
];
if ($nextToken === false || isset($ignore[$tokens[$nextToken]['code']]) === false) {
diff --git a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php
index 66fc5e4db1..a1c79c6a25 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php
@@ -72,17 +72,30 @@ public function process(File $phpcsFile, $stackPtr)
$commentEnd = $tokens[$commentStart]['comment_closer'];
- $nextToken = $phpcsFile->findNext(
- T_WHITESPACE,
- ($commentEnd + 1),
- null,
- true
- );
+ for ($nextToken = ($commentEnd + 1); $nextToken < $phpcsFile->numTokens; $nextToken++) {
+ if ($tokens[$nextToken]['code'] === T_WHITESPACE) {
+ continue;
+ }
+
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE
+ && isset($tokens[$nextToken]['attribute_closer']) === true
+ ) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($nextToken === $phpcsFile->numTokens) {
+ $nextToken--;
+ }
$ignore = [
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
T_FUNCTION,
T_CLOSURE,
T_PUBLIC,
@@ -91,6 +104,7 @@ public function process(File $phpcsFile, $stackPtr)
T_FINAL,
T_STATIC,
T_ABSTRACT,
+ T_READONLY,
T_CONST,
T_PROPERTY,
T_INCLUDE,
@@ -115,7 +129,7 @@ public function process(File $phpcsFile, $stackPtr)
// Exactly one blank line after the file comment.
$next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
- if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) {
+ if ($next !== false && $tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) {
$error = 'There must be exactly one blank line after the file comment';
$phpcsFile->addError($error, $commentEnd, 'SpacingAfterComment');
}
diff --git a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
index 38fc4d0f94..3c79a7a8c4 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
@@ -17,6 +17,13 @@
class FunctionCommentSniff extends PEARFunctionCommentSniff
{
+ /**
+ * Whether to skip inheritdoc comments.
+ *
+ * @var boolean
+ */
+ public $skipIfInheritdoc = false;
+
/**
* The current PHP version.
*
@@ -40,6 +47,12 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
$tokens = $phpcsFile->getTokens();
$return = null;
+ if ($this->skipIfInheritdoc === true) {
+ if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
+ return;
+ }
+ }
+
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
if ($tokens[$tag]['content'] === '@return') {
if ($return !== null) {
@@ -54,10 +67,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
// Skip constructor and destructor.
$methodName = $phpcsFile->getDeclarationName($stackPtr);
- $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');
- if ($isSpecialMethod === true) {
- return;
- }
+ $isSpecialMethod = in_array($methodName, $this->specialMethods, true);
if ($return !== null) {
$content = $tokens[($return + 2)]['content'];
@@ -133,9 +143,12 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
}
}
}//end if
- } else if ($returnType !== 'mixed' && in_array('void', $typeNames, true) === false) {
- // If return type is not void, there needs to be a return statement
- // somewhere in the function that returns something.
+ } else if ($returnType !== 'mixed'
+ && $returnType !== 'never'
+ && in_array('void', $typeNames, true) === false
+ ) {
+ // If return type is not void, never, or mixed, there needs to be a
+ // return statement somewhere in the function that returns something.
if (isset($tokens[$stackPtr]['scope_closer']) === true) {
$endToken = $tokens[$stackPtr]['scope_closer'];
for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
@@ -168,6 +181,10 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
}//end if
}//end if
} else {
+ if ($isSpecialMethod === true) {
+ return;
+ }
+
$error = 'Missing @return tag in function comment';
$phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
}//end if
@@ -189,6 +206,12 @@ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
+ if ($this->skipIfInheritdoc === true) {
+ if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
+ return;
+ }
+ }
+
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] !== '@throws') {
continue;
@@ -225,6 +248,8 @@ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
}
}
+ $comment = trim($comment);
+
// Starts with a capital letter and ends with a fullstop.
$firstChar = $comment[0];
if (strtoupper($firstChar) !== $firstChar) {
@@ -264,6 +289,12 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
$tokens = $phpcsFile->getTokens();
+ if ($this->skipIfInheritdoc === true) {
+ if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
+ return;
+ }
+ }
+
$params = [];
$maxType = 0;
$maxVar = 0;
@@ -377,6 +408,10 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
$suggestedTypeNames = [];
foreach ($typeNames as $typeName) {
+ if ($typeName === '') {
+ continue;
+ }
+
// Strip nullable operator.
if ($typeName[0] === '?') {
$typeName = substr($typeName, 1);
@@ -419,6 +454,12 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
}
}
+ if ($this->phpVersion >= 80000) {
+ if ($suggestedName === 'mixed') {
+ $suggestedTypeHint = 'mixed';
+ }
+ }
+
if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) {
$typeHint = $realParams[$pos]['type_hint'];
@@ -517,16 +558,38 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
// Make sure the param name is correct.
if (isset($realParams[$pos]) === true) {
- $realName = $realParams[$pos]['name'];
- if ($realName !== $param['var']) {
+ $realName = $realParams[$pos]['name'];
+ $paramVarName = $param['var'];
+
+ if ($param['var'][0] === '&') {
+ // Even when passed by reference, the variable name in $realParams does not have
+ // a leading '&'. This sniff will accept both '&$var' and '$var' in these cases.
+ $paramVarName = substr($param['var'], 1);
+
+ // This makes sure that the 'MissingParamTag' check won't throw a false positive.
+ $foundParams[(count($foundParams) - 1)] = $paramVarName;
+
+ if ($realParams[$pos]['pass_by_reference'] !== true && $realName === $paramVarName) {
+ // Don't complain about this unless the param name is otherwise correct.
+ $error = 'Doc comment for parameter %s is prefixed with "&" but parameter is not passed by reference';
+ $code = 'ParamNameUnexpectedAmpersandPrefix';
+ $data = [$paramVarName];
+
+ // We're not offering an auto-fix here because we can't tell if the docblock
+ // is wrong, or the parameter should be passed by reference.
+ $phpcsFile->addError($error, $param['tag'], $code, $data);
+ }
+ }
+
+ if ($realName !== $paramVarName) {
$code = 'ParamNameNoMatch';
$data = [
- $param['var'],
+ $paramVarName,
$realName,
];
$error = 'Doc comment for parameter %s does not match ';
- if (strtolower($param['var']) === strtolower($realName)) {
+ if (strtolower($paramVarName) === strtolower($realName)) {
$error .= 'case of ';
$code = 'ParamNameNoCaseMatch';
}
@@ -534,7 +597,7 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
$error .= 'actual variable name %s';
$phpcsFile->addError($error, $param['tag'], $code, $data);
- }
+ }//end if
} else if (substr($param['var'], -4) !== ',...') {
// We must have an extra parameter comment.
$error = 'Superfluous parameter comment';
@@ -695,4 +758,40 @@ protected function checkSpacingAfterParamName(File $phpcsFile, $param, $maxVar,
}//end checkSpacingAfterParamName()
+ /**
+ * Determines whether the whole comment is an inheritdoc comment.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ * @param int $commentStart The position in the stack where the comment started.
+ *
+ * @return boolean TRUE if the docblock contains only {@inheritdoc} (case-insensitive).
+ */
+ protected function checkInheritdoc(File $phpcsFile, $stackPtr, $commentStart)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $allowedTokens = [
+ T_DOC_COMMENT_OPEN_TAG,
+ T_DOC_COMMENT_WHITESPACE,
+ T_DOC_COMMENT_STAR,
+ ];
+ for ($i = $commentStart; $i <= $tokens[$commentStart]['comment_closer']; $i++) {
+ if (in_array($tokens[$i]['code'], $allowedTokens) === false) {
+ $trimmedContent = strtolower(trim($tokens[$i]['content']));
+
+ if ($trimmedContent === '{@inheritdoc}') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return false;
+
+ }//end checkInheritdoc()
+
+
}//end class
diff --git a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
index 09b4ee2528..e3ef5c0dc1 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
@@ -59,17 +59,22 @@ public function process(File $phpcsFile, $stackPtr)
// We are only interested in inline doc block comments, which are
// not allowed.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
- $nextToken = $phpcsFile->findNext(
- Tokens::$emptyTokens,
- ($stackPtr + 1),
- null,
- true
- );
+ $nextToken = $stackPtr;
+ do {
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextToken + 1), null, true);
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ } while (true);
$ignore = [
T_CLASS,
T_INTERFACE,
T_TRAIT,
+ T_ENUM,
T_FUNCTION,
T_CLOSURE,
T_PUBLIC,
@@ -78,6 +83,7 @@ public function process(File $phpcsFile, $stackPtr)
T_FINAL,
T_STATIC,
T_ABSTRACT,
+ T_READONLY,
T_CONST,
T_PROPERTY,
T_INCLUDE,
diff --git a/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php
index 8d8c4f898a..103d90ffba 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php
@@ -38,6 +38,7 @@ class LongConditionClosingCommentSniff implements Sniff
T_WHILE,
T_TRY,
T_CASE,
+ T_MATCH,
];
/**
@@ -154,6 +155,17 @@ public function process(File $phpcsFile, $stackPtr)
} while (isset($tokens[$nextToken]['scope_closer']) === true);
}
+ if ($startCondition['code'] === T_MATCH) {
+ // Move the stackPtr to after the semi-colon/comma if there is one.
+ $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
+ if ($nextToken !== false
+ && ($tokens[$nextToken]['code'] === T_SEMICOLON
+ || $tokens[$nextToken]['code'] === T_COMMA)
+ ) {
+ $stackPtr = $nextToken;
+ }
+ }
+
$lineDifference = ($endBrace['line'] - $startBrace['line']);
$expected = sprintf($this->commentFormat, $startCondition['content']);
diff --git a/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php
index 1164c6f234..ce19d5c1d9 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php
@@ -40,6 +40,7 @@ class PostStatementCommentSniff implements Sniff
T_WHILE => true,
T_FOR => true,
T_FOREACH => true,
+ T_MATCH => true,
];
diff --git a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
index 7b9fc933ad..32e89789a7 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
@@ -30,18 +30,33 @@ public function processMemberVar(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = [
- T_PUBLIC,
- T_PRIVATE,
- T_PROTECTED,
- T_VAR,
- T_STATIC,
- T_WHITESPACE,
- T_STRING,
- T_NS_SEPARATOR,
- T_NULLABLE,
+ T_PUBLIC => T_PUBLIC,
+ T_PRIVATE => T_PRIVATE,
+ T_PROTECTED => T_PROTECTED,
+ T_VAR => T_VAR,
+ T_STATIC => T_STATIC,
+ T_READONLY => T_READONLY,
+ T_WHITESPACE => T_WHITESPACE,
+ T_STRING => T_STRING,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_NULLABLE => T_NULLABLE,
];
- $commentEnd = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
+
if ($commentEnd === false
|| ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT)
diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php
index 032c59ace8..8a56b23fcb 100644
--- a/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php
+++ b/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php
@@ -53,6 +53,7 @@ public function register()
T_ELSE,
T_ELSEIF,
T_SWITCH,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php
index 6803f9abb4..370eab1890 100644
--- a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php
+++ b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php
@@ -222,7 +222,7 @@ public function process(File $phpcsFile, $stackPtr)
$semicolon = $openingBracket;
$targetNestinglevel = 0;
if (isset($tokens[$openingBracket]['conditions']) === true) {
- $targetNestinglevel += count($tokens[$openingBracket]['conditions']);
+ $targetNestinglevel = count($tokens[$openingBracket]['conditions']);
}
do {
diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php
index ae4a51e7a3..cba076840a 100644
--- a/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php
+++ b/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php
@@ -34,6 +34,7 @@ public function register()
T_WHILE,
T_TRY,
T_CATCH,
+ T_MATCH,
];
}//end register()
diff --git a/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php b/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php
index c50e764e48..49dfc2c42f 100644
--- a/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php
+++ b/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class JSLintSniff implements Sniff
{
@@ -48,7 +49,7 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $rhinoPath = Config::getExecutablePath('jslint');
+ $rhinoPath = Config::getExecutablePath('rhino');
$jslintPath = Config::getExecutablePath('jslint');
if ($rhinoPath === null || $jslintPath === null) {
return;
@@ -56,8 +57,8 @@ public function process(File $phpcsFile, $stackPtr)
$fileName = $phpcsFile->getFilename();
- $rhinoPath = escapeshellcmd($rhinoPath);
- $jslintPath = escapeshellcmd($jslintPath);
+ $rhinoPath = Common::escapeshellcmd($rhinoPath);
+ $jslintPath = Common::escapeshellcmd($jslintPath);
$cmd = "$rhinoPath \"$jslintPath\" ".escapeshellarg($fileName);
exec($cmd, $output, $retval);
diff --git a/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php b/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php
index 0c0f35566e..30dca6722d 100644
--- a/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php
+++ b/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Common;
class JavaScriptLintSniff implements Sniff
{
@@ -56,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr)
$fileName = $phpcsFile->getFilename();
- $cmd = '"'.escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName);
+ $cmd = '"'.Common::escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName);
$msg = exec($cmd, $output, $retval);
// Variable $exitCode is the last line of $output if no error occurs, on
diff --git a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php
index bae882146d..6277b809e6 100644
--- a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php
+++ b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php
@@ -42,7 +42,7 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$fileName = $phpcsFile->getFilename();
$extension = substr($fileName, strrpos($fileName, '.'));
- $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr);
+ $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], $stackPtr);
if ($nextClass !== false) {
$phpcsFile->recordMetric($stackPtr, 'File extension for class files', $extension);
diff --git a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php
index 3d67f9b78f..8becb74a4e 100644
--- a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php
+++ b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php
@@ -80,7 +80,8 @@ public function process(File $phpcsFile, $stackPtr)
$isAssignment = isset(Tokens::$assignmentTokens[$tokens[$previous]['code']]);
$isEquality = isset(Tokens::$equalityTokens[$tokens[$previous]['code']]);
$isComparison = isset(Tokens::$comparisonTokens[$tokens[$previous]['code']]);
- if ($isAssignment === true || $isEquality === true || $isComparison === true) {
+ $isUnary = isset(Tokens::$operators[$tokens[$previous]['code']]);
+ if ($isAssignment === true || $isEquality === true || $isComparison === true || $isUnary === true) {
// This is a negative assignment or comparison.
// We need to check that the minus and the number are
// adjacent.
@@ -107,6 +108,8 @@ public function process(File $phpcsFile, $stackPtr)
T_OPEN_CURLY_BRACKET => true,
T_OPEN_SHORT_ARRAY => true,
T_CASE => true,
+ T_EXIT => true,
+ T_MATCH_ARROW => true,
];
if (isset($invalidTokens[$tokens[$previousToken]['code']]) === true) {
@@ -139,7 +142,9 @@ public function process(File $phpcsFile, $stackPtr)
T_THIS,
T_SELF,
T_STATIC,
+ T_PARENT,
T_OBJECT_OPERATOR,
+ T_NULLSAFE_OBJECT_OPERATOR,
T_DOUBLE_COLON,
T_OPEN_SQUARE_BRACKET,
T_CLOSE_SQUARE_BRACKET,
@@ -163,7 +168,7 @@ public function process(File $phpcsFile, $stackPtr)
break;
}
- if ($prevCode === T_STRING || $prevCode === T_SWITCH) {
+ if ($prevCode === T_STRING || $prevCode === T_SWITCH || $prevCode === T_MATCH) {
// We allow simple operations to not be bracketed.
// For example, ceil($one / $two).
for ($prev = ($stackPtr - 1); $prev > $bracket; $prev--) {
@@ -202,8 +207,8 @@ public function process(File $phpcsFile, $stackPtr)
if (in_array($prevCode, Tokens::$scopeOpeners, true) === true) {
// This operation is inside a control structure like FOREACH
// or IF, but has no bracket of it's own.
- // The only control structure allowed to do this is SWITCH.
- if ($prevCode !== T_SWITCH) {
+ // The only control structures allowed to do this are SWITCH and MATCH.
+ if ($prevCode !== T_SWITCH && $prevCode !== T_MATCH) {
break;
}
}
@@ -283,6 +288,7 @@ public function addMissingBracketsError($phpcsFile, $stackPtr)
T_SELF => true,
T_STATIC => true,
T_OBJECT_OPERATOR => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
T_DOUBLE_COLON => true,
T_MODULUS => true,
T_ISSET => true,
@@ -327,6 +333,10 @@ public function addMissingBracketsError($phpcsFile, $stackPtr)
$before = $phpcsFile->findNext(Tokens::$emptyTokens, ($before + 1), null, true);
+ // A few extra tokens are allowed to be on the right side of the expression.
+ $allowed[T_EQUAL] = true;
+ $allowed[T_NEW] = true;
+
// Find the last token in the expression.
for ($after = ($stackPtr + 1); $after < $phpcsFile->numTokens; $after++) {
// Special case for plus operators because we can't tell if they are used
diff --git a/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php b/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php
index 2f0d3ab5b7..e696d8001d 100644
--- a/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php
@@ -120,8 +120,14 @@ public function processBracket($phpcsFile, $openBracket)
$next = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), $closeBracket, true);
if ($next === false) {
if (($closeBracket - $openBracket) !== 1) {
+ if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) {
+ $found = 'newline';
+ } else {
+ $found = $tokens[($openBracket + 1)]['length'];
+ }
+
$error = 'Expected 0 spaces between parenthesis of function declaration; %s found';
- $data = [$tokens[($openBracket + 1)]['length']];
+ $data = [$found];
$fix = $phpcsFile->addFixableError($error, $openBracket, 'SpacingBetween', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($openBracket + 1), '');
@@ -131,7 +137,7 @@ public function processBracket($phpcsFile, $openBracket)
// No params, so we don't check normal spacing rules.
return;
}
- }
+ }//end if
foreach ($params as $paramNumber => $param) {
if ($param['pass_by_reference'] === true) {
diff --git a/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php
index a4ea79cfbd..c12bd0c47c 100644
--- a/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php
+++ b/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php
@@ -200,7 +200,7 @@ public function processBracket($phpcsFile, $openBracket, $tokens, $type='functio
// The open bracket should be the last thing on the line.
if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) {
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true);
- if ($tokens[$next]['line'] !== ($tokens[$openBracket]['line'] + 1)) {
+ if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) {
$error = 'The first parameter of a multi-line '.$type.' declaration must be on the line after the opening bracket';
$fix = $phpcsFile->addFixableError($error, $next, $errorPrefix.'FirstParamSpacing');
if ($fix === true) {
diff --git a/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php
index ed2505817b..5c3a74930e 100644
--- a/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php
+++ b/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php
@@ -38,7 +38,9 @@ protected function processVariable(File $phpcsFile, $stackPtr)
}
$objOperator = $phpcsFile->findNext([T_WHITESPACE], ($stackPtr + 1), null, true);
- if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR) {
+ if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR
+ || $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR
+ ) {
// Check to see if we are using a variable from an object.
$var = $phpcsFile->findNext([T_WHITESPACE], ($objOperator + 1), null, true);
if ($tokens[$var]['code'] === T_STRING) {
@@ -55,28 +57,38 @@ protected function processVariable(File $phpcsFile, $stackPtr)
}
if (Common::isCamelCaps($objVarName, false, true, false) === false) {
- $error = 'Variable "%s" is not in valid camel caps format';
+ $error = 'Member variable "%s" is not in valid camel caps format';
$data = [$originalVarName];
- $phpcsFile->addError($error, $var, 'NotCamelCaps', $data);
+ $phpcsFile->addError($error, $var, 'MemberNotCamelCaps', $data);
}
}//end if
}//end if
}//end if
+ $objOperator = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
+ if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) {
+ // The variable lives within a class, and is referenced like
+ // this: MyClass::$_variable, so we don't know its scope.
+ $objVarName = $varName;
+ if (substr($objVarName, 0, 1) === '_') {
+ $objVarName = substr($objVarName, 1);
+ }
+
+ if (Common::isCamelCaps($objVarName, false, true, false) === false) {
+ $error = 'Member variable "%s" is not in valid camel caps format';
+ $data = [$tokens[$stackPtr]['content']];
+ $phpcsFile->addError($error, $stackPtr, 'MemberNotCamelCaps', $data);
+ }
+
+ return;
+ }
+
// There is no way for us to know if the var is public or private,
// so we have to ignore a leading underscore if there is one and just
// check the main part of the variable name.
$originalVarName = $varName;
if (substr($varName, 0, 1) === '_') {
- $objOperator = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
- if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) {
- // The variable lives within a class, and is referenced like
- // this: MyClass::$_variable, so we don't know its scope.
- $inClass = true;
- } else {
- $inClass = $phpcsFile->hasCondition($stackPtr, Tokens::$ooScopeTokens);
- }
-
+ $inClass = $phpcsFile->hasCondition($stackPtr, Tokens::$ooScopeTokens);
if ($inClass === true) {
$varName = substr($varName, 1);
}
diff --git a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
index fb714e16cf..84facf0540 100644
--- a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
+++ b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
@@ -48,19 +48,37 @@ public function process(File $phpcsFile, $stackPtr)
$prev = $phpcsFile->findPrevious($allowedTokens, ($stackPtr - 1), null, true);
$allowedTokens = [
- T_EQUAL => true,
- T_DOUBLE_ARROW => true,
- T_THROW => true,
- T_RETURN => true,
- T_INLINE_THEN => true,
- T_INLINE_ELSE => true,
+ T_EQUAL => T_EQUAL,
+ T_COALESCE_EQUAL => T_COALESCE_EQUAL,
+ T_DOUBLE_ARROW => T_DOUBLE_ARROW,
+ T_FN_ARROW => T_FN_ARROW,
+ T_MATCH_ARROW => T_MATCH_ARROW,
+ T_THROW => T_THROW,
+ T_RETURN => T_RETURN,
];
- if (isset($allowedTokens[$tokens[$prev]['code']]) === false) {
- $error = 'New objects must be assigned to a variable';
- $phpcsFile->addError($error, $stackPtr, 'NotAssigned');
+ if (isset($allowedTokens[$tokens[$prev]['code']]) === true) {
+ return;
}
+ $ternaryLikeTokens = [
+ T_COALESCE => true,
+ T_INLINE_THEN => true,
+ T_INLINE_ELSE => true,
+ ];
+
+ // For ternary like tokens, walk a little further back to see if it is preceded by
+ // one of the allowed tokens (within the same statement).
+ if (isset($ternaryLikeTokens[$tokens[$prev]['code']]) === true) {
+ $hasAllowedBefore = $phpcsFile->findPrevious($allowedTokens, ($prev - 1), null, false, null, true);
+ if ($hasAllowedBefore !== false) {
+ return;
+ }
+ }
+
+ $error = 'New objects must be assigned to a variable';
+ $phpcsFile->addError($error, $stackPtr, 'NotAssigned');
+
}//end process()
diff --git a/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php b/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php
index c3a8b3915e..cb3778453a 100644
--- a/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php
+++ b/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php
@@ -74,7 +74,8 @@ protected function processIncDec($phpcsFile, $stackPtr)
// start looking for other operators.
if ($tokens[($stackPtr - 1)]['code'] === T_VARIABLE
|| ($tokens[($stackPtr - 1)]['code'] === T_STRING
- && $tokens[($stackPtr - 2)]['code'] === T_OBJECT_OPERATOR)
+ && ($tokens[($stackPtr - 2)]['code'] === T_OBJECT_OPERATOR
+ || $tokens[($stackPtr - 2)]['code'] === T_NULLSAFE_OBJECT_OPERATOR))
) {
$start = ($stackPtr + 1);
} else {
diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php
index aaed48d22a..9eb2124233 100644
--- a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php
@@ -52,30 +52,34 @@ public function process(File $phpcsFile, $stackPtr)
}
}
- // Ignore values in array definitions.
- $array = $phpcsFile->findNext(
- T_ARRAY,
+ // Ignore values in array definitions or match structures.
+ $nextNonEmpty = $phpcsFile->findNext(
+ Tokens::$emptyTokens,
($stackPtr + 1),
null,
- false,
- null,
true
);
- if ($array !== false) {
+ if ($nextNonEmpty !== false
+ && ($tokens[$nextNonEmpty]['code'] === T_ARRAY
+ || $tokens[$nextNonEmpty]['code'] === T_MATCH)
+ ) {
return;
}
// Ignore function calls.
$ignore = [
+ T_NULLSAFE_OBJECT_OPERATOR,
+ T_OBJECT_OPERATOR,
T_STRING,
+ T_VARIABLE,
T_WHITESPACE,
- T_OBJECT_OPERATOR,
];
$next = $phpcsFile->findNext($ignore, ($stackPtr + 1), null, true);
- if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS
- && $tokens[($next - 1)]['code'] === T_STRING
+ if ($tokens[$next]['code'] === T_CLOSURE
+ || ($tokens[$next]['code'] === T_OPEN_PARENTHESIS
+ && $tokens[($next - 1)]['code'] === T_STRING)
) {
// Code will look like: $var = myFunction(
// and will be ignored.
diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php
index 84032fec01..4448d2445b 100644
--- a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php
@@ -43,7 +43,7 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
// Ignore default value assignments in function definitions.
- $function = $phpcsFile->findPrevious([T_FUNCTION, T_CLOSURE], ($stackPtr - 1), null, false, null, true);
+ $function = $phpcsFile->findPrevious([T_FUNCTION, T_CLOSURE, T_FN], ($stackPtr - 1), null, false, null, true);
if ($function !== false) {
$opener = $tokens[$function]['parenthesis_opener'];
$closer = $tokens[$function]['parenthesis_closer'];
@@ -83,6 +83,12 @@ public function process(File $phpcsFile, $stackPtr)
*/
for ($varToken = ($stackPtr - 1); $varToken >= 0; $varToken--) {
+ if (in_array($tokens[$varToken]['code'], [T_SEMICOLON, T_OPEN_CURLY_BRACKET], true) === true) {
+ // We've reached the next statement, so we
+ // didn't find a variable.
+ return;
+ }
+
// Skip brackets.
if (isset($tokens[$varToken]['parenthesis_opener']) === true && $tokens[$varToken]['parenthesis_opener'] < $varToken) {
$varToken = $tokens[$varToken]['parenthesis_opener'];
@@ -94,12 +100,6 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}
- if ($tokens[$varToken]['code'] === T_SEMICOLON) {
- // We've reached the next statement, so we
- // didn't find a variable.
- return;
- }
-
if ($tokens[$varToken]['code'] === T_VARIABLE) {
// We found our variable.
break;
@@ -146,6 +146,7 @@ public function process(File $phpcsFile, $stackPtr)
if ($tokens[$varToken]['code'] === T_VARIABLE
|| $tokens[$varToken]['code'] === T_OPEN_TAG
+ || $tokens[$varToken]['code'] === T_GOTO_LABEL
|| $tokens[$varToken]['code'] === T_INLINE_THEN
|| $tokens[$varToken]['code'] === T_INLINE_ELSE
|| $tokens[$varToken]['code'] === T_SEMICOLON
@@ -165,6 +166,7 @@ public function process(File $phpcsFile, $stackPtr)
T_SWITCH => T_SWITCH,
T_CASE => T_CASE,
T_FOR => T_FOR,
+ T_MATCH => T_MATCH,
];
foreach ($nested as $opener => $closer) {
if (isset($tokens[$opener]['parenthesis_owner']) === true
diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php
index cb1436631a..a0f1161c6a 100644
--- a/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php
@@ -95,7 +95,9 @@ public function process(File $phpcsFile, $stackPtr)
$functionName = 'object.'.$functionName;
} else {
// Make sure it isn't a member var.
- if ($tokens[($i - 1)]['code'] === T_OBJECT_OPERATOR) {
+ if ($tokens[($i - 1)]['code'] === T_OBJECT_OPERATOR
+ || $tokens[($i - 1)]['code'] === T_NULLSAFE_OBJECT_OPERATOR
+ ) {
continue;
}
diff --git a/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php b/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php
index 4e3ee2152c..e42ef5a16a 100644
--- a/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php
@@ -11,6 +11,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
class InnerFunctionsSniff implements Sniff
{
@@ -41,21 +42,28 @@ public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
- $function = $phpcsFile->getCondition($stackPtr, T_FUNCTION);
- if ($function === false) {
- // Not a nested function.
+ if (isset($tokens[$stackPtr]['conditions']) === false) {
return;
}
- $class = $phpcsFile->getCondition($stackPtr, T_ANON_CLASS, false);
- if ($class !== false && $class > $function) {
- // Ignore methods in anon classes.
- return;
+ $conditions = $tokens[$stackPtr]['conditions'];
+ $reversedConditions = array_reverse($conditions, true);
+
+ $outerFuncToken = null;
+ foreach ($reversedConditions as $condToken => $condition) {
+ if ($condition === T_FUNCTION || $condition === T_CLOSURE) {
+ $outerFuncToken = $condToken;
+ break;
+ }
+
+ if (\array_key_exists($condition, Tokens::$ooScopeTokens) === true) {
+ // Ignore methods in OOP structures defined within functions.
+ return;
+ }
}
- $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
- if ($tokens[$prev]['code'] === T_EQUAL) {
- // Ignore closures.
+ if ($outerFuncToken === null) {
+ // Not a nested function.
return;
}
diff --git a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php
index 486edcc01f..37f1e40e7b 100644
--- a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php
@@ -75,6 +75,11 @@ public function process(File $phpcsFile, $stackPtr)
}
// Make sure this is a function call or a use statement.
+ if (empty($tokens[$stackPtr]['nested_attributes']) === false) {
+ // Class instantiation in attribute, not function call.
+ return;
+ }
+
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if ($next === false) {
// Not a function call.
@@ -133,7 +138,9 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR) {
+ if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR
+ || $tokens[$prev]['code'] === T_NULLSAFE_OBJECT_OPERATOR
+ ) {
// Not an inbuilt function.
return;
}
diff --git a/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php b/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php
index fa3da745bc..31d9c5a351 100644
--- a/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php
+++ b/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php
@@ -16,6 +16,22 @@
class NonExecutableCodeSniff implements Sniff
{
+ /**
+ * Tokens for terminating expressions, which can be used inline.
+ *
+ * This is in contrast to terminating statements, which cannot be used inline
+ * and would result in a parse error (which is not the concern of this sniff).
+ *
+ * `throw` can be used as an expression since PHP 8.0.
+ * {@link https://wiki.php.net/rfc/throw_expression}
+ *
+ * @var array
+ */
+ private $expressionTokens = [
+ T_EXIT => T_EXIT,
+ T_THROW => T_THROW,
+ ];
+
/**
* Returns an array of tokens this test wants to listen for.
@@ -49,12 +65,34 @@ public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
- // If this token is preceded with an "or", it only relates to one line
- // and should be ignored. For example: fopen() or die().
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
- if ($tokens[$prev]['code'] === T_LOGICAL_OR || $tokens[$prev]['code'] === T_BOOLEAN_OR) {
- return;
- }
+
+ // Tokens which can be used in inline expressions need special handling.
+ if (isset($this->expressionTokens[$tokens[$stackPtr]['code']]) === true) {
+ // If this token is preceded by a logical operator, it only relates to one line
+ // and should be ignored. For example: fopen() or die().
+ // Note: There is one exception: throw expressions can not be used with xor.
+ if (isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true
+ && ($tokens[$stackPtr]['code'] === T_THROW && $tokens[$prev]['code'] === T_LOGICAL_XOR) === false
+ ) {
+ return;
+ }
+
+ // Expressions are allowed in the `else` clause of ternaries.
+ if ($tokens[$prev]['code'] === T_INLINE_THEN || $tokens[$prev]['code'] === T_INLINE_ELSE) {
+ return;
+ }
+
+ // Expressions are allowed with PHP 7.0+ null coalesce and PHP 7.4+ null coalesce equals.
+ if ($tokens[$prev]['code'] === T_COALESCE || $tokens[$prev]['code'] === T_COALESCE_EQUAL) {
+ return;
+ }
+
+ // Expressions are allowed in arrow functions.
+ if ($tokens[$prev]['code'] === T_FN_ARROW) {
+ return;
+ }
+ }//end if
// Check if this token is actually part of a one-line IF or ELSE statement.
for ($i = ($stackPtr - 1); $i > 0; $i--) {
@@ -139,7 +177,6 @@ public function process(File $phpcsFile, $stackPtr)
// This token may be part of an inline condition.
// If we find a closing parenthesis that belongs to a condition
// we should ignore this token.
- $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if (isset($tokens[$prev]['parenthesis_owner']) === true) {
$owner = $tokens[$prev]['parenthesis_owner'];
$ignore = [
@@ -204,24 +241,28 @@ public function process(File $phpcsFile, $stackPtr)
$end = ($phpcsFile->numTokens - 1);
}//end if
- // Find the semicolon that ends this statement, skipping
- // nested statements like FOR loops and closures.
+ // Find the semicolon or closing PHP tag that ends this statement,
+ // skipping nested statements like FOR loops and closures.
for ($start = ($stackPtr + 1); $start < $phpcsFile->numTokens; $start++) {
if ($start === $end) {
break;
}
- if ($tokens[$start]['code'] === T_OPEN_PARENTHESIS) {
+ if (isset($tokens[$start]['parenthesis_closer']) === true
+ && $tokens[$start]['code'] === T_OPEN_PARENTHESIS
+ ) {
$start = $tokens[$start]['parenthesis_closer'];
continue;
}
- if ($tokens[$start]['code'] === T_OPEN_CURLY_BRACKET) {
+ if (isset($tokens[$start]['bracket_closer']) === true
+ && $tokens[$start]['code'] === T_OPEN_CURLY_BRACKET
+ ) {
$start = $tokens[$start]['bracket_closer'];
continue;
}
- if ($tokens[$start]['code'] === T_SEMICOLON) {
+ if ($tokens[$start]['code'] === T_SEMICOLON || $tokens[$start]['code'] === T_CLOSE_TAG) {
break;
}
}//end for
@@ -254,6 +295,16 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}
+ // Skip HTML whitespace.
+ if ($tokens[$i]['code'] === T_INLINE_HTML && \trim($tokens[$i]['content']) === '') {
+ continue;
+ }
+
+ // Skip PHP re-open tag (eg, after inline HTML).
+ if ($tokens[$i]['code'] === T_OPEN_TAG) {
+ continue;
+ }
+
$line = $tokens[$i]['line'];
if ($line > $lastLine) {
$type = substr($tokens[$stackPtr]['type'], 2);
diff --git a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php
index 8c60208c6c..8a34a4f29f 100644
--- a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php
+++ b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php
@@ -54,17 +54,8 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop
return;
}
- $modifier = null;
- for ($i = ($stackPtr - 1); $i > 0; $i--) {
- if ($tokens[$i]['line'] < $tokens[$stackPtr]['line']) {
- break;
- } else if (isset(Tokens::$scopeModifiers[$tokens[$i]['code']]) === true) {
- $modifier = $i;
- break;
- }
- }
-
- if ($modifier === null) {
+ $properties = $phpcsFile->getMethodProperties($stackPtr);
+ if ($properties['scope_specified'] === false) {
$error = 'Visibility must be declared on method "%s"';
$data = [$methodName];
$phpcsFile->addError($error, $stackPtr, 'Missing', $data);
diff --git a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php
index 0bafbf4db4..f3b5495d90 100644
--- a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php
+++ b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php
@@ -22,7 +22,7 @@ class StaticThisUsageSniff extends AbstractScopeSniff
*/
public function __construct()
{
- parent::__construct([T_CLASS, T_TRAIT, T_ANON_CLASS], [T_FUNCTION]);
+ parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION]);
}//end __construct()
@@ -76,9 +76,9 @@ public function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope)
/**
* Check for $this variable usage between $next and $end tokens.
*
- * @param File $phpcsFile The current file being scanned.
- * @param int $next The position of the next token to check.
- * @param int $end The position of the last token to check.
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned.
+ * @param int $next The position of the next token to check.
+ * @param int $end The position of the last token to check.
*
* @return void
*/
diff --git a/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php b/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php
index 4125b63ad0..9d32e54a0a 100644
--- a/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php
@@ -55,6 +55,10 @@ public function register()
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
+ if (isset($tokens[($stackPtr + 2)]) === false) {
+ // Syntax error or live coding, bow out.
+ return;
+ }
$ignoreBefore = false;
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php
index cfeb657e39..808888f404 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php
@@ -46,6 +46,7 @@ public function register()
T_TRY,
T_CATCH,
T_FINALLY,
+ T_MATCH,
];
}//end register()
@@ -144,6 +145,7 @@ public function process(File $phpcsFile, $stackPtr)
T_CLASS => true,
T_INTERFACE => true,
T_TRAIT => true,
+ T_ENUM => true,
T_DOC_COMMENT_OPEN_TAG => true,
];
@@ -232,6 +234,16 @@ public function process(File $phpcsFile, $stackPtr)
}//end if
}//end if
+ if ($tokens[$stackPtr]['code'] === T_MATCH) {
+ // Move the scope closer to the semicolon/comma.
+ $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($scopeCloser + 1), null, true);
+ if ($next !== false
+ && ($tokens[$next]['code'] === T_SEMICOLON || $tokens[$next]['code'] === T_COMMA)
+ ) {
+ $scopeCloser = $next;
+ }
+ }
+
$trailingContent = $phpcsFile->findNext(
T_WHITESPACE,
($scopeCloser + 1),
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php
index 83a62d3542..1f4704b91f 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php
@@ -115,6 +115,12 @@ public function process(File $phpcsFile, $stackPtr)
$ignore = ([T_WHITESPACE => T_WHITESPACE] + Tokens::$methodPrefixes);
$prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
+
+ while ($tokens[$prev]['code'] === T_ATTRIBUTE_END) {
+ // Skip past function attributes.
+ $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['attribute_opener'] - 1), null, true);
+ }
+
if ($tokens[$prev]['code'] === T_DOC_COMMENT_CLOSE_TAG) {
// Skip past function docblocks.
$prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['comment_opener'] - 1), null, true);
@@ -241,6 +247,14 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
+ while ($tokens[$prevContent]['code'] === T_ATTRIBUTE_END
+ && $tokens[$prevContent]['line'] === ($currentLine - 1)
+ ) {
+ // Account for function attributes.
+ $currentLine = $tokens[$tokens[$prevContent]['attribute_opener']]['line'];
+ $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['attribute_opener'] - 1), null, true);
+ }
+
if ($tokens[$prevContent]['code'] === T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$prevContent]['line'] === ($currentLine - 1)
) {
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
index f0c84fb8e2..0ece1acad2 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
@@ -55,11 +55,26 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$endOfStatement = $phpcsFile->findNext(T_SEMICOLON, ($stackPtr + 1), null, false, null, true);
- $ignore = $validPrefixes;
- $ignore[] = T_WHITESPACE;
+ $ignore = $validPrefixes;
+ $ignore[T_WHITESPACE] = T_WHITESPACE;
$start = $startOfStatement;
- $prev = $phpcsFile->findPrevious($ignore, ($startOfStatement - 1), null, true);
+ for ($prev = ($startOfStatement - 1); $prev >= 0; $prev--) {
+ if (isset($ignore[$tokens[$prev]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$prev]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$prev]['attribute_opener']) === true
+ ) {
+ $prev = $tokens[$prev]['attribute_opener'];
+ $start = $prev;
+ continue;
+ }
+
+ break;
+ }
+
if (isset(Tokens::$commentTokens[$tokens[$prev]['code']]) === true) {
// Assume the comment belongs to the member var if it is on a line by itself.
$prevContent = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
@@ -67,28 +82,48 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
// Check the spacing, but then skip it.
$foundLines = ($tokens[$startOfStatement]['line'] - $tokens[$prev]['line'] - 1);
if ($foundLines > 0) {
- $error = 'Expected 0 blank lines after member var comment; %s found';
- $data = [$foundLines];
- $fix = $phpcsFile->addFixableError($error, $prev, 'AfterComment', $data);
- if ($fix === true) {
- $phpcsFile->fixer->beginChangeset();
- // Inline comments have the newline included in the content but
- // docblock do not.
- if ($tokens[$prev]['code'] === T_COMMENT) {
- $phpcsFile->fixer->replaceToken($prev, rtrim($tokens[$prev]['content']));
+ for ($i = ($prev + 1); $i < $startOfStatement; $i++) {
+ if ($tokens[$i]['column'] !== 1) {
+ continue;
}
- for ($i = ($prev + 1); $i <= $startOfStatement; $i++) {
- if ($tokens[$i]['line'] === $tokens[$startOfStatement]['line']) {
- break;
- }
-
- $phpcsFile->fixer->replaceToken($i, '');
- }
-
- $phpcsFile->fixer->addNewline($prev);
- $phpcsFile->fixer->endChangeset();
- }
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ $error = 'Expected 0 blank lines after member var comment; %s found';
+ $data = [$foundLines];
+ $fix = $phpcsFile->addFixableError($error, $prev, 'AfterComment', $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+ // Inline comments have the newline included in the content but
+ // docblocks do not.
+ if ($tokens[$prev]['code'] === T_COMMENT) {
+ $phpcsFile->fixer->replaceToken($prev, rtrim($tokens[$prev]['content']));
+ }
+
+ for ($i = ($prev + 1); $i <= $startOfStatement; $i++) {
+ if ($tokens[$i]['line'] === $tokens[$startOfStatement]['line']) {
+ break;
+ }
+
+ // Remove the newline after the docblock, and any entirely
+ // empty lines before the member var.
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] === $tokens[$prev]['line']
+ || ($tokens[$i]['column'] === 1
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'])
+ ) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+ }
+
+ $phpcsFile->fixer->addNewline($prev);
+ $phpcsFile->fixer->endChangeset();
+ }//end if
+
+ break;
+ }//end if
+ }//end for
}//end if
$start = $prev;
@@ -106,7 +141,7 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$first = $tokens[$start]['comment_opener'];
} else {
$first = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($start - 1), null, true);
- $first = $phpcsFile->findNext(Tokens::$commentTokens, ($first + 1));
+ $first = $phpcsFile->findNext(array_merge(Tokens::$commentTokens, [T_ATTRIBUTE]), ($first + 1));
}
// Determine if this is the first member var.
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php
index a1e83aadd1..cc5db16446 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php
@@ -33,6 +33,7 @@ public function register()
return [
T_OBJECT_OPERATOR,
T_DOUBLE_COLON,
+ T_NULLSAFE_OBJECT_OPERATOR,
];
}//end register()
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php
index c69b0b6433..f1e2fce692 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php
@@ -77,36 +77,31 @@ public function register()
// Returning/printing a negative value; eg. (return -1).
$this->nonOperandTokens += [
- T_RETURN => T_RETURN,
- T_ECHO => T_ECHO,
- T_PRINT => T_PRINT,
- T_YIELD => T_YIELD,
+ T_RETURN => T_RETURN,
+ T_ECHO => T_ECHO,
+ T_EXIT => T_EXIT,
+ T_PRINT => T_PRINT,
+ T_YIELD => T_YIELD,
+ T_FN_ARROW => T_FN_ARROW,
+ T_MATCH_ARROW => T_MATCH_ARROW,
];
// Trying to use a negative value; eg. myFunction($var, -2).
$this->nonOperandTokens += [
- T_COMMA => T_COMMA,
- T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS,
- T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET,
- T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY,
- T_DOUBLE_ARROW => T_DOUBLE_ARROW,
+ T_CASE => T_CASE,
T_COLON => T_COLON,
- T_INLINE_THEN => T_INLINE_THEN,
+ T_COMMA => T_COMMA,
T_INLINE_ELSE => T_INLINE_ELSE,
- T_CASE => T_CASE,
+ T_INLINE_THEN => T_INLINE_THEN,
T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET,
+ T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS,
+ T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY,
+ T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET,
+ T_STRING_CONCAT => T_STRING_CONCAT,
];
// Casting a negative value; eg. (array) -$a.
- $this->nonOperandTokens += [
- T_ARRAY_CAST => T_ARRAY_CAST,
- T_BOOL_CAST => T_BOOL_CAST,
- T_DOUBLE_CAST => T_DOUBLE_CAST,
- T_INT_CAST => T_INT_CAST,
- T_OBJECT_CAST => T_OBJECT_CAST,
- T_STRING_CAST => T_STRING_CAST,
- T_UNSET_CAST => T_UNSET_CAST,
- ];
+ $this->nonOperandTokens += Tokens::$castTokens;
/*
These are the tokens the sniff is looking for.
@@ -344,6 +339,7 @@ protected function isOperator(File $phpcsFile, $stackPtr)
$function = $tokens[$bracket]['parenthesis_owner'];
if ($tokens[$function]['code'] === T_FUNCTION
|| $tokens[$function]['code'] === T_CLOSURE
+ || $tokens[$function]['code'] === T_FN
|| $tokens[$function]['code'] === T_DECLARE
) {
return false;
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
index f68613932d..fd03875347 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
@@ -65,11 +65,20 @@ public function process(File $phpcsFile, $stackPtr)
// Check that the closing brace is on it's own line.
$lastContent = $phpcsFile->findPrevious([T_INLINE_HTML, T_WHITESPACE, T_OPEN_TAG], ($scopeEnd - 1), $scopeStart, true);
- if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']) {
+ for ($lineStart = $scopeEnd; $tokens[$lineStart]['column'] > 1; $lineStart--);
+
+ if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']
+ || ($tokens[$lineStart]['code'] === T_INLINE_HTML
+ && trim($tokens[$lineStart]['content']) !== '')
+ ) {
$error = 'Closing brace must be on a line by itself';
$fix = $phpcsFile->addFixableError($error, $scopeEnd, 'ContentBefore');
if ($fix === true) {
- $phpcsFile->fixer->addNewlineBefore($scopeEnd);
+ if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']) {
+ $phpcsFile->fixer->addNewlineBefore($scopeEnd);
+ } else {
+ $phpcsFile->fixer->addNewlineBefore(($lineStart + 1));
+ }
}
return;
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php
index e0fab85a8d..2d800f0520 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php
@@ -26,6 +26,7 @@ public function register()
{
$register = Tokens::$scopeModifiers;
$register[] = T_STATIC;
+ $register[] = T_READONLY;
return $register;
}//end register()
@@ -51,20 +52,63 @@ public function process(File $phpcsFile, $stackPtr)
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
$nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
- if ($tokens[$stackPtr]['code'] === T_STATIC
- && (($nextToken === false || $tokens[$nextToken]['code'] === T_DOUBLE_COLON)
- || $tokens[$prevToken]['code'] === T_NEW)
- ) {
- // Late static binding, e.g., static:: OR new static() usage or live coding.
- return;
- }
+ if ($tokens[$stackPtr]['code'] === T_STATIC) {
+ if (($nextToken === false || $tokens[$nextToken]['code'] === T_DOUBLE_COLON)
+ || $tokens[$prevToken]['code'] === T_NEW
+ ) {
+ // Late static binding, e.g., static:: OR new static() usage or live coding.
+ return;
+ }
+
+ if ($prevToken !== false
+ && $tokens[$prevToken]['code'] === T_TYPE_UNION
+ ) {
+ // Not a scope keyword, but a union return type.
+ return;
+ }
+
+ if ($prevToken !== false
+ && $tokens[$prevToken]['code'] === T_NULLABLE
+ ) {
+ // Not a scope keyword, but a return type.
+ return;
+ }
+
+ if ($prevToken !== false
+ && $tokens[$prevToken]['code'] === T_COLON
+ ) {
+ $prevPrevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), null, true);
+ if ($prevPrevToken !== false
+ && $tokens[$prevPrevToken]['code'] === T_CLOSE_PARENTHESIS
+ ) {
+ // Not a scope keyword, but a return type.
+ return;
+ }
+ }
+ }//end if
if ($tokens[$prevToken]['code'] === T_AS) {
// Trait visibility change, e.g., "use HelloWorld { sayHello as private; }".
return;
}
- if ($nextToken !== false && $tokens[$nextToken]['code'] === T_VARIABLE) {
+ $isInFunctionDeclaration = false;
+ if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) {
+ // Check if this is PHP 8.0 constructor property promotion.
+ // In that case, we can't have multi-property definitions.
+ $nestedParens = $tokens[$stackPtr]['nested_parenthesis'];
+ $lastCloseParens = end($nestedParens);
+ if (isset($tokens[$lastCloseParens]['parenthesis_owner']) === true
+ && $tokens[$tokens[$lastCloseParens]['parenthesis_owner']]['code'] === T_FUNCTION
+ ) {
+ $isInFunctionDeclaration = true;
+ }
+ }
+
+ if ($nextToken !== false
+ && $tokens[$nextToken]['code'] === T_VARIABLE
+ && $isInFunctionDeclaration === false
+ ) {
$endOfStatement = $phpcsFile->findNext(T_SEMICOLON, ($nextToken + 1));
if ($endOfStatement === false) {
// Live coding.
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php
index 831e832766..e99d829bca 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php
@@ -49,6 +49,7 @@ public function register()
{
return [
T_OPEN_TAG,
+ T_OPEN_TAG_WITH_ECHO,
T_CLOSE_TAG,
T_WHITESPACE,
T_COMMENT,
diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc
index e6fdbdc1f8..b96aec7bde 100644
--- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc
+++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc
@@ -447,7 +447,85 @@ $c);
array('a' => $a, 'b' => $b,
'c' => $c);
+array(
+ static function() {
+ return null;
+ },
+ (array) array(),
+ (bool) array(),
+ (double) array(),
+ (int) array(),
+ (object) array(),
+ (string) array(),
+ (unset) array(),
+);
+
+array(
+ 'foo',
+ 'bar'
+ // This is a non-fixable error.
+ ,
+);
+
+yield array(
+ static fn () : string => '',
+);
+
+yield array(
+ static fn () : string => '',
+ );
+
+$foo = array(
+ 'foo' => match ($anything) {
+ 'foo' => 'bar',
+ default => null,
+ },
+ );
+
// Intentional syntax error.
$a = array(
'a' =>
);
+
+// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered.
+$x = array(
+ ...$a,
+ 'foo' => 'bar',
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ );
+
+$x = array(
+ ...$a,
+ 'foo' => 'bar', // OK.
+ 'bar', // NoKeySpecified Error (based on second entry).
+ );
+
+$x = array(
+ ...$a,
+ 'bar', // OK.
+ 'foo' => 'bar', // KeySpecified Error (based on second entry).
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ 'bar', // NoKeySpecified Error (based on first entry).
+ );
+
+$x = array(
+ 'bar',
+ ...$a,
+ 'bar',
+ 'baz' => 'bar', // KeySpecified (based on first entry).
+ );
diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed
index 336ab0af9f..9da6a4279b 100644
--- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed
+++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed
@@ -483,7 +483,85 @@ array(
'c' => $c,
);
+array(
+ static function() {
+ return null;
+ },
+ (array) array(),
+ (bool) array(),
+ (double) array(),
+ (int) array(),
+ (object) array(),
+ (string) array(),
+ (unset) array(),
+);
+
+array(
+ 'foo',
+ 'bar'
+ // This is a non-fixable error.
+ ,
+);
+
+yield array(
+ static fn () : string => '',
+ );
+
+yield array(
+ static fn () : string => '',
+ );
+
+$foo = array(
+ 'foo' => match ($anything) {
+ 'foo' => 'bar',
+ default => null,
+ },
+ );
+
// Intentional syntax error.
$a = array(
'a' =>
);
+
+// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered.
+$x = array(
+ ...$a,
+ 'foo' => 'bar',
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ );
+
+$x = array(
+ ...$a,
+ 'foo' => 'bar', // OK.
+ 'bar', // NoKeySpecified Error (based on second entry).
+ );
+
+$x = array(
+ ...$a,
+ 'bar', // OK.
+ 'foo' => 'bar', // KeySpecified Error (based on second entry).
+ );
+
+$x = array(
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ 'bar', // NoKeySpecified Error (based on first entry).
+ );
+
+$x = array(
+ 'bar',
+ ...$a,
+ 'bar',
+ 'baz' => 'bar', // KeySpecified (based on first entry).
+ );
diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc
index 60ff35d64c..0c8b48fc89 100644
--- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc
+++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc
@@ -436,7 +436,85 @@ $c];
['a' => $a, 'b' => $b,
'c' => $c];
+[
+ static function() {
+ return null;
+ },
+ (array) [],
+ (bool) [],
+ (double) [],
+ (int) [],
+ (object) [],
+ (string) [],
+ (unset) [],
+];
+
+[
+ 'foo',
+ 'bar'
+ // This is a non-fixable error.
+ ,
+];
+
+yield [
+ static fn () : string => '',
+];
+
+yield [
+ static fn () : string => '',
+ ];
+
+$foo = [
+ 'foo' => match ($anything) {
+ 'foo' => 'bar',
+ default => null,
+ },
+ ];
+
// Intentional syntax error.
$a = [
'a' =>
];
+
+// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered.
+$x = [
+ ...$a,
+ 'foo' => 'bar',
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ ];
+
+$x = [
+ ...$a,
+ 'foo' => 'bar', // OK.
+ 'bar', // NoKeySpecified Error (based on second entry).
+ ];
+
+$x = [
+ ...$a,
+ 'bar', // OK.
+ 'foo' => 'bar', // KeySpecified Error (based on second entry).
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ 'bar', // NoKeySpecified Error (based on first entry).
+ ];
+
+$x = [
+ 'bar',
+ ...$a,
+ 'bar',
+ 'baz' => 'bar', // KeySpecified (based on first entry).
+ ];
diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed
index 1d17fa0c4d..4b09e2f234 100644
--- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed
+++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed
@@ -470,7 +470,85 @@ $foo = [
'c' => $c,
];
+[
+ static function() {
+ return null;
+ },
+ (array) [],
+ (bool) [],
+ (double) [],
+ (int) [],
+ (object) [],
+ (string) [],
+ (unset) [],
+];
+
+[
+ 'foo',
+ 'bar'
+ // This is a non-fixable error.
+ ,
+];
+
+yield [
+ static fn () : string => '',
+ ];
+
+yield [
+ static fn () : string => '',
+ ];
+
+$foo = [
+ 'foo' => match ($anything) {
+ 'foo' => 'bar',
+ default => null,
+ },
+ ];
+
// Intentional syntax error.
$a = [
'a' =>
];
+
+// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered.
+$x = [
+ ...$a,
+ 'foo' => 'bar',
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ ];
+
+$x = [
+ ...$a,
+ 'foo' => 'bar', // OK.
+ 'bar', // NoKeySpecified Error (based on second entry).
+ ];
+
+$x = [
+ ...$a,
+ 'bar', // OK.
+ 'foo' => 'bar', // KeySpecified Error (based on second entry).
+ ];
+
+$x = [
+ 'foo' => 'bar',
+ ...$a,
+ 'baz' => 'bar',
+ 'bar', // NoKeySpecified Error (based on first entry).
+ ];
+
+$x = [
+ 'bar',
+ ...$a,
+ 'bar',
+ 'baz' => 'bar', // KeySpecified (based on first entry).
+ ];
diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php
index 818b1b5562..9d917b46b0 100644
--- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php
+++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php
@@ -121,6 +121,13 @@ public function getErrorList($testFile='')
445 => 2,
447 => 2,
448 => 3,
+ 467 => 1,
+ 471 => 1,
+ 472 => 1,
+ 510 => 1,
+ 516 => 1,
+ 523 => 1,
+ 530 => 1,
];
case 'ArrayDeclarationUnitTest.2.inc':
return [
@@ -204,6 +211,13 @@ public function getErrorList($testFile='')
434 => 2,
436 => 2,
437 => 3,
+ 456 => 1,
+ 460 => 1,
+ 461 => 1,
+ 499 => 1,
+ 505 => 1,
+ 512 => 1,
+ 519 => 1,
];
default:
return [];
diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc
index 43ef6c9cdd..2af42d3721 100644
--- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc
@@ -119,3 +119,12 @@ class IncorrectCodeBeforeClosingBrace
{
echo phpinfo();}
+
+readonly
+class Test
+{
+}
+
+readonly class Test
+{
+}
diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
index 9083633fa9..5d01b68e0a 100644
--- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed
@@ -130,3 +130,11 @@ class IncorrectCodeBeforeClosingBrace
echo phpinfo();
}
+
+readonly class Test
+{
+}
+
+readonly class Test
+{
+}
diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php
index 914e6c87ef..23d89ec77a 100644
--- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php
+++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php
@@ -61,6 +61,8 @@ public function getErrorList()
116 => 1,
118 => 1,
121 => 1,
+ 124 => 2,
+ 128 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc
index a346a00f21..8b5a5aa708 100644
--- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc
@@ -5,7 +5,8 @@
class ClassFileNameUnitTest {}
interface ClassFileNameUnitTest {}
trait ClassFileNameUnitTest {}
-
+enum ClassFileNameUnitTest {}
+enum ClassFileNameUnitTest: int {}
// Invalid filename matching class name (case sensitive).
class classFileNameUnitTest {}
@@ -17,6 +18,9 @@ interface CLASSFILENAMEUNITTEST {}
trait classFileNameUnitTest {}
trait classfilenameunittest {}
trait CLASSFILENAMEUNITTEST {}
+enum classFileNameUnitTest {}
+enum classfilenameunittest {}
+enum CLASSFILENAMEUNITTEST {}
// Invalid non-filename matching class names.
@@ -32,6 +36,10 @@ trait CompletelyWrongClassName {}
trait ClassFileNameUnitTestExtra {}
trait ClassFileNameUnitTestInc {}
trait ExtraClassFileNameUnitTest {}
+enum CompletelyWrongClassName {}
+enum ClassFileNameUnitTestExtra {}
+enum ClassFileNameUnitTestInc {}
+enum ExtraClassFileNameUnitTest {}
-?>
\ No newline at end of file
+?>
diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php
index b229a2fc3f..5964d2b1ec 100644
--- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php
+++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php
@@ -26,7 +26,6 @@ class ClassFileNameUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 11 => 1,
12 => 1,
13 => 1,
14 => 1,
@@ -35,10 +34,10 @@ public function getErrorList()
17 => 1,
18 => 1,
19 => 1,
+ 20 => 1,
+ 21 => 1,
+ 22 => 1,
23 => 1,
- 24 => 1,
- 25 => 1,
- 26 => 1,
27 => 1,
28 => 1,
29 => 1,
@@ -47,6 +46,14 @@ public function getErrorList()
32 => 1,
33 => 1,
34 => 1,
+ 35 => 1,
+ 36 => 1,
+ 37 => 1,
+ 38 => 1,
+ 39 => 1,
+ 40 => 1,
+ 41 => 1,
+ 42 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc
index 511bbe4710..eb724505eb 100644
--- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc
@@ -3,8 +3,9 @@ Abstract Class MyClass Extends MyClass {}
Final Class MyClass Implements MyInterface {}
Interface MyInterface {}
Trait MyTrait {}
+Enum MyEnum IMPLEMENTS Colorful {}
-class MyClass
+ReadOnly class MyClass
{
Var $myVar = null;
Const myConst = true;
diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed
index 859d0d2db2..672fdfb3c4 100644
--- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed
@@ -3,8 +3,9 @@ abstract class MyClass extends MyClass {}
final class MyClass implements MyInterface {}
interface MyInterface {}
trait MyTrait {}
+enum MyEnum implements Colorful {}
-class MyClass
+readonly class MyClass
{
var $myVar = null;
const myConst = true;
diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php
index f2fc20b438..b4d5aae5a1 100644
--- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php
+++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php
@@ -30,9 +30,11 @@ public function getErrorList()
3 => 3,
4 => 1,
5 => 1,
- 9 => 1,
+ 6 => 2,
+ 8 => 1,
10 => 1,
- 13 => 1,
+ 11 => 1,
+ 14 => 1,
];
return $errors;
diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc
index aadbab5b8e..3fe39435b4 100644
--- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc
@@ -137,6 +137,50 @@ trait Base
}
}
+// Valid enum name.
+enum ValidCamelCaseClass: string {}
+
+
+// Incorrect usage of camel case.
+enum invalidCamelCaseClass {}
+enum Invalid_Camel_Case_Class_With_Underscores {}
+
+
+// All lowercase.
+enum invalidlowercaseclass: INT {}
+enum invalid_lowercase_class_with_underscores {}
+
+
+// All uppercase.
+enum VALIDUPPERCASECLASS: int {}
+enum INVALID_UPPERCASE_CLASS_WITH_UNDERSCORES {}
+
+
+// Mix camel case with uppercase.
+enum ValidCamelCaseClassWithUPPERCASE : string {}
+
+
+// Usage of numeric characters.
+enum ValidCamelCaseClassWith1Number {}
+enum ValidCamelCaseClassWith12345Numbers : string {}
+enum ValidCamelCaseClassEndingWithNumber5 {}
+
+enum Testing{}
+
+enum Base
+{
+ public function __construct()
+ {
+ $this->anonymous = new class extends ArrayObject
+ {
+ public function __construct()
+ {
+ parent::__construct(['a' => 1, 'b' => 2]);
+ }
+ };
+ }
+}
+
if ( class_exists( Test :: class ) ) {}
if ( class_exists( Test2 ::class ) ) {}
diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php
index 70777c541b..b7de260b65 100644
--- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php
+++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php
@@ -47,6 +47,11 @@ public function getErrorList()
108 => 1,
118 => 1,
120 => 1,
+ 145 => 1,
+ 146 => 1,
+ 150 => 1,
+ 151 => 1,
+ 156 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
index 877bca648e..7cd04a2114 100644
--- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
@@ -256,3 +256,54 @@ $y = 10 + /* test */ -2;
/*
* No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
*/
+
+?>
+=
+/*
+ * No blank line required above the comment if it's the first non-empty token after a PHP open tag.
+ */
+
+$contentToEcho
+?>
+=
+
+
+/*
+ * No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
+ */
+$contentToEcho
+
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ */
+abstract class MyClass
+{
+ /**
+ * Comment should be ignored.
+ */
+ readonly public string $prop;
+}
+
+/**
+ * Comment should be ignored
+ *
+ */
+enum MyEnum {
+
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
index e4c8ed78e6..2e97614e2c 100644
--- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
@@ -258,3 +258,54 @@ $y = 10 + /* test */ -2;
/*
* No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
*/
+
+?>
+=
+/*
+ * No blank line required above the comment if it's the first non-empty token after a PHP open tag.
+ */
+
+$contentToEcho
+?>
+=
+
+
+/*
+ * No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
+ */
+$contentToEcho
+
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ */
+abstract class MyClass
+{
+ /**
+ * Comment should be ignored.
+ */
+ readonly public string $prop;
+}
+
+/**
+ * Comment should be ignored
+ *
+ */
+enum MyEnum {
+
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.php
index 5dedf0995b..c8a360e244 100644
--- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.php
@@ -77,6 +77,8 @@ public function getErrorList()
232 => 1,
233 => 1,
256 => 1,
+ 271 => 1,
+ 273 => 1,
];
return $errors;
diff --git a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc
index eefa44c9bb..8de3d0a702 100644
--- a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc
@@ -123,3 +123,23 @@ class Space_At_end
{
}//end class
+
+/**
+ * Sample class comment
+ */
+#[Attribute]
+class AllGood
+{
+}
+
+/**
+ * Docblock
+ */
+abstract readonly class AbstractReadonlyWithDocblock {}
+
+/*
+ * Docblock
+ */
+readonly class ReadonlyWrongStyle {}
+
+readonly final class ReadonlyFinalWithoutDocblock {}
diff --git a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php
index 80a404be07..b370d547c0 100644
--- a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php
@@ -26,10 +26,12 @@ class ClassCommentUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 2 => 1,
- 15 => 1,
- 31 => 1,
- 54 => 1,
+ 2 => 1,
+ 15 => 1,
+ 31 => 1,
+ 54 => 1,
+ 143 => 1,
+ 145 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc
index 9c3255cf9b..1a57149b26 100644
--- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc
@@ -79,4 +79,8 @@ class TestClass
}
//end class
-?>
\ No newline at end of file
+enum MissingClosingComment {
+}
+
+enum HasClosingComment {
+}//end enum
diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php
index cdc89ba670..6f5166ec6b 100644
--- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php
@@ -34,6 +34,7 @@ public function getErrorList()
63 => 1,
67 => 1,
79 => 1,
+ 83 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
index e7d880d682..d95acd2ce1 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
@@ -31,7 +31,7 @@ class MyClass
* Some info about the class here
*
*/
-class MyClass
+readonly class MyClass
{
/**
* Some info about the function here.
@@ -77,6 +77,26 @@ class MyClass2
var $x;
}
+abstract class MyClass
+{
+ /**
+* Property comment
+ */
+ readonly public string $prop;
+}
+
+/**
+ * Some info about the enum here
+ *
+*/
+enum Suits: string
+{
+ /**
+ * Some info about the case here.
+ */
+ case HEARTS;
+}
+
/** ************************************************************************
* Example with no errors.
**************************************************************************/
diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed
index 4d8cb39273..ea6488a02b 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed
@@ -31,7 +31,7 @@ class MyClass
* Some info about the class here
*
*/
-class MyClass
+readonly class MyClass
{
/**
* Some info about the function here.
@@ -77,6 +77,26 @@ class MyClass2
var $x;
}
+abstract class MyClass
+{
+ /**
+ * Property comment
+ */
+ readonly public string $prop;
+}
+
+/**
+ * Some info about the enum here
+ *
+ */
+enum Suits: string
+{
+ /**
+ * Some info about the case here.
+ */
+ case HEARTS;
+}
+
/** ************************************************************************
* Example with no errors.
**************************************************************************/
diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
index 974951ce42..acbf13e869 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
@@ -45,6 +45,12 @@ public function getErrorList($testFile='DocCommentAlignmentUnitTest.inc')
if ($testFile === 'DocCommentAlignmentUnitTest.inc') {
$errors[75] = 1;
+ $errors[83] = 1;
+ $errors[84] = 1;
+ $errors[90] = 1;
+ $errors[91] = 1;
+ $errors[95] = 1;
+ $errors[96] = 1;
}
return $errors;
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed
index bae8e7f810..3bcd945388 100644
--- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed
@@ -25,7 +25,7 @@
* @author
* @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600)
-* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://www.php.net/license/3_0.txt
* @summary An unknown summary tag
*
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed
index 82d63d9036..56a392d985 100644
--- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed
@@ -25,7 +25,7 @@
* @author
* @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600)
* @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600)
-* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://www.php.net/license/3_0.txt
* @summary An unknown summary tag
*
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc
new file mode 100644
index 0000000000..1f82abfeaf
--- /dev/null
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc
@@ -0,0 +1,12 @@
+
+ * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600)
+ */
+
+readonly class Foo {
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc
new file mode 100644
index 0000000000..7074dac234
--- /dev/null
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc
@@ -0,0 +1,12 @@
+
+ * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600)
+ */
+#[Attribute]
+class Foo {
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc
new file mode 100644
index 0000000000..5ef90f2ad1
--- /dev/null
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc
@@ -0,0 +1,9 @@
+
+ * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600)
+ */
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc
new file mode 100644
index 0000000000..f6c9d99682
--- /dev/null
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc
@@ -0,0 +1,12 @@
+
+ * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600)
+ */
+
+enum Foo {
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php
index b3708e80cf..1402432aec 100644
--- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php
@@ -44,6 +44,9 @@ public function getErrorList($testFile='FileCommentUnitTest.inc')
case 'FileCommentUnitTest.4.inc':
case 'FileCommentUnitTest.6.inc':
+ case 'FileCommentUnitTest.7.inc':
+ case 'FileCommentUnitTest.9.inc':
+ case 'FileCommentUnitTest.10.inc':
return [1 => 1];
case 'FileCommentUnitTest.5.inc':
diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc
index 1a7020003f..a44f5e0e2e 100644
--- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc
@@ -666,7 +666,7 @@ class Baz {
* Test
*
* @return void
- * @throws E
+ * @throws E
*/
function myFunction() {}
@@ -1000,3 +1000,137 @@ public function foo(object $a, ?object $b) {}
* @return void
*/
function foo($foo) {}
+
+/**
+ * {@inheritDoc}
+ */
+public function foo($a, $b) {}
+
+// phpcs:set Squiz.Commenting.FunctionComment skipIfInheritdoc true
+
+/**
+ * {@inheritDoc}
+ */
+public function foo($a, $b) {}
+
+/**
+ * Foo.
+ *
+ * @param mixed $a Comment.
+ *
+ * @return mixed
+ */
+public function foo(mixed $a): mixed {}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[]
+class Bar {
+ /**
+ * The PHP5 constructor
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] ignored
+/**
+ * Should be ok
+ */
+public function ignored() {
+
+}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+/**
+ * @return void
+ * @throws Exception If any other error occurs. */
+function throwCommentOneLine() {}
+
+/**
+ * When two adjacent pipe symbols are used (by mistake), the sniff should not throw a PHP Fatal error
+ *
+ * @param stdClass||null $object While invalid, this should not throw a PHP Fatal error.
+ * @return void
+ */
+function doublePipeFatalError(?stdClass $object) {}
+
+/**
+ * Test for passing variables by reference
+ *
+ * This sniff treats the '&' as optional for parameters passed by reference, but
+ * forbidden for parameters which are not passed by reference.
+ *
+ * Because mismatches may be in either direction, we cannot auto-fix these.
+ *
+ * @param string $foo A string passed in by reference.
+ * @param string &$bar A string passed in by reference.
+ * @param string $baz A string NOT passed in by reference.
+ * @param string &$qux A string NOT passed in by reference.
+ * @param string &$case1 A string passed in by reference with a case mismatch.
+ * @param string &$CASE2 A string NOT passed in by reference, also with a case mismatch.
+ *
+ * @return void
+ */
+public function variablesPassedByReference(&$foo, &$bar, $baz, $qux, &$CASE1, $case2)
+{
+ return;
+}
+
+/**
+ * Test for param tag containing ref, but param in declaration not being by ref.
+ *
+ * @param string &$foo This should be flagged as (only) ParamNameUnexpectedAmpersandPrefix.
+ * @param string &$bar This should be flagged as (only) ParamNameNoMatch.
+ * @param string &$baz This should be flagged as (only) ParamNameNoCaseMatch.
+ *
+ * @return void
+ */
+function passedByRefMismatch($foo, $bra, $BAZ) {
+ return;
+}
+
+/**
+ * Test variable case
+ *
+ * @param string $foo This parameter is lowercase.
+ * @param string $BAR This parameter is UPPERCASE.
+ * @param string $BazQux This parameter is TitleCase.
+ * @param string $corgeGrault This parameter is camelCase.
+ * @param string $GARPLY This parameter should be in lowercase.
+ * @param string $waldo This parameter should be in TitleCase.
+ * @param string $freD This parameter should be in UPPERCASE.
+ * @param string $PLUGH This parameter should be in TitleCase.
+ *
+ * @return void
+ */
+public function variableCaseTest(
+ $foo,
+ $BAR,
+ $BazQux,
+ $corgeGrault,
+ $garply,
+ $Waldo,
+ $FRED,
+ $PluGh
+) {
+ return;
+}
+
+/**
+ * Test variable order mismatch
+ *
+ * @param string $foo This is the third parameter.
+ * @param string $bar This is the first parameter.
+ * @param string $baz This is the second parameter.
+ *
+ * @return void
+ */
+public function variableOrderMismatch($bar, $baz, $foo) {
+ return;
+}
+
+/**
+ * @return never
+ */
+function foo() {}
diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
index 4d91ecdcab..3d2e10fd6a 100644
--- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
@@ -666,7 +666,7 @@ class Baz {
* Test
*
* @return void
- * @throws E
+ * @throws E
*/
function myFunction() {}
@@ -1000,3 +1000,137 @@ public function foo(object $a, ?object $b) {}
* @return void
*/
function foo($foo) {}
+
+/**
+ * {@inheritDoc}
+ */
+public function foo($a, $b) {}
+
+// phpcs:set Squiz.Commenting.FunctionComment skipIfInheritdoc true
+
+/**
+ * {@inheritDoc}
+ */
+public function foo($a, $b) {}
+
+/**
+ * Foo.
+ *
+ * @param mixed $a Comment.
+ *
+ * @return mixed
+ */
+public function foo(mixed $a): mixed {}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[]
+class Bar {
+ /**
+ * The PHP5 constructor
+ */
+ public function __construct() {
+
+ }
+}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] ignored
+/**
+ * Should be ok
+ */
+public function ignored() {
+
+}
+
+// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+/**
+ * @return void
+ * @throws Exception If any other error occurs. */
+function throwCommentOneLine() {}
+
+/**
+ * When two adjacent pipe symbols are used (by mistake), the sniff should not throw a PHP Fatal error
+ *
+ * @param stdClass|null $object While invalid, this should not throw a PHP Fatal error.
+ * @return void
+ */
+function doublePipeFatalError(?stdClass $object) {}
+
+/**
+ * Test for passing variables by reference
+ *
+ * This sniff treats the '&' as optional for parameters passed by reference, but
+ * forbidden for parameters which are not passed by reference.
+ *
+ * Because mismatches may be in either direction, we cannot auto-fix these.
+ *
+ * @param string $foo A string passed in by reference.
+ * @param string &$bar A string passed in by reference.
+ * @param string $baz A string NOT passed in by reference.
+ * @param string &$qux A string NOT passed in by reference.
+ * @param string &$case1 A string passed in by reference with a case mismatch.
+ * @param string &$CASE2 A string NOT passed in by reference, also with a case mismatch.
+ *
+ * @return void
+ */
+public function variablesPassedByReference(&$foo, &$bar, $baz, $qux, &$CASE1, $case2)
+{
+ return;
+}
+
+/**
+ * Test for param tag containing ref, but param in declaration not being by ref.
+ *
+ * @param string &$foo This should be flagged as (only) ParamNameUnexpectedAmpersandPrefix.
+ * @param string &$bar This should be flagged as (only) ParamNameNoMatch.
+ * @param string &$baz This should be flagged as (only) ParamNameNoCaseMatch.
+ *
+ * @return void
+ */
+function passedByRefMismatch($foo, $bra, $BAZ) {
+ return;
+}
+
+/**
+ * Test variable case
+ *
+ * @param string $foo This parameter is lowercase.
+ * @param string $BAR This parameter is UPPERCASE.
+ * @param string $BazQux This parameter is TitleCase.
+ * @param string $corgeGrault This parameter is camelCase.
+ * @param string $GARPLY This parameter should be in lowercase.
+ * @param string $waldo This parameter should be in TitleCase.
+ * @param string $freD This parameter should be in UPPERCASE.
+ * @param string $PLUGH This parameter should be in TitleCase.
+ *
+ * @return void
+ */
+public function variableCaseTest(
+ $foo,
+ $BAR,
+ $BazQux,
+ $corgeGrault,
+ $garply,
+ $Waldo,
+ $FRED,
+ $PluGh
+) {
+ return;
+}
+
+/**
+ * Test variable order mismatch
+ *
+ * @param string $foo This is the third parameter.
+ * @param string $bar This is the first parameter.
+ * @param string $baz This is the second parameter.
+ *
+ * @return void
+ */
+public function variableOrderMismatch($bar, $baz, $foo) {
+ return;
+}
+
+/**
+ * @return never
+ */
+function foo() {}
diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php
index e8e02f8496..ff1d10c487 100644
--- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php
@@ -26,93 +26,112 @@ class FunctionCommentUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
$errors = [
- 5 => 1,
- 10 => 3,
- 12 => 2,
- 13 => 2,
- 14 => 1,
- 15 => 1,
- 28 => 1,
- 43 => 1,
- 76 => 1,
- 87 => 1,
- 103 => 1,
- 109 => 1,
- 112 => 1,
- 122 => 1,
- 123 => 3,
- 124 => 2,
- 125 => 1,
- 126 => 1,
- 137 => 4,
- 138 => 4,
- 139 => 4,
- 143 => 2,
- 152 => 1,
- 155 => 2,
- 159 => 1,
- 166 => 1,
- 173 => 1,
- 183 => 1,
- 190 => 2,
- 193 => 2,
- 196 => 1,
- 199 => 2,
- 210 => 1,
- 211 => 1,
- 222 => 1,
- 223 => 1,
- 224 => 1,
- 225 => 1,
- 226 => 1,
- 227 => 1,
- 230 => 2,
- 232 => 2,
- 246 => 1,
- 248 => 4,
- 261 => 1,
- 263 => 1,
- 276 => 1,
- 277 => 1,
- 278 => 1,
- 279 => 1,
- 280 => 1,
- 281 => 1,
- 284 => 1,
- 286 => 7,
- 294 => 1,
- 302 => 1,
- 312 => 1,
- 358 => 1,
- 359 => 2,
- 372 => 1,
- 373 => 1,
- 387 => 1,
- 407 => 1,
- 441 => 1,
- 500 => 1,
- 526 => 1,
- 548 => 1,
- 641 => 1,
- 669 => 1,
- 688 => 1,
- 744 => 1,
- 748 => 1,
- 767 => 1,
- 789 => 1,
- 792 => 1,
- 794 => 1,
- 797 => 1,
- 801 => 1,
- 828 => 1,
- 840 => 1,
- 852 => 1,
- 864 => 1,
- 886 => 1,
- 888 => 1,
- 890 => 1,
- 978 => 1,
- 997 => 1,
+ 5 => 1,
+ 10 => 3,
+ 12 => 2,
+ 13 => 2,
+ 14 => 1,
+ 15 => 1,
+ 28 => 1,
+ 43 => 1,
+ 76 => 1,
+ 87 => 1,
+ 103 => 1,
+ 109 => 1,
+ 112 => 1,
+ 122 => 1,
+ 123 => 3,
+ 124 => 2,
+ 125 => 1,
+ 126 => 1,
+ 137 => 4,
+ 138 => 4,
+ 139 => 4,
+ 143 => 2,
+ 155 => 1,
+ 159 => 1,
+ 166 => 1,
+ 173 => 1,
+ 183 => 1,
+ 190 => 2,
+ 193 => 2,
+ 196 => 1,
+ 199 => 2,
+ 210 => 1,
+ 211 => 1,
+ 222 => 1,
+ 223 => 1,
+ 224 => 1,
+ 225 => 1,
+ 226 => 1,
+ 227 => 1,
+ 230 => 2,
+ 232 => 2,
+ 246 => 1,
+ 248 => 4,
+ 261 => 1,
+ 263 => 1,
+ 276 => 1,
+ 277 => 1,
+ 278 => 1,
+ 279 => 1,
+ 280 => 1,
+ 281 => 1,
+ 284 => 1,
+ 286 => 7,
+ 294 => 1,
+ 302 => 1,
+ 312 => 1,
+ 358 => 1,
+ 359 => 2,
+ 372 => 1,
+ 373 => 1,
+ 387 => 1,
+ 407 => 1,
+ 441 => 1,
+ 500 => 1,
+ 526 => 1,
+ 548 => 1,
+ 641 => 1,
+ 669 => 1,
+ 688 => 1,
+ 744 => 1,
+ 748 => 1,
+ 767 => 1,
+ 789 => 1,
+ 792 => 1,
+ 794 => 1,
+ 797 => 1,
+ 801 => 1,
+ 828 => 1,
+ 840 => 1,
+ 852 => 1,
+ 864 => 1,
+ 886 => 1,
+ 888 => 1,
+ 890 => 1,
+ 978 => 1,
+ 997 => 1,
+ 1004 => 2,
+ 1006 => 1,
+ 1029 => 1,
+ 1053 => 1,
+ 1058 => 2,
+ 1069 => 1,
+ 1070 => 1,
+ 1071 => 1,
+ 1080 => 2,
+ 1083 => 1,
+ 1084 => 1,
+ 1085 => 1,
+ 1093 => 4,
+ 1100 => 1,
+ 1101 => 1,
+ 1102 => 1,
+ 1103 => 1,
+ 1123 => 1,
+ 1124 => 1,
+ 1125 => 1,
];
// Scalar type hints only work from PHP 7 onwards.
@@ -128,12 +147,16 @@ public function getErrorList()
$errors[575] = 2;
$errors[627] = 1;
$errors[1002] = 1;
+ $errors[1075] = 6;
+ $errors[1089] = 3;
+ $errors[1107] = 8;
+ $errors[1129] = 3;
} else {
$errors[729] = 4;
$errors[740] = 2;
$errors[752] = 2;
$errors[982] = 1;
- }
+ }//end if
// Object type hints only work from PHP 7.2 onwards.
if (PHP_VERSION_ID >= 70200) {
@@ -142,6 +165,15 @@ public function getErrorList()
$errors[992] = 2;
}
+ // Mixed type hints only work from PHP 8.0 onwards.
+ if (PHP_VERSION_ID >= 80000) {
+ $errors[265] = 1;
+ $errors[459] = 1;
+ $errors[893] = 3;
+ } else {
+ $errors[1023] = 1;
+ }
+
return $errors;
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc
index 377db023d9..024876842e 100644
--- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc
@@ -149,6 +149,43 @@ if ($foo) {
// another comment here.
$foo++;
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ *
+ */
+enum MyEnum {
+
+}
+
+/**
+ * Comment should be ignored.
+ *
+ */
+readonly class MyClass
+{
+ /**
+ * Comment should be ignored.
+ *
+ */
+ readonly $property = 10;
+}
+
/*
* N.B.: The below test line must be the last test in the file.
* Testing that a new line after an inline comment when it's the last non-whitespace
diff --git a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed
index 975143f2c5..949a9ff949 100644
--- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed
@@ -142,6 +142,43 @@ if ($foo) {
// another comment here.
$foo++;
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ *
+ */
+enum MyEnum {
+
+}
+
+/**
+ * Comment should be ignored.
+ *
+ */
+readonly class MyClass
+{
+ /**
+ * Comment should be ignored.
+ *
+ */
+ readonly $property = 10;
+}
+
/*
* N.B.: The below test line must be the last test in the file.
* Testing that a new line after an inline comment when it's the last non-whitespace
diff --git a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc
index 732003b70a..d6c4cf6c87 100644
--- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc
@@ -960,3 +960,74 @@ try {
} finally {
// some code here.
}
+
+$expr = match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+}; //end switch
+
+$expr = match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+};
+
+$array = [
+ 'match' => match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+ },
+];
diff --git a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed
index 6edc34dfc4..176cfe2486 100644
--- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed
@@ -960,3 +960,74 @@ try {
} finally {
// some code here.
}//end try
+
+$expr = match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+}; //end match
+
+$expr = match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+};//end match
+
+$array = [
+ 'match' => match ($foo) {
+ // Line 1
+ // Line 2
+ // Line 3
+ // Line 4
+ // Line 5
+ // Line 6
+ // Line 7
+ // Line 8
+ // Line 9
+ // Line 10
+ // Line 11
+ // Line 12
+ // Line 13
+ // Line 14
+ // Line 15
+ // Line 16
+ // Line 17
+ // Line 18
+ // Line 19
+ // Line 20
+ },//end match
+];
diff --git a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php
index 60d0292d23..5b61612c50 100644
--- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php
@@ -30,32 +30,35 @@ public function getErrorList($testFile='LongConditionClosingCommentUnitTest.inc'
switch ($testFile) {
case 'LongConditionClosingCommentUnitTest.inc':
return [
- 49 => 1,
- 99 => 1,
- 146 => 1,
- 192 => 1,
- 215 => 1,
- 238 => 1,
- 261 => 1,
- 286 => 1,
- 309 => 1,
- 332 => 1,
- 355 => 1,
- 378 => 1,
- 493 => 1,
- 531 => 1,
- 536 => 1,
- 540 => 1,
- 562 => 1,
- 601 => 1,
- 629 => 1,
- 663 => 1,
- 765 => 1,
- 798 => 1,
- 811 => 1,
- 897 => 1,
- 931 => 1,
- 962 => 1,
+ 49 => 1,
+ 99 => 1,
+ 146 => 1,
+ 192 => 1,
+ 215 => 1,
+ 238 => 1,
+ 261 => 1,
+ 286 => 1,
+ 309 => 1,
+ 332 => 1,
+ 355 => 1,
+ 378 => 1,
+ 493 => 1,
+ 531 => 1,
+ 536 => 1,
+ 540 => 1,
+ 562 => 1,
+ 601 => 1,
+ 629 => 1,
+ 663 => 1,
+ 765 => 1,
+ 798 => 1,
+ 811 => 1,
+ 897 => 1,
+ 931 => 1,
+ 962 => 1,
+ 985 => 2,
+ 1008 => 1,
+ 1032 => 1,
];
break;
case 'LongConditionClosingCommentUnitTest.js':
diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc
index 5650e19b38..b83469613f 100644
--- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc
@@ -46,3 +46,9 @@ for (
if ( $condition === true // comment
&& $anotherCondition === false
) {}
+
+$match = match($foo // comment
+ && $bar
+) {
+ 1 => 1, // comment
+};
diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed
index 99c9e92a29..21a4bbe032 100644
--- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed
@@ -50,3 +50,10 @@ for (
if ( $condition === true // comment
&& $anotherCondition === false
) {}
+
+$match = match($foo // comment
+ && $bar
+) {
+ 1 => 1,
+// comment
+};
diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php
index fbc08007c9..a04426f15a 100644
--- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php
@@ -34,6 +34,7 @@ public function getErrorList($testFile='PostStatementCommentUnitTest.inc')
10 => 1,
18 => 1,
35 => 1,
+ 53 => 1,
];
case 'PostStatementCommentUnitTest.1.js':
diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc
index 65f4389bdc..36efc443bf 100644
--- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc
@@ -363,3 +363,42 @@ class Foo
var int $noComment = 1;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+}
+
+class ReadOnlyProps
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ public readonly array $variableName = array();
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var
+ */
+ readonly protected ?int $variableName = 10;
+
+ private readonly string $variable;
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed
index ca0b052e35..5c652f5402 100644
--- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed
@@ -363,3 +363,42 @@ class Foo
var int $noComment = 1;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+}
+
+class ReadOnlyProps
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ public readonly array $variableName = array();
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var
+ */
+ readonly protected ?int $variableName = 10;
+
+ private readonly string $variable;
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php
index f3ee3c76dd..1af5e14845 100644
--- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php
@@ -58,6 +58,8 @@ public function getErrorList()
336 => 1,
361 => 1,
364 => 1,
+ 399 => 1,
+ 403 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc
index 249ae15847..8eaf1b0373 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc
+++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc
@@ -299,6 +299,12 @@ while ( $level-- ):
ob_end_clean();
endwhile;
+$r = match ($x) {
+ 1 => 1,
+};
+
+$r = match($x){1 => 1};
+
// Intentional parse error. This should be the last test in the file.
foreach
// Some unrelated comment.
diff --git a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed
index f0cdde7d78..dc5233d917 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed
@@ -302,6 +302,13 @@ while ( $level-- ):
ob_end_clean();
endwhile;
+$r = match ($x) {
+ 1 => 1,
+};
+
+$r = match ($x) {
+1 => 1};
+
// Intentional parse error. This should be the last test in the file.
foreach
// Some unrelated comment.
diff --git a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php
index 07e57b4661..92427f0da1 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php
+++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php
@@ -76,6 +76,7 @@ public function getErrorList($testFile='ControlSignatureUnitTest.inc')
$errors[276] = 1;
$errors[279] = 1;
$errors[283] = 1;
+ $errors[306] = 3;
}//end if
return $errors;
diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc
index c82f35aee4..1acf3eac43 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc
+++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc
@@ -20,3 +20,5 @@ If ($condition) {
TRY {
} Catch (Exception $e) {
}
+
+$r = MATCH ($x) {};
diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed
index 973781d77f..3c0d3c6378 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed
@@ -20,3 +20,5 @@ if ($condition) {
try {
} catch (Exception $e) {
}
+
+$r = match ($x) {};
diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php
index 758119084f..dc8a3ac6c0 100644
--- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php
+++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php
@@ -37,6 +37,7 @@ public function getErrorList()
16 => 1,
20 => 1,
21 => 1,
+ 24 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed
new file mode 100644
index 0000000000..fd8ad1e683
--- /dev/null
+++ b/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed
@@ -0,0 +1,342 @@
+
diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc
index e9c8061bfd..8e62896387 100644
--- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc
@@ -170,3 +170,34 @@ $value = (binary) $blah + b"binary $foo";
$test = (1 * static::TEST);
$test = myfunc(1 * static::TEST);
+
+$errorPos = $params[$x]?->getLine() + $commentStart;
+
+$foo = $this->gmail ?? $this->gmail = new Google_Service_Gmail($this->google);
+
+exit -1;
+
+$expr = match ($number - 10) {
+ -1 => 0,
+};
+
+$expr = match ($number % 10) {
+ 1 => 2 * $num,
+};
+
+$expr = match (true) {
+ $num * 100 > 500 => 'expression in key',
+};
+
+// PHP 8.0 named parameters.
+if ($pos === count(value: $this->tokens) - 1) {
+ $file = '...'.substr(string: $file, offset: $padding * -1 + 3);
+}
+
+match ($a) {
+ 'a' => -1,
+ 'b', 'c', 'd' => -2,
+ default => -3,
+};
+
+$cntPages = ceil(count($items) / parent::ON_PAGE);
diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed
index a0e0a895b3..9fa0216cb6 100644
--- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed
@@ -170,3 +170,34 @@ $value = ((binary) $blah + b"binary $foo");
$test = (1 * static::TEST);
$test = myfunc(1 * static::TEST);
+
+$errorPos = ($params[$x]?->getLine() + $commentStart);
+
+$foo = ($this->gmail ?? $this->gmail = new Google_Service_Gmail($this->google));
+
+exit -1;
+
+$expr = match ($number - 10) {
+ -1 => 0,
+};
+
+$expr = match ($number % 10) {
+ 1 => (2 * $num),
+};
+
+$expr = match (true) {
+ ($num * 100) > 500 => 'expression in key',
+};
+
+// PHP 8.0 named parameters.
+if ($pos === (count(value: $this->tokens) - 1)) {
+ $file = '...'.substr(string: $file, offset: ($padding * -1 + 3));
+}
+
+match ($a) {
+ 'a' => -1,
+ 'b', 'c', 'd' => -2,
+ default => -3,
+};
+
+$cntPages = ceil(count($items) / parent::ON_PAGE);
diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php
index 651319b877..4f04a6ebd0 100644
--- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php
+++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php
@@ -66,6 +66,12 @@ public function getErrorList($testFile='OperatorBracketUnitTest.inc')
163 => 2,
165 => 2,
169 => 1,
+ 174 => 1,
+ 176 => 1,
+ 185 => 1,
+ 189 => 1,
+ 193 => 1,
+ 194 => 2,
];
break;
case 'OperatorBracketUnitTest.js':
diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc
index 3076104a2b..811c56ec14 100644
--- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc
@@ -197,3 +197,108 @@ function foo(
$bar
) {
}
+
+class ConstructorPropertyPromotionSingleLineDocblockIndentOK
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+/**
+ * @var string
+ * @Assert\NotBlank()
+ */
+#[NotBlank]
+private string $private,
+ ) {
+ }
+}
+
+// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration.
+function usingNewInInitializersCallParamsIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA(),
+ new InjectedDependencyB
+ )
+) {}
+
+function usingNewInInitializersCallParamsNotIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA,
+ new InjectedDependencyB()
+ )
+) {}
+
+function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+new InjectedDependencyA(), new InjectedDependencyB()
+)
+) {}
+
+class UsingNewInInitializers {
+ public function doSomething(
+ object $paramA,
+ stdClass $paramB = new stdClass(),
+ Exception $paramC = new Exception(
+ new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+
+ public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ Exception $param = new Exception(
+new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+}
diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed
index 4965f237c0..c38e3ecc0a 100644
--- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed
@@ -209,3 +209,108 @@ function foo(
$bar
) {
}
+
+class ConstructorPropertyPromotionSingleLineDocblockIndentOK
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent
+{
+ public function __construct(
+ /** @var string */
+ public string $public,
+ /** @var string */
+ private string $private,
+ ) {
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent
+{
+ public function __construct(
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ public string $public,
+ /**
+ * @var string
+ * @Assert\NotBlank()
+ */
+ #[NotBlank]
+ private string $private,
+ ) {
+ }
+}
+
+// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration.
+function usingNewInInitializersCallParamsIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA(),
+ new InjectedDependencyB
+ )
+) {}
+
+function usingNewInInitializersCallParamsNotIndented(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+ new InjectedDependencyA,
+ new InjectedDependencyB()
+ )
+) {}
+
+function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ int $paramA,
+ string $paramB,
+ object $paramC = new SomeClass(
+new InjectedDependencyA(), new InjectedDependencyB()
+)
+) {}
+
+class UsingNewInInitializers {
+ public function doSomething(
+ object $paramA,
+ stdClass $paramB = new stdClass(),
+ Exception $paramC = new Exception(
+ new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+
+ public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed(
+ Exception $param = new Exception(
+new ExceptionMessage(),
+ new ExceptionCode(),
+ ),
+ ) {
+ }
+}
diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php
index caf14d1de1..5208ad0cdb 100644
--- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php
+++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php
@@ -55,7 +55,21 @@ public function getErrorList($testFile='MultiLineFunctionDeclarationUnitTest.inc
190 => 2,
194 => 1,
195 => 1,
- 196 => 1,
+ 233 => 1,
+ 234 => 1,
+ 235 => 1,
+ 236 => 1,
+ 244 => 1,
+ 245 => 1,
+ 246 => 1,
+ 247 => 1,
+ 248 => 1,
+ 249 => 1,
+ 250 => 1,
+ 251 => 1,
+ 252 => 1,
+ 253 => 1,
+ 254 => 1,
];
} else {
$errors = [
diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc
index 5692d6b33b..87c3bdf2e7 100644
--- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc
+++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc
@@ -142,3 +142,16 @@ $anonClass = new class() {
}
};
+echo $obj?->varName;
+echo $obj?->var_name;
+echo $obj?->varname;
+echo $obj?->_varName;
+
+enum SomeEnum
+{
+ public function foo($foo, $_foo, $foo_bar) {
+ $bar = 1;
+ $_bar = 2;
+ $bar_foo = 3;
+ }
+}
diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php
index 4ec65e5e1a..9acbe241a8 100644
--- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php
+++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php
@@ -60,6 +60,9 @@ public function getErrorList()
123 => 1,
138 => 1,
141 => 1,
+ 146 => 1,
+ 152 => 1,
+ 155 => 1,
];
return $errors;
diff --git a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
index 8a5c232319..725c60ed69 100644
--- a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
@@ -13,5 +13,41 @@ function foo() { return new MyClass(); }
$doodad = $x ? new Foo : new Bar;
+function returnFn() {
+ $fn = fn($x) => new MyClass();
+}
+
+function returnMatch() {
+ $match = match($x) {
+ 0 => new MyClass()
+ }
+}
+
+// Issue 3333.
+$time2 ??= new \DateTime();
+$time3 = $time1 ?? new \DateTime();
+$time3 = $time1 ?? $time2 ?? new \DateTime();
+
+function_call($time1 ?? new \DateTime());
+$return = function_call($time1 ?? new \DateTime()); // False negative depending on interpretation of the sniff.
+
+function returnViaTernary() {
+ return ($y == false ) ? ($x === true ? new Foo : new Bar) : new FooBar;
+}
+
+function nonAssignmentTernary() {
+ if (($x ? new Foo() : new Bar) instanceof FooBar) {
+ // Do something.
+ }
+}
+
+// Test for tokenizer issue #3789.
+$a = $b !== null
+ ? match ($c) {
+ default => 5,
+ }
+ : new Foo;
+
+// Intentional parse error. This must be the last test in the file.
function new
?>
diff --git a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
index fa32521c85..f9979fa29f 100644
--- a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
+++ b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
@@ -26,8 +26,10 @@ class ObjectInstantiationUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 5 => 1,
- 8 => 1,
+ 5 => 1,
+ 8 => 1,
+ 31 => 1,
+ 39 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc
index e8d3beb94c..a4f82d1a8c 100644
--- a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc
@@ -37,4 +37,6 @@ $var = (1 + $var);
$expected[$i]['sort_order'] = ($i + 1);
$expected[($i + 1)]['sort_order'] = ($i + 1);
-$id = $id.($this->i++).$id;
+$id = $id.($obj->i++).$id;
+$id = $obj?->i++.$id;
+$id = $obj?->i++*10;
diff --git a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php
index 768911b508..3846905da0 100644
--- a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php
+++ b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php
@@ -35,6 +35,8 @@ public function getErrorList()
27 => 1,
29 => 1,
31 => 1,
+ 41 => 1,
+ 42 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php b/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
index d51f23ca3b..36c556d8c2 100644
--- a/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
+++ b/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
@@ -49,7 +49,6 @@ public function getWarningList($testFile='CommentedOutCodeUnitTest.inc')
8 => 1,
15 => 1,
19 => 1,
- 35 => 1,
87 => 1,
91 => 1,
97 => 1,
diff --git a/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc
index d4fce10f4d..a07047b196 100644
--- a/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc
@@ -62,3 +62,22 @@ $a = [
$a = [
'a' => ($foo) ? fn() => return 1 : fn() => return 2,
];
+
+$var = $foo->something(!$var);
+$var = $foo?->something(!$var);
+
+$callback = function ($value) {
+ if ($value > 10) {
+ return false;
+ }
+};
+
+function issue3616() {
+ $food = 'cake';
+
+ $returnValue = match (true) {
+ $food === 'apple' => 'This food is an apple',
+ $food === 'bar' => 'This food is a bar',
+ $food === 'cake' => 'This food is a cake',
+ };
+}
diff --git a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc
index 98c1bb5661..f657fb4a4c 100644
--- a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc
@@ -75,3 +75,36 @@ class Bar
public bool $c = false, $d = true;
protected int $e = 123, $f = 987;
}
+
+switch ($b < 10 && $a = 10) {
+ case true:
+ break;
+}
+
+$array = [
+ match ($b < 10 && $a = 10) {
+ true => 10,
+ false => 0
+ },
+];
+
+$arrow_function = fn ($a = null) => $a;
+
+function ($html) {
+ $regEx = '/regexp/';
+
+ return preg_replace_callback($regEx, function ($matches) {
+ [$all] = $matches;
+ return $all;
+ }, $html);
+};
+
+
+function () {
+ $a = false;
+
+ some_label:
+
+ $b = getB();
+};
+
diff --git a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php
index 919d4570d0..618d76efa4 100644
--- a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php
+++ b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php
@@ -33,6 +33,8 @@ public function getErrorList()
12 => 1,
14 => 1,
15 => 1,
+ 79 => 1,
+ 85 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc
index 76ec49962c..56802e37aa 100644
--- a/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc
@@ -55,4 +55,4 @@ do {
} while($a->count);
for ($i = 0; $i < $a->count; $i++) {}
-?>
+for ($i = 0; $i < $a?->count; $i++) {}
diff --git a/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc
index ca457a2e9a..3c875d0983 100644
--- a/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc
@@ -2,4 +2,6 @@
error_log('test');
print_r($array);
var_dump($array);
-?>
+
+#[Var_Dump(10)]
+function debugMe() {}
diff --git a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc
index dd851461b9..d16c7f2eb6 100644
--- a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc
@@ -48,3 +48,40 @@ new class {
}
}
};
+
+$outerClosure = function ()
+{
+ // Functions inside closures are not allowed.
+ function innerFunction() {
+ }
+};
+
+// Allow methods in classes/traits/interfaces defined inside functions
+function foo() {
+ if (class_exists('MyClass') === false) {
+ class MyClass {
+ function foo() {}
+ }
+ }
+
+ if (trait_exists('MyTrait') === false) {
+ trait MyTrait {
+ function foo() {}
+ }
+ }
+
+ if (interface_exists('MyInterface') === false) {
+ interface MyInterface {
+ function foo();
+ }
+ }
+
+ // But disallow functions nested inside those methods
+ if (class_exists('NestedFunctionInMethod') === false) {
+ class NestedFunctionInMethod {
+ function foo() {
+ function innerFunction() {}
+ }
+ }
+ }
+}
diff --git a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php
index 3c9ad07bd3..b0b13b6147 100644
--- a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php
+++ b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php
@@ -28,6 +28,8 @@ public function getErrorList()
return [
5 => 1,
46 => 1,
+ 55 => 1,
+ 83 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc
index 330919d1c0..702b13de0a 100644
--- a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc
+++ b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc
@@ -39,3 +39,12 @@ $callToNamespacedFunction = MyNamespace /* phpcs:ignore Standard */ \STR_REPEAT(
$callToNamespacedFunction = namespace\STR_REPEAT($a, 2); // Could potentially be false negative.
$filePath = new \File($path);
+
+$count = $object?->Count();
+
+class AttributesShouldBeIgnored
+{
+ #[Putenv('FOO', 'foo')]
+ public function foo(): void
+ {}
+}
diff --git a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed
index eae5b4ade4..281425c595 100644
--- a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed
@@ -39,3 +39,12 @@ $callToNamespacedFunction = MyNamespace /* phpcs:ignore Standard */ \STR_REPEAT(
$callToNamespacedFunction = namespace\STR_REPEAT($a, 2); // Could potentially be false negative.
$filePath = new \File($path);
+
+$count = $object?->Count();
+
+class AttributesShouldBeIgnored
+{
+ #[Putenv('FOO', 'foo')]
+ public function foo(): void
+ {}
+}
diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc
index 163c312569..051b6c6b11 100644
--- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc
+++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc
@@ -296,3 +296,105 @@ class TestAlternativeControlStructures {
$var_after_class_in_global_space = 1;
do_something_else();
+
+// These are parse errors, but that's not the concern of the sniff.
+function parseError1() {
+ defined('FOO') or return 'foo';
+ echo 'unreachable';
+}
+
+function parseError2() {
+ defined('FOO') || continue;
+ echo 'unreachable';
+}
+
+// All logical operators are allowed with inline expressions (but this was not correctly handled by the sniff).
+function exitExpressionsWithLogicalOperators() {
+ $condition = false;
+ $condition || exit();
+ $condition or die();
+
+ $condition = true;
+ $condition && die();
+ $condition and exit;
+
+ $condition xor die();
+
+ echo 'still executable as exit, in all of the above cases, is used as part of an expression';
+}
+
+// Inline expressions are allowed in ternaries.
+function exitExpressionsInTernary() {
+ $value = $myValue ? $myValue : exit();
+ $value = $myValue ?: exit();
+ $value = $var == 'foo' ? 'bar' : die( 'world' );
+
+ $value = (!$myValue ) ? exit() : $myValue;
+ $value = $var != 'foo' ? die( 'world' ) : 'bar';
+
+ echo 'still executable';
+}
+
+// Inline expressions are allowed with null coalesce and null coalesce equals.
+function exitExpressionsWithNullCoalesce() {
+ $value = $nullableValue ?? exit();
+ $value ??= die();
+ echo 'still executable';
+}
+
+// Inline expressions are allowed in arrow functions.
+function exitExpressionsInArrowFunction() {
+ $callable = fn() => die();
+ echo 'still executable';
+}
+
+// PHP 8.0+: throw expressions which don't stop execution.
+function nonStoppingThrowExpressions() {
+ $callable = fn() => throw new Exception();
+
+ $value = $myValue ? 'something' : throw new Exception();
+ $value = $myValue ?: throw new Exception();
+ $value = $myValue ? throw new Exception() : 'something';
+
+ $value = $nullableValue ?? throw new Exception();
+ $value ??= throw new Exception();
+
+ $condition && throw new Exception();
+ $condition || throw new Exception();
+ $condition and throw new Exception();
+ $condition or throw new Exception();
+
+ echo 'still executable as throw, in all of the above cases, is used as part of an expression';
+
+ throw new Exception();
+ echo 'non-executable';
+}
+
+// PHP 8.0+: throw expressions which do stop execution.
+function executionStoppingThrowExpressionsA() {
+ $condition xor throw new Exception();
+ echo 'non-executable';
+}
+
+function executionStoppingThrowExpressionsB() {
+ throw $userIsAuthorized ? new ForbiddenException() : new UnauthorizedException();
+ echo 'non-executable';
+}
+
+function executionStoppingThrowExpressionsC() {
+ throw $condition1 && $condition2 ? new Exception1() : new Exception2();
+ echo 'non-executable';
+}
+
+function executionStoppingThrowExpressionsD() {
+ throw $exception ??= new Exception();
+ echo 'non-executable';
+}
+
+function executionStoppingThrowExpressionsE() {
+ throw $maybeNullException ?? new Exception();
+ echo 'non-executable';
+}
+
+// Intentional syntax error.
+return array_map(
diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc
index 407c4740b1..9b7a22bcc9 100644
--- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc
+++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc
@@ -45,6 +45,12 @@ trait Something {
}
}
+enum Something {
+ function getReturnType() {
+ echo 'no error';
+ }
+}
+
$a = new class {
public function log($msg)
{
@@ -52,4 +58,16 @@ $a = new class {
}
};
+// Multiple statements are still one line of unreachable code, so should get
+// only one complaint from this sniff. (Well, technically two here since there
+// are two 'exit()' statements above, so one complaint from each of those. So,
+// two here, but not six.)
+echo 'one'; echo 'two'; echo 'three';
+
+// A single statement split across multiple lines. Here we get complaints for
+// each line, even though they're all part of one statement.
+echo 'one' . 'two'
+ . 'three' . 'four'
+ . 'five' . 'six';
+
interface MyInterface {
diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc
new file mode 100644
index 0000000000..6fe5c16c66
--- /dev/null
+++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ non-executable
+
+
+
+
+
+
+
+
+ non-executable
+
+
+
+
+
+
+
+
+ non-executable
+
+
+
+
+
+
+
+
+ = 'unreachable - no semicolon' ?>
+
+
+
+
+
+
+
+ = 'unreachable - with semicolon'; ?>
+
+
diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php
index f66eb3f7fe..40e4068e42 100644
--- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php
+++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php
@@ -74,6 +74,14 @@ public function getWarningList($testFile='')
252 => 1,
253 => 1,
254 => 2,
+ 303 => 1,
+ 308 => 1,
+ 370 => 1,
+ 376 => 1,
+ 381 => 1,
+ 386 => 1,
+ 391 => 1,
+ 396 => 1,
];
break;
case 'NonExecutableCodeUnitTest.2.inc':
@@ -83,9 +91,21 @@ public function getWarningList($testFile='')
9 => 1,
10 => 2,
14 => 1,
- 48 => 2,
+ 54 => 2,
+ 65 => 2,
+ 69 => 2,
+ 70 => 2,
+ 71 => 2,
];
break;
+ case 'NonExecutableCodeUnitTest.3.inc':
+ return [
+ 27 => 1,
+ 36 => 1,
+ 45 => 1,
+ 54 => 1,
+ 62 => 1,
+ ];
default:
return [];
break;
diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc
index cec0355c43..3cc617d75b 100644
--- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc
@@ -40,3 +40,18 @@ class Nested {
};
}
}
+
+enum SomeEnum
+{
+ function func1() {}
+ public function func1() {}
+ private function func1() {}
+ protected function func1() {}
+}
+
+class UnconventionalSpacing {
+ public
+ static
+ function
+ myFunction() {}
+}
diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php
index 7fdab23b77..4dc7177953 100644
--- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php
+++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php
@@ -29,6 +29,7 @@ public function getErrorList()
6 => 1,
30 => 1,
39 => 1,
+ 46 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc
index 38b443f2fd..dd6530e802 100644
--- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc
@@ -115,3 +115,13 @@ $b = new class()
return $This;
}
}
+
+enum MyEnum {
+ private function notStatic () {
+ $this->doSomething();
+ }
+
+ public static function myFunc() {
+ $this->doSomething();
+ }
+}
diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php
index 2935241b44..b1a5dd6afd 100644
--- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php
+++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php
@@ -26,18 +26,19 @@ class StaticThisUsageUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 7 => 1,
- 8 => 1,
- 9 => 1,
- 14 => 1,
- 20 => 1,
- 41 => 1,
- 61 => 1,
- 69 => 1,
- 76 => 1,
- 80 => 1,
- 84 => 1,
- 99 => 1,
+ 7 => 1,
+ 8 => 1,
+ 9 => 1,
+ 14 => 1,
+ 20 => 1,
+ 41 => 1,
+ 61 => 1,
+ 69 => 1,
+ 76 => 1,
+ 80 => 1,
+ 84 => 1,
+ 99 => 1,
+ 125 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc
index 785a4a8cc3..70abae434d 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc
@@ -251,3 +251,19 @@ echo 'hi';
?>
+
+ 1,
+ 2 => 2,
+
+};
+echo $expr;
+
+if($true) {
+
+ enum SomeEnum {}
+
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed
index 1674b3dbfa..c64de25e16 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed
@@ -244,3 +244,18 @@ echo 'hi';
?>
+
+ 1,
+ 2 => 2,
+};
+
+echo $expr;
+
+if($true) {
+
+ enum SomeEnum {}
+
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php
index eeef2b0635..ac3f5d6ff5 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php
@@ -57,6 +57,9 @@ public function getErrorList($testFile='ControlStructureSpacingUnitTest.inc')
242 => 1,
246 => 1,
248 => 1,
+ 257 => 3,
+ 261 => 1,
+ 262 => 1,
];
break;
case 'ControlStructureSpacingUnitTest.js':
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc
index 2e058050d8..36a287f077 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc
@@ -540,6 +540,37 @@ class MyClass {
// function d() {}
}
+class ClassWithAttributes {
+
+ #[Attribute1]
+ #[Attribute2]
+ function a(){}
+
+
+ #[Attribute3]
+ function b(){}
+ #[Attribute4]
+ function c(){}
+
+
+ /**
+ * Description.
+ */
+ #[Attribute5]
+ function d(){}
+ /**
+ * Description.
+ */
+ #[Attribute6]
+ #[Attribute7]
+ function e(){}
+
+
+ #[Attribute8]
+ #[Attribute9]
+ function f(){}
+}
+
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacing 2
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingBeforeFirst 2
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingAfterLast 2
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed
index eec26c6913..ac2df1cb7b 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed
@@ -624,6 +624,35 @@ class MyClass {
// function d() {}
}
+class ClassWithAttributes {
+ #[Attribute1]
+ #[Attribute2]
+ function a(){}
+
+ #[Attribute3]
+ function b(){}
+
+ #[Attribute4]
+ function c(){}
+
+ /**
+ * Description.
+ */
+ #[Attribute5]
+ function d(){}
+
+ /**
+ * Description.
+ */
+ #[Attribute6]
+ #[Attribute7]
+ function e(){}
+
+ #[Attribute8]
+ #[Attribute9]
+ function f(){}
+}
+
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacing 2
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingBeforeFirst 2
// phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingAfterLast 2
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php
index 9f202888a9..fabb6adb92 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php
@@ -90,6 +90,11 @@ public function getErrorList($testFile='')
495 => 1,
529 => 1,
539 => 1,
+ 547 => 2,
+ 551 => 1,
+ 553 => 1,
+ 560 => 1,
+ 566 => 1,
];
case 'FunctionSpacingUnitTest.2.inc':
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
index fd7c6e34fc..12b55176c4 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
@@ -332,3 +332,43 @@ class CommentedOutCodeAtStartOfClassNoBlankLine {
*/
public $property = true;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+
+ #[ORM\Id]#[ORM\Column("integer")]
+
+ private $id;
+
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+
+ #[SingleAttribute]
+ protected $propertySingle;
+
+ #[FirstAttribute]
+ #[SecondAttribute]
+ protected $propertyDouble;
+ #[ThirdAttribute]
+ protected $propertyWithoutSpacing;
+}
+
+enum SomeEnum
+{
+ // Enum cannot have properties
+
+ case ONE = 'one';
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
index b6ebcc9ab1..d683eaadfb 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
@@ -319,3 +319,41 @@ class CommentedOutCodeAtStartOfClassNoBlankLine {
*/
public $property = true;
}
+
+class HasAttributes
+{
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+
+ #[SingleAttribute]
+ protected $propertySingle;
+
+ #[FirstAttribute]
+ #[SecondAttribute]
+ protected $propertyDouble;
+
+ #[ThirdAttribute]
+ protected $propertyWithoutSpacing;
+}
+
+enum SomeEnum
+{
+ // Enum cannot have properties
+
+ case ONE = 'one';
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
index 08a11bca41..9b4066811a 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
@@ -57,6 +57,11 @@ public function getErrorList()
288 => 1,
292 => 1,
333 => 1,
+ 342 => 1,
+ 346 => 1,
+ 353 => 1,
+ 357 => 1,
+ 366 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc
index bcea3dc7a8..67f8ee1447 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc
@@ -46,3 +46,7 @@ thisObject::
testThis();
// phpcs:set Squiz.WhiteSpace.ObjectOperatorSpacing ignoreNewlines false
+
+$this?->testThis();
+$this?-> testThis();
+$this ?-> testThis();
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed
index fdeb6f3140..730b8e4a72 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed
@@ -42,3 +42,7 @@ thisObject::
testThis();
// phpcs:set Squiz.WhiteSpace.ObjectOperatorSpacing ignoreNewlines false
+
+$this?->testThis();
+$this?->testThis();
+$this?->testThis();
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php
index 48208cef32..82a4056f7e 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php
@@ -43,6 +43,8 @@ public function getErrorList()
39 => 1,
40 => 2,
42 => 2,
+ 51 => 1,
+ 52 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc
index 8d0efb3246..06462acc35 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc
@@ -463,5 +463,25 @@ $a = [$a, - $b];
$a = $a[- $b];
$a = $a ? - $b : - $b;
+exit -1;
+
+$cl = function ($boo =-1) {};
+$cl = function ($boo =+1) {};
+$fn = fn ($boo =-1) => $boo;
+$fn = fn ($boo =+1) => $boo;
+
+$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
+
+$a = 'a '.-MY_CONSTANT;
+$a = 'a '.-$b;
+$a = 'a '.- MY_CONSTANT;
+$a = 'a '.- $b;
+
+match ($a) {
+ 'a' => -1,
+ 'b', 'c', 'd' => -2,
+ default => -3,
+};
+
/* Intentional parse error. This has to be the last test in the file. */
$a = 10 +
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed
index a03b642003..8b92a4875a 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed
@@ -457,5 +457,25 @@ $a = [$a, - $b];
$a = $a[- $b];
$a = $a ? - $b : - $b;
+exit -1;
+
+$cl = function ($boo =-1) {};
+$cl = function ($boo =+1) {};
+$fn = fn ($boo =-1) => $boo;
+$fn = fn ($boo =+1) => $boo;
+
+$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
+
+$a = 'a '.-MY_CONSTANT;
+$a = 'a '.-$b;
+$a = 'a '.- MY_CONSTANT;
+$a = 'a '.- $b;
+
+match ($a) {
+ 'a' => -1,
+ 'b', 'c', 'd' => -2,
+ default => -3,
+};
+
/* Intentional parse error. This has to be the last test in the file. */
$a = 10 +
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
index 95cd371706..ecae5c6d53 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
@@ -107,3 +107,28 @@ public function foo()
}
$fn1 = fn($x) => $x + $y;
+
+$match = match ($test) { 1 => 'a', 2 => 'b' };
+
+$match = match ($test) {
+ 1 => 'a',
+ 2 => 'b'
+ };
+
+?>
+
+
+
+
+
+ $x + $y;
+
+$match = match ($test) { 1 => 'a', 2 => 'b'
+};
+
+$match = match ($test) {
+ 1 => 'a',
+ 2 => 'b'
+};
+
+?>
+
+
+
+
+
+
+ 1,
80 => 1,
102 => 1,
+ 111 => 1,
+ 116 => 1,
+ 122 => 1,
+ 130 => 1,
+ 134 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc
index 791cc83f93..12685dc97d 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc
@@ -25,7 +25,7 @@ class MyClass
public static$var = null;
- public
+ public
static
$var = null;
}
@@ -82,3 +82,60 @@ class MyOtherClass
$varS,
$varT
}
+
+// Issue #3188 - static as return type.
+public static function fCreate($attributes = []): static
+{
+ return static::factory()->create($attributes);
+}
+
+public static function fCreate($attributes = []): ?static
+{
+ return static::factory()->create($attributes);
+}
+
+// Also account for static used within union types.
+public function fCreate($attributes = []): object|static
+{
+}
+
+// Ensure that static as a scope keyword when preceeded by a colon which is not for a type declaration is still handled.
+$callback = $cond ? get_fn_name() : static function ($a) { return $a * 10; };
+
+class TypedProperties {
+ public
+ int $var;
+
+ protected string $stringA, $stringB;
+
+ private bool
+ $boolA,
+ $boolB;
+}
+
+// PHP 8.0 constructor property promotion.
+class ConstructorPropertyPromotionTest {
+ public function __construct(
+ public $x = 0.0,
+ protected $y = '',
+ private $z = null,
+ $normalParam,
+ ) {}
+}
+
+class ConstructorPropertyPromotionWithTypesTest {
+ public function __construct(protected float|int $x, public?string &$y = 'test', private mixed $z) {}
+}
+
+// PHP 8.1 readonly keywords.
+class ReadonlyTest {
+ public readonly int $publicReadonlyProperty;
+
+ protected readonly int $protectedReadonlyProperty;
+
+ readonly protected int $protectedReadonlyProperty;
+
+ readonly private int $privateReadonlyProperty;
+
+ public function __construct(readonly protected float|int $x, public readonly?string &$y = 'test') {}
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed
index a4b792b3c5..d3b682ed75 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed
@@ -77,3 +77,59 @@ class MyOtherClass
$varS,
$varT
}
+
+// Issue #3188 - static as return type.
+public static function fCreate($attributes = []): static
+{
+ return static::factory()->create($attributes);
+}
+
+public static function fCreate($attributes = []): ?static
+{
+ return static::factory()->create($attributes);
+}
+
+// Also account for static used within union types.
+public function fCreate($attributes = []): object|static
+{
+}
+
+// Ensure that static as a scope keyword when preceeded by a colon which is not for a type declaration is still handled.
+$callback = $cond ? get_fn_name() : static function ($a) { return $a * 10; };
+
+class TypedProperties {
+ public int $var;
+
+ protected string $stringA, $stringB;
+
+ private bool
+ $boolA,
+ $boolB;
+}
+
+// PHP 8.0 constructor property promotion.
+class ConstructorPropertyPromotionTest {
+ public function __construct(
+ public $x = 0.0,
+ protected $y = '',
+ private $z = null,
+ $normalParam,
+ ) {}
+}
+
+class ConstructorPropertyPromotionWithTypesTest {
+ public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {}
+}
+
+// PHP 8.1 readonly keywords.
+class ReadonlyTest {
+ public readonly int $publicReadonlyProperty;
+
+ protected readonly int $protectedReadonlyProperty;
+
+ readonly protected int $protectedReadonlyProperty;
+
+ readonly private int $privateReadonlyProperty;
+
+ public function __construct(readonly protected float|int $x, public readonly ?string &$y = 'test') {}
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php
index c24dcc49d7..30b66215c9 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php
@@ -26,18 +26,27 @@ class ScopeKeywordSpacingUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 7 => 2,
- 8 => 1,
- 13 => 1,
- 14 => 1,
- 15 => 1,
- 17 => 2,
- 26 => 1,
- 28 => 1,
- 29 => 1,
- 64 => 1,
- 67 => 1,
- 71 => 1,
+ 7 => 2,
+ 8 => 1,
+ 13 => 1,
+ 14 => 1,
+ 15 => 1,
+ 17 => 2,
+ 26 => 1,
+ 28 => 1,
+ 29 => 1,
+ 64 => 1,
+ 67 => 1,
+ 71 => 1,
+ 103 => 1,
+ 106 => 1,
+ 111 => 1,
+ 119 => 1,
+ 121 => 1,
+ 127 => 2,
+ 134 => 2,
+ 138 => 2,
+ 140 => 3,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css
index 1dd1b6e6ba..e3f3f02918 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css
+++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css
@@ -23,3 +23,10 @@
}
/* phpcs:set Squiz.WhiteSpace.SuperfluousWhitespace ignoreBlankLines false */
+// /**
+// * This text is in two types of comment: each line is commented out
+// * individually, and the whole block is in what looks like a
+// * docblock-comment. This sniff should ignore all this text as there
+// * is no superfluous white-space here.
+// */
+
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed
index 59ddddb084..11be21d5c9 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed
@@ -21,3 +21,10 @@
float: left;
}
/* phpcs:set Squiz.WhiteSpace.SuperfluousWhitespace ignoreBlankLines false */
+
+// /**
+// * This text is in two types of comment: each line is commented out
+// * individually, and the whole block is in what looks like a
+// * docblock-comment. This sniff should ignore all this text as there
+// * is no superfluous white-space here.
+// */
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php
index b9ff96fbc7..2f1ead2307 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php
@@ -84,7 +84,7 @@ public function getErrorList($testFile='SuperfluousWhitespaceUnitTest.inc')
8 => 1,
9 => 1,
11 => 1,
- 25 => 1,
+ 32 => 1,
];
break;
default:
diff --git a/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php b/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php
index d18c947294..5df4b0fc2c 100644
--- a/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php
+++ b/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php
@@ -14,6 +14,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Exceptions\RuntimeException;
+use PHP_CodeSniffer\Util\Common;
class CodeAnalyzerSniff implements Sniff
{
@@ -53,7 +54,7 @@ public function process(File $phpcsFile, $stackPtr)
// In the command, 2>&1 is important because the code analyzer sends its
// findings to stderr. $output normally contains only stdout, so using 2>&1
// will pipe even stderr to stdout.
- $cmd = escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1';
+ $cmd = Common::escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1';
// There is the possibility to pass "--ide" as an option to the analyzer.
// This would result in an output format which would be easier to parse.
diff --git a/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php
index ea98fd2847..267cd0ad62 100644
--- a/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php
+++ b/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php
@@ -38,7 +38,9 @@ protected function processVariable(File $phpcsFile, $stackPtr)
}
$objOperator = $phpcsFile->findNext([T_WHITESPACE], ($stackPtr + 1), null, true);
- if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR) {
+ if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR
+ || $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR
+ ) {
// Check to see if we are using a variable from an object.
$var = $phpcsFile->findNext([T_WHITESPACE], ($objOperator + 1), null, true);
if ($tokens[$var]['code'] === T_STRING) {
diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc
index 1bf486cab6..3325e1152d 100644
--- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc
+++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc
@@ -116,3 +116,16 @@ $anonClass = new class() {
$bar_foo = 3;
}
};
+
+echo $obj?->varName;
+echo $obj?->var_name;
+echo $obj?->varName;
+
+enum SomeEnum
+{
+ public function foo($foo, $_foo, $foo_bar) {
+ $bar = 1;
+ $_bar = 2;
+ $bar_foo = 3;
+ }
+}
diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php
index d22b24fd3f..e57c735646 100644
--- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php
+++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php
@@ -53,6 +53,9 @@ public function getErrorList()
99 => 1,
113 => 1,
116 => 1,
+ 121 => 1,
+ 126 => 1,
+ 129 => 1,
];
}//end getErrorList()
diff --git a/src/Tokenizers/CSS.php b/src/Tokenizers/CSS.php
index b7c2018bcc..2f486d6cfa 100644
--- a/src/Tokenizers/CSS.php
+++ b/src/Tokenizers/CSS.php
@@ -196,7 +196,11 @@ public function tokenize($string)
// The first and last tokens are the open/close tags.
array_shift($commentTokens);
- array_pop($commentTokens);
+ $closeTag = array_pop($commentTokens);
+
+ while ($closeTag['content'] !== '?'.'>') {
+ $closeTag = array_pop($commentTokens);
+ }
if ($leadingZero === true) {
$commentTokens[0]['content'] = substr($commentTokens[0]['content'], 1);
diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php
index 2fc86a86d6..0fa6d42c17 100644
--- a/src/Tokenizers/PHP.php
+++ b/src/Tokenizers/PHP.php
@@ -152,6 +152,13 @@ class PHP extends Tokenizer
'shared' => false,
'with' => [],
],
+ T_ENUM => [
+ 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET],
+ 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET],
+ 'strict' => true,
+ 'shared' => false,
+ 'with' => [],
+ ],
T_USE => [
'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET],
'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET],
@@ -251,6 +258,13 @@ class PHP extends Tokenizer
T_SWITCH => T_SWITCH,
],
],
+ T_MATCH => [
+ 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET],
+ 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET],
+ 'strict' => true,
+ 'shared' => false,
+ 'with' => [],
+ ],
T_START_HEREDOC => [
'start' => [T_START_HEREDOC => T_START_HEREDOC],
'end' => [T_END_HEREDOC => T_END_HEREDOC],
@@ -283,8 +297,10 @@ class PHP extends Tokenizer
T_ENDFOREACH => T_ENDFOREACH,
T_ENDWHILE => T_ENDWHILE,
T_ENDSWITCH => T_ENDSWITCH,
+ T_ENDDECLARE => T_ENDDECLARE,
T_BREAK => T_BREAK,
T_END_HEREDOC => T_END_HEREDOC,
+ T_END_NOWDOC => T_END_NOWDOC,
];
/**
@@ -320,6 +336,7 @@ class PHP extends Tokenizer
T_DOUBLE_ARROW => 2,
T_DOUBLE_COLON => 2,
T_ECHO => 4,
+ T_ELLIPSIS => 3,
T_ELSE => 4,
T_ELSEIF => 6,
T_EMPTY => 5,
@@ -329,11 +346,14 @@ class PHP extends Tokenizer
T_ENDIF => 5,
T_ENDSWITCH => 9,
T_ENDWHILE => 8,
+ T_ENUM => 4,
+ T_ENUM_CASE => 4,
T_EVAL => 4,
T_EXTENDS => 7,
T_FILE => 8,
T_FINAL => 5,
T_FINALLY => 7,
+ T_FN => 2,
T_FOR => 3,
T_FOREACH => 7,
T_FUNCTION => 8,
@@ -361,6 +381,9 @@ class PHP extends Tokenizer
T_LOGICAL_AND => 3,
T_LOGICAL_OR => 2,
T_LOGICAL_XOR => 3,
+ T_MATCH => 5,
+ T_MATCH_ARROW => 2,
+ T_MATCH_DEFAULT => 7,
T_METHOD_C => 10,
T_MINUS_EQUAL => 2,
T_POW_EQUAL => 3,
@@ -370,6 +393,7 @@ class PHP extends Tokenizer
T_NS_C => 13,
T_NS_SEPARATOR => 1,
T_NEW => 3,
+ T_NULLSAFE_OBJECT_OPERATOR => 3,
T_OBJECT_OPERATOR => 2,
T_OPEN_TAG_WITH_ECHO => 3,
T_OR_EQUAL => 2,
@@ -378,6 +402,7 @@ class PHP extends Tokenizer
T_PRIVATE => 7,
T_PUBLIC => 6,
T_PROTECTED => 9,
+ T_READONLY => 8,
T_REQUIRE => 7,
T_REQUIRE_ONCE => 12,
T_RETURN => 6,
@@ -436,6 +461,33 @@ class PHP extends Tokenizer
T_BACKTICK => 1,
T_OPEN_SHORT_ARRAY => 1,
T_CLOSE_SHORT_ARRAY => 1,
+ T_TYPE_UNION => 1,
+ T_TYPE_INTERSECTION => 1,
+ ];
+
+ /**
+ * Contexts in which keywords should always be tokenized as T_STRING.
+ *
+ * @var array
+ */
+ protected $tstringContexts = [
+ T_OBJECT_OPERATOR => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
+ T_FUNCTION => true,
+ T_CLASS => true,
+ T_INTERFACE => true,
+ T_TRAIT => true,
+ T_ENUM => true,
+ T_ENUM_CASE => true,
+ T_EXTENDS => true,
+ T_IMPLEMENTS => true,
+ T_ATTRIBUTE => true,
+ T_NEW => true,
+ T_CONST => true,
+ T_NS_SEPARATOR => true,
+ T_USE => true,
+ T_NAMESPACE => true,
+ T_PAAMAYIM_NEKUDOTAYIM => true,
];
/**
@@ -462,7 +514,7 @@ protected function tokenize($string)
if (PHP_CODESNIFFER_VERBOSITY > 1) {
echo "\t*** START PHP TOKENIZING ***".PHP_EOL;
$isWin = false;
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ if (stripos(PHP_OS, 'WIN') === 0) {
$isWin = true;
}
}
@@ -549,6 +601,116 @@ protected function tokenize($string)
echo PHP_EOL;
}
+ /*
+ Tokenize context sensitive keyword as string when it should be string.
+ */
+
+ if ($tokenIsArray === true
+ && isset(Util\Tokens::$contextSensitiveKeywords[$token[0]]) === true
+ && (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true
+ || $finalTokens[$lastNotEmptyToken]['content'] === '&')
+ ) {
+ if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
+ $preserveKeyword = false;
+
+ // `new class`, and `new static` should be preserved.
+ if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW
+ && ($token[0] === T_CLASS
+ || $token[0] === T_STATIC)
+ ) {
+ $preserveKeyword = true;
+ }
+
+ // `new class extends` `new class implements` should be preserved
+ if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS)
+ && $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS
+ ) {
+ $preserveKeyword = true;
+ }
+
+ // `namespace\` should be preserved
+ if ($token[0] === T_NAMESPACE) {
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false) {
+ break;
+ }
+
+ if (isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true) {
+ continue;
+ }
+
+ if ($tokens[$i][0] === T_NS_SEPARATOR) {
+ $preserveKeyword = true;
+ }
+
+ break;
+ }
+ }
+ }//end if
+
+ if ($finalTokens[$lastNotEmptyToken]['content'] === '&') {
+ $preserveKeyword = true;
+
+ for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) {
+ if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) {
+ continue;
+ }
+
+ if ($finalTokens[$i]['code'] === T_FUNCTION) {
+ $preserveKeyword = false;
+ }
+
+ break;
+ }
+ }
+
+ if ($preserveKeyword === false) {
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $type = Util\Tokens::tokenName($token[0]);
+ echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL;
+ }
+
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_STRING,
+ 'type' => 'T_STRING',
+ 'content' => $token[1],
+ ];
+
+ $newStackPtr++;
+ continue;
+ }
+ }//end if
+
+ /*
+ Special case for `static` used as a function name, i.e. `static()`.
+ */
+
+ if ($tokenIsArray === true
+ && $token[0] === T_STATIC
+ && $finalTokens[$lastNotEmptyToken]['code'] !== T_NEW
+ ) {
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === true
+ && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true
+ ) {
+ continue;
+ }
+
+ if ($tokens[$i][0] === '(') {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_STRING,
+ 'type' => 'T_STRING',
+ 'content' => $token[1],
+ ];
+
+ $newStackPtr++;
+ continue 2;
+ }
+
+ break;
+ }
+ }//end if
+
/*
Parse doc blocks into something that can be easily iterated over.
*/
@@ -566,6 +728,107 @@ protected function tokenize($string)
continue;
}
+ /*
+ PHP 8 tokenizes a new line after a slash and hash comment to the next whitespace token.
+ */
+
+ if (PHP_VERSION_ID >= 80000
+ && $tokenIsArray === true
+ && ($token[0] === T_COMMENT && (strpos($token[1], '//') === 0 || strpos($token[1], '#') === 0))
+ && isset($tokens[($stackPtr + 1)]) === true
+ && is_array($tokens[($stackPtr + 1)]) === true
+ && $tokens[($stackPtr + 1)][0] === T_WHITESPACE
+ ) {
+ $nextToken = $tokens[($stackPtr + 1)];
+
+ // If the next token is a single new line, merge it into the comment token
+ // and set to it up to be skipped.
+ if ($nextToken[1] === "\n" || $nextToken[1] === "\r\n" || $nextToken[1] === "\n\r") {
+ $token[1] .= $nextToken[1];
+ $tokens[($stackPtr + 1)] = null;
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* merged newline after comment into comment token $stackPtr".PHP_EOL;
+ }
+ } else {
+ // This may be a whitespace token consisting of multiple new lines.
+ if (strpos($nextToken[1], "\r\n") === 0) {
+ $token[1] .= "\r\n";
+ $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 2);
+ } else if (strpos($nextToken[1], "\n\r") === 0) {
+ $token[1] .= "\n\r";
+ $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 2);
+ } else if (strpos($nextToken[1], "\n") === 0) {
+ $token[1] .= "\n";
+ $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 1);
+ }
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* stripped first newline after comment and added it to comment token $stackPtr".PHP_EOL;
+ }
+ }//end if
+ }//end if
+
+ /*
+ For Explicit Octal Notation prior to PHP 8.1 we need to combine the
+ T_LNUMBER and T_STRING token values into a single token value, and
+ then ignore the T_STRING token.
+ */
+
+ if (PHP_VERSION_ID < 80100
+ && $tokenIsArray === true && $token[1] === '0'
+ && (isset($tokens[($stackPtr + 1)]) === true
+ && is_array($tokens[($stackPtr + 1)]) === true
+ && $tokens[($stackPtr + 1)][0] === T_STRING
+ && isset($tokens[($stackPtr + 1)][1][0], $tokens[($stackPtr + 1)][1][1]) === true
+ && strtolower($tokens[($stackPtr + 1)][1][0]) === 'o'
+ && $tokens[($stackPtr + 1)][1][1] !== '_')
+ && preg_match('`^(o[0-7]+(?:_[0-7]+)?)([0-9_]*)$`i', $tokens[($stackPtr + 1)][1], $matches) === 1
+ ) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_LNUMBER,
+ 'type' => 'T_LNUMBER',
+ 'content' => $token[1] .= $matches[1],
+ ];
+ $newStackPtr++;
+
+ if (isset($matches[2]) === true && $matches[2] !== '') {
+ $type = 'T_LNUMBER';
+ if ($matches[2][0] === '_') {
+ $type = 'T_STRING';
+ }
+
+ $finalTokens[$newStackPtr] = [
+ 'code' => constant($type),
+ 'type' => $type,
+ 'content' => $matches[2],
+ ];
+ $newStackPtr++;
+ }
+
+ $stackPtr++;
+ continue;
+ }//end if
+
+ /*
+ PHP 8.1 introduced two dedicated tokens for the & character.
+ Retokenizing both of these to T_BITWISE_AND, which is the
+ token PHPCS already tokenized them as.
+ */
+
+ if ($tokenIsArray === true
+ && ($token[0] === T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
+ || $token[0] === T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG)
+ ) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_BITWISE_AND,
+ 'type' => 'T_BITWISE_AND',
+ 'content' => $token[1],
+ ];
+ $newStackPtr++;
+ continue;
+ }
+
/*
If this is a double quoted string, PHP will tokenize the whole
thing which causes problems with the scope map when braces are
@@ -592,7 +855,8 @@ protected function tokenize($string)
if ($subTokenIsArray === true) {
$tokenContent .= $subToken[1];
- if ($subToken[1] === '{'
+ if (($subToken[1] === '{'
+ || $subToken[1] === '${')
&& $subToken[0] !== T_ENCAPSED_AND_WHITESPACE
) {
$nestedVars[] = $i;
@@ -771,6 +1035,312 @@ protected function tokenize($string)
continue;
}//end if
+ /*
+ Enum keyword for PHP < 8.1
+ */
+
+ if ($tokenIsArray === true
+ && $token[0] === T_STRING
+ && strtolower($token[1]) === 'enum'
+ ) {
+ // Get the next non-empty token.
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
+ ) {
+ break;
+ }
+ }
+
+ if (isset($tokens[$i]) === true
+ && is_array($tokens[$i]) === true
+ && $tokens[$i][0] === T_STRING
+ ) {
+ // Modify $tokens directly so we can use it later when converting enum "case".
+ $tokens[$stackPtr][0] = T_ENUM;
+
+ $newToken = [];
+ $newToken['code'] = T_ENUM;
+ $newToken['type'] = 'T_ENUM';
+ $newToken['content'] = $token[1];
+ $finalTokens[$newStackPtr] = $newToken;
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $stackPtr changed from T_STRING to T_ENUM".PHP_EOL;
+ }
+
+ $newStackPtr++;
+ continue;
+ }
+ }//end if
+
+ /*
+ Convert enum "case" to T_ENUM_CASE
+ */
+
+ if ($tokenIsArray === true
+ && $token[0] === T_CASE
+ && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
+ ) {
+ $isEnumCase = false;
+ $scope = 1;
+
+ for ($i = ($stackPtr - 1); $i > 0; $i--) {
+ if ($tokens[$i] === '}') {
+ $scope++;
+ continue;
+ }
+
+ if ($tokens[$i] === '{') {
+ $scope--;
+ continue;
+ }
+
+ if (is_array($tokens[$i]) === false) {
+ continue;
+ }
+
+ if ($scope !== 0) {
+ continue;
+ }
+
+ if ($tokens[$i][0] === T_SWITCH) {
+ break;
+ }
+
+ if ($tokens[$i][0] === T_ENUM || $tokens[$i][0] === T_ENUM_CASE) {
+ $isEnumCase = true;
+ break;
+ }
+ }//end for
+
+ if ($isEnumCase === true) {
+ // Modify $tokens directly so we can use it as optimisation for other enum "case".
+ $tokens[$stackPtr][0] = T_ENUM_CASE;
+
+ $newToken = [];
+ $newToken['code'] = T_ENUM_CASE;
+ $newToken['type'] = 'T_ENUM_CASE';
+ $newToken['content'] = $token[1];
+ $finalTokens[$newStackPtr] = $newToken;
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $stackPtr changed from T_CASE to T_ENUM_CASE".PHP_EOL;
+ }
+
+ $newStackPtr++;
+ continue;
+ }
+ }//end if
+
+ /*
+ As of PHP 8.0 fully qualified, partially qualified and namespace relative
+ identifier names are tokenized differently.
+ This "undoes" the new tokenization so the tokenization will be the same in
+ in PHP 5, 7 and 8.
+ */
+
+ if (PHP_VERSION_ID >= 80000
+ && $tokenIsArray === true
+ && ($token[0] === T_NAME_QUALIFIED
+ || $token[0] === T_NAME_FULLY_QUALIFIED
+ || $token[0] === T_NAME_RELATIVE)
+ ) {
+ $name = $token[1];
+
+ if ($token[0] === T_NAME_FULLY_QUALIFIED) {
+ $newToken = [];
+ $newToken['code'] = T_NS_SEPARATOR;
+ $newToken['type'] = 'T_NS_SEPARATOR';
+ $newToken['content'] = '\\';
+ $finalTokens[$newStackPtr] = $newToken;
+ ++$newStackPtr;
+
+ $name = ltrim($name, '\\');
+ }
+
+ if ($token[0] === T_NAME_RELATIVE) {
+ $newToken = [];
+ $newToken['code'] = T_NAMESPACE;
+ $newToken['type'] = 'T_NAMESPACE';
+ $newToken['content'] = substr($name, 0, 9);
+ $finalTokens[$newStackPtr] = $newToken;
+ ++$newStackPtr;
+
+ $newToken = [];
+ $newToken['code'] = T_NS_SEPARATOR;
+ $newToken['type'] = 'T_NS_SEPARATOR';
+ $newToken['content'] = '\\';
+ $finalTokens[$newStackPtr] = $newToken;
+ ++$newStackPtr;
+
+ $name = substr($name, 10);
+ }
+
+ $parts = explode('\\', $name);
+ $partCount = count($parts);
+ $lastPart = ($partCount - 1);
+
+ foreach ($parts as $i => $part) {
+ $newToken = [];
+ $newToken['code'] = T_STRING;
+ $newToken['type'] = 'T_STRING';
+ $newToken['content'] = $part;
+ $finalTokens[$newStackPtr] = $newToken;
+ ++$newStackPtr;
+
+ if ($i !== $lastPart) {
+ $newToken = [];
+ $newToken['code'] = T_NS_SEPARATOR;
+ $newToken['type'] = 'T_NS_SEPARATOR';
+ $newToken['content'] = '\\';
+ $finalTokens[$newStackPtr] = $newToken;
+ ++$newStackPtr;
+ }
+ }
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $type = Util\Tokens::tokenName($token[0]);
+ $content = Util\Common::prepareForOutput($token[1]);
+ echo "\t\t* token $stackPtr split into individual tokens; was: $type => $content".PHP_EOL;
+ }
+
+ continue;
+ }//end if
+
+ /*
+ PHP 8.0 Attributes
+ */
+
+ if (PHP_VERSION_ID < 80000
+ && $token[0] === T_COMMENT
+ && strpos($token[1], '#[') === 0
+ ) {
+ $subTokens = $this->parsePhpAttribute($tokens, $stackPtr);
+ if ($subTokens !== null) {
+ array_splice($tokens, $stackPtr, 1, $subTokens);
+ $numTokens = count($tokens);
+
+ $tokenIsArray = true;
+ $token = $tokens[$stackPtr];
+ } else {
+ $token[0] = T_ATTRIBUTE;
+ }
+ }
+
+ if ($tokenIsArray === true
+ && $token[0] === T_ATTRIBUTE
+ ) {
+ // Go looking for the close bracket.
+ $bracketCloser = $this->findCloser($tokens, ($stackPtr + 1), ['[', '#['], ']');
+
+ $newToken = [];
+ $newToken['code'] = T_ATTRIBUTE;
+ $newToken['type'] = 'T_ATTRIBUTE';
+ $newToken['content'] = '#[';
+ $finalTokens[$newStackPtr] = $newToken;
+
+ $tokens[$bracketCloser] = [];
+ $tokens[$bracketCloser][0] = T_ATTRIBUTE_END;
+ $tokens[$bracketCloser][1] = ']';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $bracketCloser changed from T_CLOSE_SQUARE_BRACKET to T_ATTRIBUTE_END".PHP_EOL;
+ }
+
+ $newStackPtr++;
+ continue;
+ }//end if
+
+ /*
+ Tokenize the parameter labels for PHP 8.0 named parameters as a special T_PARAM_NAME
+ token and ensures that the colon after it is always T_COLON.
+ */
+
+ if ($tokenIsArray === true
+ && ($token[0] === T_STRING
+ || preg_match('`^[a-zA-Z_\x80-\xff]`', $token[1]) === 1)
+ ) {
+ // Get the next non-empty token.
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
+ ) {
+ break;
+ }
+ }
+
+ if (isset($tokens[$i]) === true
+ && is_array($tokens[$i]) === false
+ && $tokens[$i] === ':'
+ ) {
+ // Get the previous non-empty token.
+ for ($j = ($stackPtr - 1); $j > 0; $j--) {
+ if (is_array($tokens[$j]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$j][0]]) === false
+ ) {
+ break;
+ }
+ }
+
+ if (is_array($tokens[$j]) === false
+ && ($tokens[$j] === '('
+ || $tokens[$j] === ',')
+ ) {
+ $newToken = [];
+ $newToken['code'] = T_PARAM_NAME;
+ $newToken['type'] = 'T_PARAM_NAME';
+ $newToken['content'] = $token[1];
+ $finalTokens[$newStackPtr] = $newToken;
+
+ $newStackPtr++;
+
+ // Modify the original token stack so that future checks, like
+ // determining T_COLON vs T_INLINE_ELSE can handle this correctly.
+ $tokens[$stackPtr][0] = T_PARAM_NAME;
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $type = Util\Tokens::tokenName($token[0]);
+ echo "\t\t* token $stackPtr changed from $type to T_PARAM_NAME".PHP_EOL;
+ }
+
+ continue;
+ }
+ }//end if
+ }//end if
+
+ /*
+ "readonly" keyword for PHP < 8.1
+ */
+
+ if (PHP_VERSION_ID < 80100
+ && $tokenIsArray === true
+ && strtolower($token[1]) === 'readonly'
+ && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
+ ) {
+ // Get the next non-whitespace token.
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false
+ || $tokens[$i][0] !== T_WHITESPACE
+ ) {
+ break;
+ }
+ }
+
+ if (isset($tokens[$i]) === false
+ || $tokens[$i] !== '('
+ ) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_READONLY,
+ 'type' => 'T_READONLY',
+ 'content' => $token[1],
+ ];
+ $newStackPtr++;
+
+ continue;
+ }
+ }//end if
+
/*
Before PHP 7.0, the "yield from" was tokenized as
T_YIELD, T_WHITESPACE and T_STRING. So look for
@@ -787,7 +1357,7 @@ protected function tokenize($string)
&& $tokens[($stackPtr + 2)][0] === T_STRING
&& strtolower($tokens[($stackPtr + 2)][1]) === 'from'
) {
- // Could be multi-line, so just the token stack.
+ // Could be multi-line, so adjust the token stack.
$token[0] = T_YIELD_FROM;
$token[1] .= $tokens[($stackPtr + 1)][1].$tokens[($stackPtr + 2)][1];
@@ -814,6 +1384,7 @@ protected function tokenize($string)
&& $tokenIsArray === true
&& $token[0] === T_STRING
&& strtolower($token[1]) === 'yield'
+ && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
) {
if (isset($tokens[($stackPtr + 1)]) === true
&& isset($tokens[($stackPtr + 2)]) === true
@@ -919,7 +1490,7 @@ protected function tokenize($string)
/*
Before PHP 7, the ??= operator was tokenized as
T_INLINE_THEN, T_INLINE_THEN, T_EQUAL.
- Between PHP 7.0 and 7.2, the ??= operator was tokenized as
+ Between PHP 7.0 and 7.3, the ??= operator was tokenized as
T_COALESCE, T_EQUAL.
So look for and combine these tokens in earlier versions.
*/
@@ -974,6 +1545,29 @@ protected function tokenize($string)
continue;
}
+ /*
+ Before PHP 8, the ?-> operator was tokenized as
+ T_INLINE_THEN followed by T_OBJECT_OPERATOR.
+ So look for and combine these tokens in earlier versions.
+ */
+
+ if ($tokenIsArray === false
+ && $token[0] === '?'
+ && isset($tokens[($stackPtr + 1)]) === true
+ && is_array($tokens[($stackPtr + 1)]) === true
+ && $tokens[($stackPtr + 1)][0] === T_OBJECT_OPERATOR
+ ) {
+ $newToken = [];
+ $newToken['code'] = T_NULLSAFE_OBJECT_OPERATOR;
+ $newToken['type'] = 'T_NULLSAFE_OBJECT_OPERATOR';
+ $newToken['content'] = '?->';
+ $finalTokens[$newStackPtr] = $newToken;
+
+ $newStackPtr++;
+ $stackPtr++;
+ continue;
+ }
+
/*
Before PHP 7.4, underscores inside T_LNUMBER and T_DNUMBER
tokens split the token with a T_STRING. So look for
@@ -1032,6 +1626,7 @@ protected function tokenize($string)
if ($newType === T_LNUMBER
&& ((stripos($newContent, '0x') === 0 && hexdec(str_replace('_', '', $newContent)) > PHP_INT_MAX)
|| (stripos($newContent, '0b') === 0 && bindec(str_replace('_', '', $newContent)) > PHP_INT_MAX)
+ || (stripos($newContent, '0o') === 0 && octdec(str_replace('_', '', $newContent)) > PHP_INT_MAX)
|| (stripos($newContent, '0x') !== 0
&& stripos($newContent, 'e') !== false || strpos($newContent, '.') !== false)
|| (strpos($newContent, '0') === 0 && stripos($newContent, '0x') !== 0
@@ -1052,6 +1647,121 @@ protected function tokenize($string)
continue;
}//end if
+ /*
+ Backfill the T_MATCH token for PHP versions < 8.0 and
+ do initial correction for non-match expression T_MATCH tokens
+ to T_STRING for PHP >= 8.0.
+ A final check for non-match expression T_MATCH tokens is done
+ in PHP::processAdditional().
+ */
+
+ if ($tokenIsArray === true
+ && (($token[0] === T_STRING
+ && strtolower($token[1]) === 'match')
+ || $token[0] === T_MATCH)
+ ) {
+ $isMatch = false;
+ for ($x = ($stackPtr + 1); $x < $numTokens; $x++) {
+ if (isset($tokens[$x][0], Util\Tokens::$emptyTokens[$tokens[$x][0]]) === true) {
+ continue;
+ }
+
+ if ($tokens[$x] !== '(') {
+ // This is not a match expression.
+ break;
+ }
+
+ if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
+ // Also not a match expression.
+ break;
+ }
+
+ $isMatch = true;
+ break;
+ }//end for
+
+ if ($isMatch === true && $token[0] === T_STRING) {
+ $newToken = [];
+ $newToken['code'] = T_MATCH;
+ $newToken['type'] = 'T_MATCH';
+ $newToken['content'] = $token[1];
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $stackPtr changed from T_STRING to T_MATCH".PHP_EOL;
+ }
+
+ $finalTokens[$newStackPtr] = $newToken;
+ $newStackPtr++;
+ continue;
+ } else if ($isMatch === false && $token[0] === T_MATCH) {
+ // PHP 8.0, match keyword, but not a match expression.
+ $newToken = [];
+ $newToken['code'] = T_STRING;
+ $newToken['type'] = 'T_STRING';
+ $newToken['content'] = $token[1];
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $stackPtr changed from T_MATCH to T_STRING".PHP_EOL;
+ }
+
+ $finalTokens[$newStackPtr] = $newToken;
+ $newStackPtr++;
+ continue;
+ }//end if
+ }//end if
+
+ /*
+ Retokenize the T_DEFAULT in match control structures as T_MATCH_DEFAULT
+ to prevent scope being set and the scope for switch default statements
+ breaking.
+ */
+
+ if ($tokenIsArray === true
+ && $token[0] === T_DEFAULT
+ && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false
+ ) {
+ for ($x = ($stackPtr + 1); $x < $numTokens; $x++) {
+ if ($tokens[$x] === ',') {
+ // Skip over potential trailing comma (supported in PHP).
+ continue;
+ }
+
+ if (is_array($tokens[$x]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false
+ ) {
+ // Non-empty, non-comma content.
+ break;
+ }
+ }
+
+ if (isset($tokens[$x]) === true
+ && is_array($tokens[$x]) === true
+ && $tokens[$x][0] === T_DOUBLE_ARROW
+ ) {
+ // Modify the original token stack for the double arrow so that
+ // future checks can disregard the double arrow token more easily.
+ // For match expression "case" statements, this is handled
+ // in PHP::processAdditional().
+ $tokens[$x][0] = T_MATCH_ARROW;
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL;
+ }
+
+ $newToken = [];
+ $newToken['code'] = T_MATCH_DEFAULT;
+ $newToken['type'] = 'T_MATCH_DEFAULT';
+ $newToken['content'] = $token[1];
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $stackPtr changed from T_DEFAULT to T_MATCH_DEFAULT".PHP_EOL;
+ }
+
+ $finalTokens[$newStackPtr] = $newToken;
+ $newStackPtr++;
+ continue;
+ }//end if
+ }//end if
+
/*
Convert ? to T_NULLABLE OR T_INLINE_THEN
*/
@@ -1064,7 +1774,7 @@ protected function tokenize($string)
* Check if the next non-empty token is one of the tokens which can be used
* in type declarations. If not, it's definitely a ternary.
* At this point, the only token types which need to be taken into consideration
- * as potential type declarations are T_STRING, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR.
+ * as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR.
*/
$lastRelevantNonEmpty = null;
@@ -1081,7 +1791,11 @@ protected function tokenize($string)
}
if ($tokenType === T_STRING
+ || $tokenType === T_NAME_FULLY_QUALIFIED
+ || $tokenType === T_NAME_RELATIVE
+ || $tokenType === T_NAME_QUALIFIED
|| $tokenType === T_ARRAY
+ || $tokenType === T_NAMESPACE
|| $tokenType === T_NS_SEPARATOR
) {
$lastRelevantNonEmpty = $tokenType;
@@ -1092,7 +1806,10 @@ protected function tokenize($string)
&& isset($lastRelevantNonEmpty) === false)
|| ($lastRelevantNonEmpty === T_ARRAY
&& $tokenType === '(')
- || ($lastRelevantNonEmpty === T_STRING
+ || (($lastRelevantNonEmpty === T_STRING
+ || $lastRelevantNonEmpty === T_NAME_FULLY_QUALIFIED
+ || $lastRelevantNonEmpty === T_NAME_RELATIVE
+ || $lastRelevantNonEmpty === T_NAME_QUALIFIED)
&& ($tokenType === T_DOUBLE_COLON
|| $tokenType === '('
|| $tokenType === ':'))
@@ -1141,7 +1858,7 @@ protected function tokenize($string)
&& isset(Util\Tokens::$emptyTokens[$tokenType]) === false
) {
// Found the previous non-empty token.
- if ($tokenType === ':' || $tokenType === ',') {
+ if ($tokenType === ':' || $tokenType === ',' || $tokenType === T_ATTRIBUTE_END) {
$newToken['code'] = T_NULLABLE;
$newToken['type'] = 'T_NULLABLE';
@@ -1233,10 +1950,9 @@ protected function tokenize($string)
}
/*
- The string-like token after a function keyword should always be
- tokenized as T_STRING even if it appears to be a different token,
- such as when writing code like: function default(): foo
- so go forward and change the token type before it is processed.
+ This is a special condition for T_ARRAY tokens used for
+ function return types. We want to keep the parenthesis map clean,
+ so let's tag these tokens as T_STRING.
*/
if ($tokenIsArray === true
@@ -1244,32 +1960,6 @@ protected function tokenize($string)
|| $token[0] === T_FN)
&& $finalTokens[$lastNotEmptyToken]['code'] !== T_USE
) {
- if ($token[0] === T_FUNCTION) {
- for ($x = ($stackPtr + 1); $x < $numTokens; $x++) {
- if (is_array($tokens[$x]) === false
- || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false
- ) {
- // Non-empty content.
- break;
- }
- }
-
- if ($x < $numTokens && is_array($tokens[$x]) === true) {
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- $oldType = Util\Tokens::tokenName($tokens[$x][0]);
- echo "\t\t* token $x changed from $oldType to T_STRING".PHP_EOL;
- }
-
- $tokens[$x][0] = T_STRING;
- }
- }//end if
-
- /*
- This is a special condition for T_ARRAY tokens used for
- function return types. We want to keep the parenthesis map clean,
- so let's tag these tokens as T_STRING.
- */
-
// Go looking for the colon to start the return type hint.
// Start by finding the closing parenthesis of the function.
$parenthesisStack = [];
@@ -1309,17 +1999,6 @@ function return types. We want to keep the parenthesis map clean,
&& is_array($tokens[$x]) === false
&& $tokens[$x] === ':'
) {
- $allowed = [
- T_STRING => T_STRING,
- T_ARRAY => T_ARRAY,
- T_CALLABLE => T_CALLABLE,
- T_SELF => T_SELF,
- T_PARENT => T_PARENT,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
- ];
-
- $allowed += Util\Tokens::$emptyTokens;
-
// Find the start of the return type.
for ($x += 1; $x < $numTokens; $x++) {
if (is_array($tokens[$x]) === true
@@ -1331,7 +2010,7 @@ function return types. We want to keep the parenthesis map clean,
if (is_array($tokens[$x]) === false && $tokens[$x] === '?') {
// Found a nullable operator, so skip it.
- // But also covert the token to save the tokenizer
+ // But also convert the token to save the tokenizer
// a bit of time later on.
$tokens[$x] = [
T_NULLABLE,
@@ -1347,21 +2026,6 @@ function return types. We want to keep the parenthesis map clean,
break;
}//end for
-
- // Any T_ARRAY tokens we find between here and the next
- // token that can't be part of the return type need to be
- // converted to T_STRING tokens.
- for ($x; $x < $numTokens; $x++) {
- if (is_array($tokens[$x]) === false || isset($allowed[$tokens[$x][0]]) === false) {
- break;
- } else if ($tokens[$x][0] === T_ARRAY) {
- $tokens[$x][0] = T_STRING;
-
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- echo "\t\t* token $x changed from T_ARRAY to T_STRING".PHP_EOL;
- }
- }
- }
}//end if
}//end if
}//end if
@@ -1400,13 +2064,16 @@ function return types. We want to keep the parenthesis map clean,
&& $token[0] === T_STRING
&& isset($tokens[($stackPtr + 1)]) === true
&& $tokens[($stackPtr + 1)] === ':'
- && $tokens[($stackPtr - 1)][0] !== T_PAAMAYIM_NEKUDOTAYIM
+ && (is_array($tokens[($stackPtr - 1)]) === false
+ || $tokens[($stackPtr - 1)][0] !== T_PAAMAYIM_NEKUDOTAYIM)
) {
$stopTokens = [
T_CASE => true,
T_SEMICOLON => true,
+ T_OPEN_TAG => true,
T_OPEN_CURLY_BRACKET => true,
T_INLINE_THEN => true,
+ T_ENUM => true,
];
for ($x = ($newStackPtr - 1); $x > 0; $x--) {
@@ -1417,6 +2084,7 @@ function return types. We want to keep the parenthesis map clean,
if ($finalTokens[$x]['code'] !== T_CASE
&& $finalTokens[$x]['code'] !== T_INLINE_THEN
+ && $finalTokens[$x]['code'] !== T_ENUM
) {
$finalTokens[$newStackPtr] = [
'content' => $token[1].':',
@@ -1465,45 +2133,61 @@ function return types. We want to keep the parenthesis map clean,
$newStackPtr++;
}
} else {
+ // Some T_STRING tokens should remain that way due to their context.
if ($tokenIsArray === true && $token[0] === T_STRING) {
- // Some T_STRING tokens should remain that way
- // due to their context.
- $context = [
- T_OBJECT_OPERATOR => true,
- T_FUNCTION => true,
- T_CLASS => true,
- T_EXTENDS => true,
- T_IMPLEMENTS => true,
- T_NEW => true,
- T_CONST => true,
- T_NS_SEPARATOR => true,
- T_USE => true,
- T_NAMESPACE => true,
- T_PAAMAYIM_NEKUDOTAYIM => true,
- ];
+ $preserveTstring = false;
+
+ if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
+ $preserveTstring = true;
- if (isset($context[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
- // Special case for syntax like: return new self
- // where self should not be a string.
+ // Special case for syntax like: return new self/new parent
+ // where self/parent should not be a string.
+ $tokenContentLower = strtolower($token[1]);
if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW
- && strtolower($token[1]) === 'self'
+ && ($tokenContentLower === 'self' || $tokenContentLower === 'parent')
) {
- $finalTokens[$newStackPtr] = [
- 'content' => $token[1],
- 'code' => T_SELF,
- 'type' => 'T_SELF',
- ];
- } else {
- $finalTokens[$newStackPtr] = [
- 'content' => $token[1],
- 'code' => T_STRING,
- 'type' => 'T_STRING',
- ];
+ $preserveTstring = false;
+ }
+ } else if ($finalTokens[$lastNotEmptyToken]['content'] === '&') {
+ // Function names for functions declared to return by reference.
+ for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) {
+ if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) {
+ continue;
+ }
+
+ if ($finalTokens[$i]['code'] === T_FUNCTION) {
+ $preserveTstring = true;
+ }
+
+ break;
+ }
+ } else {
+ // Keywords with special PHPCS token when used as a function call.
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === true
+ && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true
+ ) {
+ continue;
+ }
+
+ if ($tokens[$i][0] === '(') {
+ $preserveTstring = true;
+ }
+
+ break;
}
+ }//end if
+
+ if ($preserveTstring === true) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_STRING,
+ 'type' => 'T_STRING',
+ 'content' => $token[1],
+ ];
$newStackPtr++;
continue;
- }//end if
+ }
}//end if
$newToken = null;
@@ -1532,74 +2216,100 @@ function return types. We want to keep the parenthesis map clean,
// Convert colons that are actually the ELSE component of an
// inline IF statement.
if (empty($insideInlineIf) === false && $newToken['code'] === T_COLON) {
- // Make sure this isn't the return type separator of a closure.
$isInlineIf = true;
+
+ // Make sure this isn't a named parameter label.
+ // Get the previous non-empty token.
for ($i = ($stackPtr - 1); $i > 0; $i--) {
if (is_array($tokens[$i]) === false
- || ($tokens[$i][0] !== T_DOC_COMMENT
- && $tokens[$i][0] !== T_COMMENT
- && $tokens[$i][0] !== T_WHITESPACE)
+ || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
) {
break;
}
}
- if ($tokens[$i] === ')') {
- $parenCount = 1;
- for ($i--; $i > 0; $i--) {
- if ($tokens[$i] === '(') {
- $parenCount--;
- if ($parenCount === 0) {
+ if ($tokens[$i][0] === T_PARAM_NAME) {
+ $isInlineIf = false;
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token is parameter label, not T_INLINE_ELSE".PHP_EOL;
+ }
+ }
+
+ if ($isInlineIf === true) {
+ // Make sure this isn't a return type separator.
+ for ($i = ($stackPtr - 1); $i > 0; $i--) {
+ if (is_array($tokens[$i]) === false
+ || ($tokens[$i][0] !== T_DOC_COMMENT
+ && $tokens[$i][0] !== T_COMMENT
+ && $tokens[$i][0] !== T_WHITESPACE)
+ ) {
+ break;
+ }
+ }
+
+ if ($tokens[$i] === ')') {
+ $parenCount = 1;
+ for ($i--; $i > 0; $i--) {
+ if ($tokens[$i] === '(') {
+ $parenCount--;
+ if ($parenCount === 0) {
+ break;
+ }
+ } else if ($tokens[$i] === ')') {
+ $parenCount++;
+ }
+ }
+
+ // We've found the open parenthesis, so if the previous
+ // non-empty token is FUNCTION or USE, this is a return type.
+ // Note that we need to skip T_STRING tokens here as these
+ // can be function names.
+ for ($i--; $i > 0; $i--) {
+ if (is_array($tokens[$i]) === false
+ || ($tokens[$i][0] !== T_DOC_COMMENT
+ && $tokens[$i][0] !== T_COMMENT
+ && $tokens[$i][0] !== T_WHITESPACE
+ && $tokens[$i][0] !== T_STRING)
+ ) {
break;
}
- } else if ($tokens[$i] === ')') {
- $parenCount++;
}
- }
- // We've found the open parenthesis, so if the previous
- // non-empty token is FUNCTION or USE, this is a closure.
- for ($i--; $i > 0; $i--) {
+ if ($tokens[$i][0] === T_FUNCTION || $tokens[$i][0] === T_FN || $tokens[$i][0] === T_USE) {
+ $isInlineIf = false;
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token is return type, not T_INLINE_ELSE".PHP_EOL;
+ }
+ }
+ }//end if
+ }//end if
+
+ // Check to see if this is a CASE or DEFAULT opener.
+ if ($isInlineIf === true) {
+ $inlineIfToken = $insideInlineIf[(count($insideInlineIf) - 1)];
+ for ($i = $stackPtr; $i > $inlineIfToken; $i--) {
+ if (is_array($tokens[$i]) === true
+ && ($tokens[$i][0] === T_CASE
+ || $tokens[$i][0] === T_DEFAULT)
+ ) {
+ $isInlineIf = false;
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token is T_CASE or T_DEFAULT opener, not T_INLINE_ELSE".PHP_EOL;
+ }
+
+ break;
+ }
+
if (is_array($tokens[$i]) === false
- || ($tokens[$i][0] !== T_DOC_COMMENT
- && $tokens[$i][0] !== T_COMMENT
- && $tokens[$i][0] !== T_WHITESPACE)
+ && ($tokens[$i] === ';'
+ || $tokens[$i] === '{'
+ || $tokens[$i] === '}')
) {
break;
}
- }
-
- if ($tokens[$i][0] === T_FUNCTION || $tokens[$i][0] === T_FN || $tokens[$i][0] === T_USE) {
- $isInlineIf = false;
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- echo "\t\t* token is function return type, not T_INLINE_ELSE".PHP_EOL;
- }
- }
+ }//end for
}//end if
- // Check to see if this is a CASE or DEFAULT opener.
- $inlineIfToken = $insideInlineIf[(count($insideInlineIf) - 1)];
- for ($i = $stackPtr; $i > $inlineIfToken; $i--) {
- if (is_array($tokens[$i]) === true
- && ($tokens[$i][0] === T_CASE
- || $tokens[$i][0] === T_DEFAULT)
- ) {
- $isInlineIf = false;
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- echo "\t\t* token is T_CASE or T_DEFAULT opener, not T_INLINE_ELSE".PHP_EOL;
- }
-
- break;
- }
-
- if (is_array($tokens[$i]) === false
- && ($tokens[$i] === ';'
- || $tokens[$i] === '{')
- ) {
- break;
- }
- }
-
if ($isInlineIf === true) {
array_pop($insideInlineIf);
$newToken['code'] = T_INLINE_ELSE;
@@ -1611,41 +2321,37 @@ function return types. We want to keep the parenthesis map clean,
}
}//end if
- // This is a special condition for T_ARRAY tokens used for
- // type hinting function arguments as being arrays. We want to keep
- // the parenthesis map clean, so let's tag these tokens as
+ // This is a special condition for T_ARRAY tokens used for anything else
+ // but array declarations, like type hinting function arguments as
+ // being arrays.
+ // We want to keep the parenthesis map clean, so let's tag these tokens as
// T_STRING.
if ($newToken['code'] === T_ARRAY) {
- for ($i = $stackPtr; $i < $numTokens; $i++) {
- if ($tokens[$i] === '(') {
- break;
- } else if ($tokens[$i][0] === T_VARIABLE) {
- $newToken['code'] = T_STRING;
- $newToken['type'] = 'T_STRING';
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
+ ) {
+ // Non-empty content.
break;
}
}
+
+ if ($i !== $numTokens && $tokens[$i] !== '(') {
+ $newToken['code'] = T_STRING;
+ $newToken['type'] = 'T_STRING';
+ }
}
// This is a special case when checking PHP 5.5+ code in PHP < 5.5
// where "finally" should be T_FINALLY instead of T_STRING.
if ($newToken['code'] === T_STRING
&& strtolower($newToken['content']) === 'finally'
+ && $finalTokens[$lastNotEmptyToken]['code'] === T_CLOSE_CURLY_BRACKET
) {
$newToken['code'] = T_FINALLY;
$newToken['type'] = 'T_FINALLY';
}
- // This is a special case for the PHP 5.5 classname::class syntax
- // where "class" should be T_STRING instead of T_CLASS.
- if (($newToken['code'] === T_CLASS
- || $newToken['code'] === T_FUNCTION)
- && $finalTokens[$lastNotEmptyToken]['code'] === T_DOUBLE_COLON
- ) {
- $newToken['code'] = T_STRING;
- $newToken['type'] = 'T_STRING';
- }
-
// This is a special case for PHP 5.6 use function and use const
// where "function" and "const" should be T_STRING instead of T_FUNCTION
// and T_CONST.
@@ -1706,6 +2412,8 @@ protected function processAdditional()
echo "\t*** START ADDITIONAL PHP PROCESSING ***".PHP_EOL;
}
+ $this->createAttributesNestingMap();
+
$numTokens = count($this->tokens);
for ($i = ($numTokens - 1); $i >= 0; $i--) {
// Check for any unset scope conditions due to alternate IF/ENDIF syntax.
@@ -1821,14 +2529,19 @@ protected function processAdditional()
if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) {
$ignore = Util\Tokens::$emptyTokens;
$ignore += [
- T_STRING => T_STRING,
- T_ARRAY => T_ARRAY,
- T_COLON => T_COLON,
- T_NS_SEPARATOR => T_NS_SEPARATOR,
- T_NULLABLE => T_NULLABLE,
- T_CALLABLE => T_CALLABLE,
- T_PARENT => T_PARENT,
- T_SELF => T_SELF,
+ T_ARRAY => T_ARRAY,
+ T_CALLABLE => T_CALLABLE,
+ T_COLON => T_COLON,
+ T_NAMESPACE => T_NAMESPACE,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_NULL => T_NULL,
+ T_NULLABLE => T_NULLABLE,
+ T_PARENT => T_PARENT,
+ T_SELF => T_SELF,
+ T_STATIC => T_STATIC,
+ T_STRING => T_STRING,
+ T_TYPE_UNION => T_TYPE_UNION,
+ T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
];
$closer = $this->tokens[$x]['parenthesis_closer'];
@@ -1855,17 +2568,59 @@ protected function processAdditional()
$lastEndToken = null;
for ($scopeCloser = ($arrow + 1); $scopeCloser < $numTokens; $scopeCloser++) {
+ // Arrow function closer should never be shared with the closer of a match
+ // control structure.
+ if (isset($this->tokens[$scopeCloser]['scope_closer'], $this->tokens[$scopeCloser]['scope_condition']) === true
+ && $scopeCloser === $this->tokens[$scopeCloser]['scope_closer']
+ && $this->tokens[$this->tokens[$scopeCloser]['scope_condition']]['code'] === T_MATCH
+ ) {
+ if ($arrow < $this->tokens[$scopeCloser]['scope_condition']) {
+ // Match in return value of arrow function. Move on to the next token.
+ continue;
+ }
+
+ // Arrow function as return value for the last match case without trailing comma.
+ if ($lastEndToken !== null) {
+ $scopeCloser = $lastEndToken;
+ break;
+ }
+
+ for ($lastNonEmpty = ($scopeCloser - 1); $lastNonEmpty > $arrow; $lastNonEmpty--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$lastNonEmpty]['code']]) === false) {
+ $scopeCloser = $lastNonEmpty;
+ break 2;
+ }
+ }
+ }
+
if (isset($endTokens[$this->tokens[$scopeCloser]['code']]) === true) {
if ($lastEndToken !== null
- && $this->tokens[$scopeCloser]['code'] === T_CLOSE_PARENTHESIS
- && $this->tokens[$scopeCloser]['parenthesis_opener'] < $arrow
+ && ((isset($this->tokens[$scopeCloser]['parenthesis_opener']) === true
+ && $this->tokens[$scopeCloser]['parenthesis_opener'] < $arrow)
+ || (isset($this->tokens[$scopeCloser]['bracket_opener']) === true
+ && $this->tokens[$scopeCloser]['bracket_opener'] < $arrow))
) {
- $scopeCloser = $lastEndToken;
+ for ($lastNonEmpty = ($scopeCloser - 1); $lastNonEmpty > $arrow; $lastNonEmpty--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$lastNonEmpty]['code']]) === false) {
+ $scopeCloser = $lastNonEmpty;
+ break;
+ }
+ }
}
break;
}
+ if ($inTernary === false
+ && isset($this->tokens[$scopeCloser]['scope_closer'], $this->tokens[$scopeCloser]['scope_condition']) === true
+ && $scopeCloser === $this->tokens[$scopeCloser]['scope_closer']
+ && $this->tokens[$this->tokens[$scopeCloser]['scope_condition']]['code'] === T_FN
+ ) {
+ // Found a nested arrow function that already has the closer set and is in
+ // the same scope as us, so we can use its closer.
+ break;
+ }
+
if (isset($this->tokens[$scopeCloser]['scope_closer']) === true
&& $this->tokens[$scopeCloser]['code'] !== T_INLINE_ELSE
&& $this->tokens[$scopeCloser]['code'] !== T_END_HEREDOC
@@ -1970,9 +2725,12 @@ protected function processAdditional()
T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS,
T_VARIABLE => T_VARIABLE,
T_OBJECT_OPERATOR => T_OBJECT_OPERATOR,
+ T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR,
T_STRING => T_STRING,
T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING,
+ T_DOUBLE_QUOTED_STRING => T_DOUBLE_QUOTED_STRING,
];
+ $allowed += Util\Tokens::$magicConstants;
for ($x = ($i - 1); $x >= 0; $x--) {
// If we hit a scope opener, the statement has ended
@@ -1984,13 +2742,18 @@ protected function processAdditional()
}
if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) {
- if (isset($allowed[$this->tokens[$x]['code']]) === false) {
+ // Allow for control structures without braces.
+ if (($this->tokens[$x]['code'] === T_CLOSE_PARENTHESIS
+ && isset($this->tokens[$x]['parenthesis_owner']) === true
+ && isset(Util\Tokens::$scopeOpeners[$this->tokens[$this->tokens[$x]['parenthesis_owner']]['code']]) === true)
+ || isset($allowed[$this->tokens[$x]['code']]) === false
+ ) {
$isShortArray = true;
}
break;
}
- }
+ }//end for
if ($isShortArray === true) {
$this->tokens[$i]['code'] = T_OPEN_SHORT_ARRAY;
@@ -2007,6 +2770,262 @@ protected function processAdditional()
}
}
+ continue;
+ } else if ($this->tokens[$i]['code'] === T_MATCH) {
+ if (isset($this->tokens[$i]['scope_opener'], $this->tokens[$i]['scope_closer']) === false) {
+ // Not a match expression after all.
+ $this->tokens[$i]['code'] = T_STRING;
+ $this->tokens[$i]['type'] = 'T_STRING';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $i changed from T_MATCH to T_STRING".PHP_EOL;
+ }
+
+ if (isset($this->tokens[$i]['parenthesis_opener'], $this->tokens[$i]['parenthesis_closer']) === true) {
+ $opener = $this->tokens[$i]['parenthesis_opener'];
+ $closer = $this->tokens[$i]['parenthesis_closer'];
+ unset(
+ $this->tokens[$opener]['parenthesis_owner'],
+ $this->tokens[$closer]['parenthesis_owner']
+ );
+ unset(
+ $this->tokens[$i]['parenthesis_opener'],
+ $this->tokens[$i]['parenthesis_closer'],
+ $this->tokens[$i]['parenthesis_owner']
+ );
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* cleaned parenthesis of token $i *".PHP_EOL;
+ }
+ }
+ } else {
+ // Retokenize the double arrows for match expression cases to `T_MATCH_ARROW`.
+ $searchFor = [
+ T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET,
+ T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET,
+ T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS,
+ T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY,
+ T_DOUBLE_ARROW => T_DOUBLE_ARROW,
+ ];
+ $searchFor += Util\Tokens::$scopeOpeners;
+
+ for ($x = ($this->tokens[$i]['scope_opener'] + 1); $x < $this->tokens[$i]['scope_closer']; $x++) {
+ if (isset($searchFor[$this->tokens[$x]['code']]) === false) {
+ continue;
+ }
+
+ if (isset($this->tokens[$x]['scope_closer']) === true) {
+ $x = $this->tokens[$x]['scope_closer'];
+ continue;
+ }
+
+ if (isset($this->tokens[$x]['parenthesis_closer']) === true) {
+ $x = $this->tokens[$x]['parenthesis_closer'];
+ continue;
+ }
+
+ if (isset($this->tokens[$x]['bracket_closer']) === true) {
+ $x = $this->tokens[$x]['bracket_closer'];
+ continue;
+ }
+
+ // This must be a double arrow, but make sure anyhow.
+ if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) {
+ $this->tokens[$x]['code'] = T_MATCH_ARROW;
+ $this->tokens[$x]['type'] = 'T_MATCH_ARROW';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL;
+ }
+ }
+ }//end for
+ }//end if
+
+ continue;
+ } else if ($this->tokens[$i]['code'] === T_BITWISE_OR
+ || $this->tokens[$i]['code'] === T_BITWISE_AND
+ ) {
+ /*
+ Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR.
+ Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND.
+ */
+
+ $allowed = [
+ T_STRING => T_STRING,
+ T_CALLABLE => T_CALLABLE,
+ T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
+ T_STATIC => T_STATIC,
+ T_FALSE => T_FALSE,
+ T_NULL => T_NULL,
+ T_NAMESPACE => T_NAMESPACE,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ ];
+
+ $suspectedType = null;
+ $typeTokenCount = 0;
+
+ for ($x = ($i + 1); $x < $numTokens; $x++) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ if (isset($allowed[$this->tokens[$x]['code']]) === true) {
+ ++$typeTokenCount;
+ continue;
+ }
+
+ if ($typeTokenCount > 0
+ && ($this->tokens[$x]['code'] === T_BITWISE_AND
+ || $this->tokens[$x]['code'] === T_ELLIPSIS)
+ ) {
+ // Skip past reference and variadic indicators for parameter types.
+ continue;
+ }
+
+ if ($this->tokens[$x]['code'] === T_VARIABLE) {
+ // Parameter/Property defaults can not contain variables, so this could be a type.
+ $suspectedType = 'property or parameter';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) {
+ // Possible arrow function.
+ $suspectedType = 'return';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_SEMICOLON) {
+ // Possible abstract method or interface method.
+ $suspectedType = 'return';
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_OPEN_CURLY_BRACKET
+ && isset($this->tokens[$x]['scope_condition']) === true
+ && $this->tokens[$this->tokens[$x]['scope_condition']]['code'] === T_FUNCTION
+ ) {
+ $suspectedType = 'return';
+ }
+
+ break;
+ }//end for
+
+ if ($typeTokenCount === 0 || isset($suspectedType) === false) {
+ // Definitely not a union or intersection type, move on.
+ continue;
+ }
+
+ $typeTokenCount = 0;
+ $typeOperators = [$i];
+ $confirmed = false;
+
+ for ($x = ($i - 1); $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ if (isset($allowed[$this->tokens[$x]['code']]) === true) {
+ ++$typeTokenCount;
+ continue;
+ }
+
+ // Union and intersection types can't use the nullable operator, but be tolerant to parse errors.
+ if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) {
+ continue;
+ }
+
+ if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) {
+ $typeOperators[] = $x;
+ continue;
+ }
+
+ if ($suspectedType === 'return' && $this->tokens[$x]['code'] === T_COLON) {
+ $confirmed = true;
+ break;
+ }
+
+ if ($suspectedType === 'property or parameter'
+ && (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true
+ || $this->tokens[$x]['code'] === T_VAR
+ || $this->tokens[$x]['code'] === T_READONLY)
+ ) {
+ // This will also confirm constructor property promotion parameters, but that's fine.
+ $confirmed = true;
+ }
+
+ break;
+ }//end for
+
+ if ($confirmed === false
+ && $suspectedType === 'property or parameter'
+ && isset($this->tokens[$i]['nested_parenthesis']) === true
+ ) {
+ $parens = $this->tokens[$i]['nested_parenthesis'];
+ $last = end($parens);
+
+ if (isset($this->tokens[$last]['parenthesis_owner']) === true
+ && $this->tokens[$this->tokens[$last]['parenthesis_owner']]['code'] === T_FUNCTION
+ ) {
+ $confirmed = true;
+ } else {
+ // No parenthesis owner set, this may be an arrow function which has not yet
+ // had additional processing done.
+ if (isset($this->tokens[$last]['parenthesis_opener']) === true) {
+ for ($x = ($this->tokens[$last]['parenthesis_opener'] - 1); $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] === T_FN) {
+ for (--$x; $x >= 0; $x--) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true
+ || $this->tokens[$x]['code'] === T_BITWISE_AND
+ ) {
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->tokens[$x]['code'] !== T_FUNCTION) {
+ $confirmed = true;
+ }
+ }
+ }//end if
+ }//end if
+
+ unset($parens, $last);
+ }//end if
+
+ if ($confirmed === false) {
+ // Not a union or intersection type after all, move on.
+ continue;
+ }
+
+ foreach ($typeOperators as $x) {
+ if ($this->tokens[$x]['code'] === T_BITWISE_OR) {
+ $this->tokens[$x]['code'] = T_TYPE_UNION;
+ $this->tokens[$x]['type'] = 'T_TYPE_UNION';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$x]['line'];
+ echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL;
+ }
+ } else {
+ $this->tokens[$x]['code'] = T_TYPE_INTERSECTION;
+ $this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION';
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$x]['line'];
+ echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL;
+ }
+ }
+ }
+
continue;
} else if ($this->tokens[$i]['code'] === T_STATIC) {
for ($x = ($i - 1); $x > 0; $x--) {
@@ -2037,12 +3056,7 @@ protected function processAdditional()
}
}
- $context = [
- T_OBJECT_OPERATOR => true,
- T_NS_SEPARATOR => true,
- T_PAAMAYIM_NEKUDOTAYIM => true,
- ];
- if (isset($context[$this->tokens[$x]['code']]) === true) {
+ if (isset($this->tstringContexts[$this->tokens[$x]['code']]) === true) {
if (PHP_CODESNIFFER_VERBOSITY > 1) {
$line = $this->tokens[$i]['line'];
$type = $this->tokens[$i]['type'];
@@ -2052,25 +3066,6 @@ protected function processAdditional()
$this->tokens[$i]['code'] = T_STRING;
$this->tokens[$i]['type'] = 'T_STRING';
}
- } else if ($this->tokens[$i]['code'] === T_CONST) {
- // Context sensitive keywords support.
- for ($x = ($i + 1); $i < $numTokens; $x++) {
- if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) {
- // Non-whitespace content.
- break;
- }
- }
-
- if ($this->tokens[$x]['code'] !== T_STRING) {
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- $line = $this->tokens[$x]['line'];
- $type = $this->tokens[$x]['type'];
- echo "\t* token $x on line $line changed from $type to T_STRING".PHP_EOL;
- }
-
- $this->tokens[$x]['code'] = T_STRING;
- $this->tokens[$x]['type'] = 'T_STRING';
- }
}//end if
if (($this->tokens[$i]['code'] !== T_CASE
@@ -2408,4 +3403,141 @@ public static function resolveSimpleToken($token)
}//end resolveSimpleToken()
+ /**
+ * Finds a "closer" token (closing parenthesis or square bracket for example)
+ * Handle parenthesis balancing while searching for closing token
+ *
+ * @param array $tokens The list of tokens to iterate searching the closing token (as returned by token_get_all)
+ * @param int $start The starting position
+ * @param string|string[] $openerTokens The opening character
+ * @param string $closerChar The closing character
+ *
+ * @return int|null The position of the closing token, if found. NULL otherwise.
+ */
+ private function findCloser(array &$tokens, $start, $openerTokens, $closerChar)
+ {
+ $numTokens = count($tokens);
+ $stack = [0];
+ $closer = null;
+ $openerTokens = (array) $openerTokens;
+
+ for ($x = $start; $x < $numTokens; $x++) {
+ if (in_array($tokens[$x], $openerTokens, true) === true
+ || (is_array($tokens[$x]) === true && in_array($tokens[$x][1], $openerTokens, true) === true)
+ ) {
+ $stack[] = $x;
+ } else if ($tokens[$x] === $closerChar) {
+ array_pop($stack);
+ if (empty($stack) === true) {
+ $closer = $x;
+ break;
+ }
+ }
+ }
+
+ return $closer;
+
+ }//end findCloser()
+
+
+ /**
+ * PHP 8 attributes parser for PHP < 8
+ * Handles single-line and multiline attributes.
+ *
+ * @param array $tokens The original array of tokens (as returned by token_get_all)
+ * @param int $stackPtr The current position in token array
+ *
+ * @return array|null The array of parsed attribute tokens
+ */
+ private function parsePhpAttribute(array &$tokens, $stackPtr)
+ {
+
+ $token = $tokens[$stackPtr];
+
+ $commentBody = substr($token[1], 2);
+ $subTokens = @token_get_all(' $subToken) {
+ if (is_array($subToken) === true
+ && $subToken[0] === T_COMMENT
+ && strpos($subToken[1], '#[') === 0
+ ) {
+ $reparsed = $this->parsePhpAttribute($subTokens, $i);
+ if ($reparsed !== null) {
+ array_splice($subTokens, $i, 1, $reparsed);
+ } else {
+ $subToken[0] = T_ATTRIBUTE;
+ }
+ }
+ }
+
+ array_splice($subTokens, 0, 1, [[T_ATTRIBUTE, '#[']]);
+
+ // Go looking for the close bracket.
+ $bracketCloser = $this->findCloser($subTokens, 1, '[', ']');
+ if (PHP_VERSION_ID < 80000 && $bracketCloser === null) {
+ foreach (array_slice($tokens, ($stackPtr + 1)) as $token) {
+ if (is_array($token) === true) {
+ $commentBody .= $token[1];
+ } else {
+ $commentBody .= $token;
+ }
+ }
+
+ $subTokens = @token_get_all('findCloser($subTokens, 1, '[', ']');
+ if ($bracketCloser !== null) {
+ array_splice($tokens, ($stackPtr + 1), count($tokens), array_slice($subTokens, ($bracketCloser + 1)));
+ $subTokens = array_slice($subTokens, 0, ($bracketCloser + 1));
+ }
+ }
+
+ if ($bracketCloser === null) {
+ return null;
+ }
+
+ return $subTokens;
+
+ }//end parsePhpAttribute()
+
+
+ /**
+ * Creates a map for the attributes tokens that surround other tokens.
+ *
+ * @return void
+ */
+ private function createAttributesNestingMap()
+ {
+ $map = [];
+ for ($i = 0; $i < $this->numTokens; $i++) {
+ if (isset($this->tokens[$i]['attribute_opener']) === true
+ && $i === $this->tokens[$i]['attribute_opener']
+ ) {
+ if (empty($map) === false) {
+ $this->tokens[$i]['nested_attributes'] = $map;
+ }
+
+ if (isset($this->tokens[$i]['attribute_closer']) === true) {
+ $map[$this->tokens[$i]['attribute_opener']]
+ = $this->tokens[$i]['attribute_closer'];
+ }
+ } else if (isset($this->tokens[$i]['attribute_closer']) === true
+ && $i === $this->tokens[$i]['attribute_closer']
+ ) {
+ array_pop($map);
+ if (empty($map) === false) {
+ $this->tokens[$i]['nested_attributes'] = $map;
+ }
+ } else {
+ if (empty($map) === false) {
+ $this->tokens[$i]['nested_attributes'] = $map;
+ }
+ }//end if
+ }//end for
+
+ }//end createAttributesNestingMap()
+
+
}//end class
diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php
index 82b2b9cc78..0e00bf7f22 100644
--- a/src/Tokenizers/Tokenizer.php
+++ b/src/Tokenizers/Tokenizer.php
@@ -194,6 +194,8 @@ private function createPositionMap()
T_DOUBLE_QUOTED_STRING => true,
T_HEREDOC => true,
T_NOWDOC => true,
+ T_END_HEREDOC => true,
+ T_END_NOWDOC => true,
T_INLINE_HTML => true,
];
@@ -250,7 +252,7 @@ private function createPositionMap()
|| $this->tokens[$i]['code'] === T_DOC_COMMENT_TAG
|| ($inTests === true && $this->tokens[$i]['code'] === T_INLINE_HTML)
) {
- $commentText = ltrim($this->tokens[$i]['content'], " \t/*");
+ $commentText = ltrim($this->tokens[$i]['content'], " \t/*#");
$commentText = rtrim($commentText, " */\t\r\n");
$commentTextLower = strtolower($commentText);
if (strpos($commentText, '@codingStandards') !== false) {
@@ -424,10 +426,10 @@ private function createPositionMap()
$disabledSniffs = [];
$additionalText = substr($commentText, 14);
- if ($additionalText === false) {
+ if (empty($additionalText) === true) {
$ignoring = ['.all' => true];
} else {
- $parts = explode(',', substr($commentText, 13));
+ $parts = explode(',', $additionalText);
foreach ($parts as $sniffCode) {
$sniffCode = trim($sniffCode);
$disabledSniffs[$sniffCode] = true;
@@ -459,10 +461,10 @@ private function createPositionMap()
$enabledSniffs = [];
$additionalText = substr($commentText, 13);
- if ($additionalText === false) {
+ if (empty($additionalText) === true) {
$ignoring = null;
} else {
- $parts = explode(',', substr($commentText, 13));
+ $parts = explode(',', $additionalText);
foreach ($parts as $sniffCode) {
$sniffCode = trim($sniffCode);
$enabledSniffs[$sniffCode] = true;
@@ -520,10 +522,10 @@ private function createPositionMap()
$ignoreRules = [];
$additionalText = substr($commentText, 13);
- if ($additionalText === false) {
+ if (empty($additionalText) === true) {
$ignoreRules = ['.all' => true];
} else {
- $parts = explode(',', substr($commentText, 13));
+ $parts = explode(',', $additionalText);
foreach ($parts as $sniffCode) {
$ignoreRules[trim($sniffCode)] = true;
}
@@ -638,25 +640,13 @@ public function replaceTabsInToken(&$token, $prefix=' ', $padding=' ', $tabWidth
}
// Process the tab that comes after the content.
- $lastCurrColumn = $currColumn;
$tabNum++;
// Move the pointer to the next tab stop.
- if (($currColumn % $tabWidth) === 0) {
- // This is the first tab, and we are already at a
- // tab stop, so this tab counts as a single space.
- $currColumn++;
- } else {
- $currColumn++;
- while (($currColumn % $tabWidth) !== 0) {
- $currColumn++;
- }
-
- $currColumn++;
- }
-
- $length += ($currColumn - $lastCurrColumn);
- $newContent .= $prefix.str_repeat($padding, ($currColumn - $lastCurrColumn - 1));
+ $pad = ($tabWidth - ($currColumn + $tabWidth - 1) % $tabWidth);
+ $currColumn += $pad;
+ $length += $pad;
+ $newContent .= $prefix.str_repeat($padding, ($pad - 1));
}//end foreach
}//end if
@@ -740,6 +730,40 @@ private function createTokenMap()
$this->tokens[$i]['parenthesis_closer'] = $i;
$this->tokens[$opener]['parenthesis_closer'] = $i;
}//end if
+ } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE) {
+ $openers[] = $i;
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo str_repeat("\t", count($openers));
+ echo "=> Found attribute opener at $i".PHP_EOL;
+ }
+
+ $this->tokens[$i]['attribute_opener'] = $i;
+ $this->tokens[$i]['attribute_closer'] = null;
+ } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE_END) {
+ $numOpeners = count($openers);
+ if ($numOpeners !== 0) {
+ $opener = array_pop($openers);
+ if (isset($this->tokens[$opener]['attribute_opener']) === true) {
+ $this->tokens[$opener]['attribute_closer'] = $i;
+
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo str_repeat("\t", (count($openers) + 1));
+ echo "=> Found attribute closer at $i for $opener".PHP_EOL;
+ }
+
+ for ($x = ($opener + 1); $x <= $i; ++$x) {
+ if (isset($this->tokens[$x]['attribute_closer']) === true) {
+ continue;
+ }
+
+ $this->tokens[$x]['attribute_opener'] = $opener;
+ $this->tokens[$x]['attribute_closer'] = $i;
+ }
+ } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ echo str_repeat("\t", (count($openers) + 1));
+ echo "=> Found unowned attribute closer at $i for $opener".PHP_EOL;
+ }
+ }//end if
}//end if
/*
@@ -1111,6 +1135,13 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
continue;
}
+ if ($tokenType === T_NAMESPACE) {
+ // PHP namespace keywords are special because they can be
+ // used as blocks but also inline as operators.
+ // So if we find them nested inside another opener, just skip them.
+ continue;
+ }
+
if ($tokenType === T_FUNCTION
&& $this->tokens[$stackPtr]['code'] !== T_FUNCTION
) {
@@ -1291,11 +1322,12 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0)
// a new statement, it isn't a scope opener.
$disallowed = Util\Tokens::$assignmentTokens;
$disallowed += [
- T_DOLLAR => true,
- T_VARIABLE => true,
- T_OBJECT_OPERATOR => true,
- T_COMMA => true,
- T_OPEN_PARENTHESIS => true,
+ T_DOLLAR => true,
+ T_VARIABLE => true,
+ T_OBJECT_OPERATOR => true,
+ T_NULLSAFE_OBJECT_OPERATOR => true,
+ T_COMMA => true,
+ T_OPEN_PARENTHESIS => true,
];
if (isset($disallowed[$this->tokens[$x]['code']]) === true) {
diff --git a/src/Util/Cache.php b/src/Util/Cache.php
index b7819c91eb..68abef59c4 100644
--- a/src/Util/Cache.php
+++ b/src/Util/Cache.php
@@ -95,22 +95,25 @@ public static function load(Ruleset $ruleset, Config $config)
// hash. This ensures that core PHPCS changes will also invalidate the cache.
// Note that we ignore sniffs here, and any files that don't affect
// the outcome of the run.
- $di = new \RecursiveDirectoryIterator($installDir);
+ $di = new \RecursiveDirectoryIterator(
+ $installDir,
+ (\FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS)
+ );
$filter = new \RecursiveCallbackFilterIterator(
$di,
function ($file, $key, $iterator) {
- // Skip hidden files.
+ // Skip non-php files.
$filename = $file->getFilename();
- if (substr($filename, 0, 1) === '.') {
+ if ($file->isFile() === true && substr($filename, -4) !== '.php') {
return false;
}
- $filePath = Common::realpath($file->getPathname());
+ $filePath = Common::realpath($key);
if ($filePath === false) {
return false;
}
- if (is_dir($filePath) === true
+ if ($iterator->hasChildren() === true
&& ($filename === 'Standards'
|| $filename === 'Exceptions'
|| $filename === 'Reports'
diff --git a/src/Util/Common.php b/src/Util/Common.php
index f939fc76de..ce7967cc33 100644
--- a/src/Util/Common.php
+++ b/src/Util/Common.php
@@ -48,6 +48,34 @@ public static function isPharFile($path)
}//end isPharFile()
+ /**
+ * Checks if a file is readable.
+ *
+ * Addresses PHP bug related to reading files from network drives on Windows.
+ * e.g. when using WSL2.
+ *
+ * @param string $path The path to the file.
+ *
+ * @return boolean
+ */
+ public static function isReadable($path)
+ {
+ if (@is_readable($path) === true) {
+ return true;
+ }
+
+ if (@file_exists($path) === true && @is_file($path) === true) {
+ $f = @fopen($path, 'rb');
+ if (fclose($f) === true) {
+ return true;
+ }
+ }
+
+ return false;
+
+ }//end isReadable()
+
+
/**
* CodeSniffer alternative for realpath.
*
@@ -211,6 +239,28 @@ public static function isStdinATTY()
}//end isStdinATTY()
+ /**
+ * Escape a path to a system command.
+ *
+ * @param string $cmd The path to the system command.
+ *
+ * @return string
+ */
+ public static function escapeshellcmd($cmd)
+ {
+ $cmd = escapeshellcmd($cmd);
+
+ if (stripos(PHP_OS, 'WIN') === 0) {
+ // Spaces are not escaped by escapeshellcmd on Windows, but need to be
+ // for the command to be able to execute.
+ $cmd = preg_replace('`(?= 48 && $ascii <= 57) {
- // The character is a number, so it cant be a capital.
+ // The character is a number, so it can't be a capital.
$isCaps = false;
} else {
if (strtoupper($string[$i]) === $string[$i]) {
diff --git a/src/Util/Standards.php b/src/Util/Standards.php
index 50f58f0294..65e5d6cb3c 100644
--- a/src/Util/Standards.php
+++ b/src/Util/Standards.php
@@ -180,7 +180,8 @@ public static function getInstalledStandards(
// Check if the installed dir is actually a standard itself.
$csFile = $standardsDir.'/ruleset.xml';
if (is_file($csFile) === true) {
- $installedStandards[] = basename($standardsDir);
+ $basename = basename($standardsDir);
+ $installedStandards[$basename] = $basename;
continue;
}
@@ -190,6 +191,7 @@ public static function getInstalledStandards(
}
$di = new \DirectoryIterator($standardsDir);
+ $standardsInDir = [];
foreach ($di as $file) {
if ($file->isDir() === true && $file->isDot() === false) {
$filename = $file->getFilename();
@@ -202,10 +204,13 @@ public static function getInstalledStandards(
// Valid coding standard dirs include a ruleset.
$csFile = $file->getPathname().'/ruleset.xml';
if (is_file($csFile) === true) {
- $installedStandards[] = $filename;
+ $standardsInDir[$filename] = $filename;
}
}
}
+
+ natsort($standardsInDir);
+ $installedStandards += $standardsInDir;
}//end foreach
return $installedStandards;
diff --git a/src/Util/Timing.php b/src/Util/Timing.php
index cf27dcfe36..95ee85216d 100644
--- a/src/Util/Timing.php
+++ b/src/Util/Timing.php
@@ -64,7 +64,7 @@ public static function printRunTime($force=false)
if ($time > 60000) {
$mins = floor($time / 60000);
- $secs = round((($time % 60000) / 1000), 2);
+ $secs = round((fmod($time, 60000) / 1000), 2);
$time = $mins.' mins';
if ($secs !== 0) {
$time .= ", $secs secs";
diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php
index d11b193629..bb1fb2ca99 100644
--- a/src/Util/Tokens.php
+++ b/src/Util/Tokens.php
@@ -74,7 +74,14 @@
define('T_CLOSE_USE_GROUP', 'PHPCS_T_CLOSE_USE_GROUP');
define('T_ZSR', 'PHPCS_T_ZSR');
define('T_ZSR_EQUAL', 'PHPCS_T_ZSR_EQUAL');
-define('T_FN_ARROW', 'T_FN_ARROW');
+define('T_FN_ARROW', 'PHPCS_T_FN_ARROW');
+define('T_TYPE_UNION', 'PHPCS_T_TYPE_UNION');
+define('T_PARAM_NAME', 'PHPCS_T_PARAM_NAME');
+define('T_MATCH_ARROW', 'PHPCS_T_MATCH_ARROW');
+define('T_MATCH_DEFAULT', 'PHPCS_T_MATCH_DEFAULT');
+define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END');
+define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE');
+define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION');
// Some PHP 5.5 tokens, replicated for lower versions.
if (defined('T_FINALLY') === false) {
@@ -124,6 +131,48 @@
define('T_FN', 'PHPCS_T_FN');
}
+// Some PHP 8.0 tokens, replicated for lower versions.
+if (defined('T_NULLSAFE_OBJECT_OPERATOR') === false) {
+ define('T_NULLSAFE_OBJECT_OPERATOR', 'PHPCS_T_NULLSAFE_OBJECT_OPERATOR');
+}
+
+if (defined('T_NAME_QUALIFIED') === false) {
+ define('T_NAME_QUALIFIED', 'PHPCS_T_NAME_QUALIFIED');
+}
+
+if (defined('T_NAME_FULLY_QUALIFIED') === false) {
+ define('T_NAME_FULLY_QUALIFIED', 'PHPCS_T_NAME_FULLY_QUALIFIED');
+}
+
+if (defined('T_NAME_RELATIVE') === false) {
+ define('T_NAME_RELATIVE', 'PHPCS_T_NAME_RELATIVE');
+}
+
+if (defined('T_MATCH') === false) {
+ define('T_MATCH', 'PHPCS_T_MATCH');
+}
+
+if (defined('T_ATTRIBUTE') === false) {
+ define('T_ATTRIBUTE', 'PHPCS_T_ATTRIBUTE');
+}
+
+// Some PHP 8.1 tokens, replicated for lower versions.
+if (defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') === false) {
+ define('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG');
+}
+
+if (defined('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG') === false) {
+ define('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG');
+}
+
+if (defined('T_READONLY') === false) {
+ define('T_READONLY', 'PHPCS_T_READONLY');
+}
+
+if (defined('T_ENUM') === false) {
+ define('T_ENUM', 'PHPCS_T_ENUM');
+}
+
// Tokens used for parsing doc blocks.
define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR');
define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE');
@@ -151,6 +200,7 @@ final class Tokens
T_CLASS => 1000,
T_INTERFACE => 1000,
T_TRAIT => 1000,
+ T_ENUM => 1000,
T_NAMESPACE => 1000,
T_FUNCTION => 100,
T_CLOSURE => 100,
@@ -170,6 +220,7 @@ final class Tokens
T_CATCH => 50,
T_FINALLY => 50,
T_SWITCH => 50,
+ T_MATCH => 50,
T_SELF => 25,
T_PARENT => 25,
@@ -362,6 +413,7 @@ final class Tokens
T_ELSEIF => T_ELSEIF,
T_CATCH => T_CATCH,
T_DECLARE => T_DECLARE,
+ T_MATCH => T_MATCH,
];
/**
@@ -374,6 +426,7 @@ final class Tokens
T_ANON_CLASS => T_ANON_CLASS,
T_INTERFACE => T_INTERFACE,
T_TRAIT => T_TRAIT,
+ T_ENUM => T_ENUM,
T_NAMESPACE => T_NAMESPACE,
T_FUNCTION => T_FUNCTION,
T_CLOSURE => T_CLOSURE,
@@ -394,6 +447,7 @@ final class Tokens
T_PROPERTY => T_PROPERTY,
T_OBJECT => T_OBJECT,
T_USE => T_USE,
+ T_MATCH => T_MATCH,
];
/**
@@ -574,6 +628,7 @@ final class Tokens
T_UNSET => T_UNSET,
T_EMPTY => T_EMPTY,
T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
T_STATIC => T_STATIC,
];
@@ -587,6 +642,105 @@ final class Tokens
T_ANON_CLASS => T_ANON_CLASS,
T_INTERFACE => T_INTERFACE,
T_TRAIT => T_TRAIT,
+ T_ENUM => T_ENUM,
+ ];
+
+ /**
+ * Tokens representing PHP magic constants.
+ *
+ * @var array =>
+ *
+ * @link https://www.php.net/language.constants.predefined PHP Manual on magic constants
+ */
+ public static $magicConstants = [
+ T_CLASS_C => T_CLASS_C,
+ T_DIR => T_DIR,
+ T_FILE => T_FILE,
+ T_FUNC_C => T_FUNC_C,
+ T_LINE => T_LINE,
+ T_METHOD_C => T_METHOD_C,
+ T_NS_C => T_NS_C,
+ T_TRAIT_C => T_TRAIT_C,
+ ];
+
+ /**
+ * Tokens representing context sensitive keywords in PHP.
+ *
+ * @var array
+ *
+ * https://wiki.php.net/rfc/context_sensitive_lexer
+ */
+ public static $contextSensitiveKeywords = [
+ T_ABSTRACT => T_ABSTRACT,
+ T_ARRAY => T_ARRAY,
+ T_AS => T_AS,
+ T_BREAK => T_BREAK,
+ T_CALLABLE => T_CALLABLE,
+ T_CASE => T_CASE,
+ T_CATCH => T_CATCH,
+ T_CLASS => T_CLASS,
+ T_CLONE => T_CLONE,
+ T_CONST => T_CONST,
+ T_CONTINUE => T_CONTINUE,
+ T_DECLARE => T_DECLARE,
+ T_DEFAULT => T_DEFAULT,
+ T_DO => T_DO,
+ T_ECHO => T_ECHO,
+ T_ELSE => T_ELSE,
+ T_ELSEIF => T_ELSEIF,
+ T_EMPTY => T_EMPTY,
+ T_ENDDECLARE => T_ENDDECLARE,
+ T_ENDFOR => T_ENDFOR,
+ T_ENDFOREACH => T_ENDFOREACH,
+ T_ENDIF => T_ENDIF,
+ T_ENDSWITCH => T_ENDSWITCH,
+ T_ENDWHILE => T_ENDWHILE,
+ T_ENUM => T_ENUM,
+ T_EVAL => T_EVAL,
+ T_EXIT => T_EXIT,
+ T_EXTENDS => T_EXTENDS,
+ T_FINAL => T_FINAL,
+ T_FINALLY => T_FINALLY,
+ T_FN => T_FN,
+ T_FOR => T_FOR,
+ T_FOREACH => T_FOREACH,
+ T_FUNCTION => T_FUNCTION,
+ T_GLOBAL => T_GLOBAL,
+ T_GOTO => T_GOTO,
+ T_IF => T_IF,
+ T_IMPLEMENTS => T_IMPLEMENTS,
+ T_INCLUDE => T_INCLUDE,
+ T_INCLUDE_ONCE => T_INCLUDE_ONCE,
+ T_INSTANCEOF => T_INSTANCEOF,
+ T_INSTEADOF => T_INSTEADOF,
+ T_INTERFACE => T_INTERFACE,
+ T_ISSET => T_ISSET,
+ T_LIST => T_LIST,
+ T_LOGICAL_AND => T_LOGICAL_AND,
+ T_LOGICAL_OR => T_LOGICAL_OR,
+ T_LOGICAL_XOR => T_LOGICAL_XOR,
+ T_MATCH => T_MATCH,
+ T_NAMESPACE => T_NAMESPACE,
+ T_NEW => T_NEW,
+ T_PRINT => T_PRINT,
+ T_PRIVATE => T_PRIVATE,
+ T_PROTECTED => T_PROTECTED,
+ T_PUBLIC => T_PUBLIC,
+ T_READONLY => T_READONLY,
+ T_REQUIRE => T_REQUIRE,
+ T_REQUIRE_ONCE => T_REQUIRE_ONCE,
+ T_RETURN => T_RETURN,
+ T_STATIC => T_STATIC,
+ T_SWITCH => T_SWITCH,
+ T_THROW => T_THROW,
+ T_TRAIT => T_TRAIT,
+ T_TRY => T_TRY,
+ T_UNSET => T_UNSET,
+ T_USE => T_USE,
+ T_VAR => T_VAR,
+ T_WHILE => T_WHILE,
+ T_YIELD => T_YIELD,
+ T_YIELD_FROM => T_YIELD_FROM,
];
diff --git a/tests/AllTests.php b/tests/AllTests.php
index 4ed001dfed..9d099c1e3d 100644
--- a/tests/AllTests.php
+++ b/tests/AllTests.php
@@ -9,16 +9,13 @@
namespace PHP_CodeSniffer\Tests;
-$GLOBALS['PHP_CODESNIFFER_PEAR'] = false;
-
-if (is_file(__DIR__.'/../autoload.php') === true) {
+if ($GLOBALS['PHP_CODESNIFFER_PEAR'] === false) {
include_once 'Core/AllTests.php';
include_once 'Standards/AllSniffs.php';
} else {
include_once 'CodeSniffer/Core/AllTests.php';
include_once 'CodeSniffer/Standards/AllSniffs.php';
include_once 'FileList.php';
- $GLOBALS['PHP_CODESNIFFER_PEAR'] = true;
}
// PHPUnit 7 made the TestSuite run() method incompatible with
diff --git a/tests/Core/Autoloader/DetermineLoadedClassTest.php b/tests/Core/Autoloader/DetermineLoadedClassTest.php
new file mode 100644
index 0000000000..c0f38fa6f1
--- /dev/null
+++ b/tests/Core/Autoloader/DetermineLoadedClassTest.php
@@ -0,0 +1,119 @@
+
+ * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Autoloader;
+
+use PHP_CodeSniffer\Autoload;
+use PHPUnit\Framework\TestCase;
+
+class DetermineLoadedClassTest extends TestCase
+{
+
+
+ /**
+ * Load the test files.
+ *
+ * @return void
+ */
+ public static function setUpBeforeClass()
+ {
+ include __DIR__.'/TestFiles/Sub/C.inc';
+
+ }//end setUpBeforeClass()
+
+
+ /**
+ * Test for when class list is ordered.
+ *
+ * @return void
+ */
+ public function testOrdered()
+ {
+ $classesBeforeLoad = [
+ 'classes' => [],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $classesAfterLoad = [
+ 'classes' => [
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\A',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\B',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
+ ],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
+ $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
+
+ }//end testOrdered()
+
+
+ /**
+ * Test for when class list is out of order.
+ *
+ * @return void
+ */
+ public function testUnordered()
+ {
+ $classesBeforeLoad = [
+ 'classes' => [],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $classesAfterLoad = [
+ 'classes' => [
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\A',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\B',
+ ],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
+ $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
+
+ $classesAfterLoad = [
+ 'classes' => [
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\A',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\B',
+ ],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
+ $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
+
+ $classesAfterLoad = [
+ 'classes' => [
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\A',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\C',
+ 'PHP_CodeSniffer\Tests\Core\Autoloader\B',
+ ],
+ 'interfaces' => [],
+ 'traits' => [],
+ ];
+
+ $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad);
+ $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className);
+
+ }//end testUnordered()
+
+
+}//end class
diff --git a/tests/Core/Autoloader/TestFiles/A.inc b/tests/Core/Autoloader/TestFiles/A.inc
new file mode 100644
index 0000000000..c1433718bb
--- /dev/null
+++ b/tests/Core/Autoloader/TestFiles/A.inc
@@ -0,0 +1,3 @@
+
+ * @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Config;
+
+use PHP_CodeSniffer\Config;
+use PHPUnit\Framework\TestCase;
+use ReflectionProperty;
+
+class ReportWidthTest extends TestCase
+{
+
+
+ /**
+ * Set static properties in the Config class to prevent tests influencing each other.
+ *
+ * @before
+ *
+ * @return void
+ */
+ public static function cleanConfig()
+ {
+ // Set to the property's default value to clear out potentially set values from other tests.
+ self::setStaticProperty('executablePaths', []);
+
+ // Set to a usable value to circumvent Config trying to find a phpcs.xml config file.
+ self::setStaticProperty('overriddenDefaults', ['standards' => ['PSR1']]);
+
+ // Set to values which prevent the test-runner user's `CodeSniffer.conf` file
+ // from being read and influencing the tests.
+ self::setStaticProperty('configData', []);
+ self::setStaticProperty('configDataFile', '');
+
+ }//end cleanConfig()
+
+
+ /**
+ * Clean up after each finished test.
+ *
+ * @after
+ *
+ * @return void
+ */
+ public function resetConfig()
+ {
+ $_SERVER['argv'] = [];
+
+ }//end resetConfig()
+
+
+ /**
+ * Reset the static properties in the Config class to their true defaults to prevent this class
+ * from unfluencing other tests.
+ *
+ * @afterClass
+ *
+ * @return void
+ */
+ public static function resetConfigToDefaults()
+ {
+ self::setStaticProperty('overriddenDefaults', []);
+ self::setStaticProperty('executablePaths', []);
+ self::setStaticProperty('configData', null);
+ self::setStaticProperty('configDataFile', null);
+ $_SERVER['argv'] = [];
+
+ }//end resetConfigToDefaults()
+
+
+ /**
+ * Test that report width without overrules will always be set to a non-0 positive integer.
+ *
+ * @return void
+ */
+ public function testReportWidthDefault()
+ {
+ $config = new Config();
+
+ // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests.
+ $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer');
+ $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0');
+
+ }//end testReportWidthDefault()
+
+
+ /**
+ * Test that the report width will be set to a non-0 positive integer when not found in the CodeSniffer.conf file.
+ *
+ * @return void
+ */
+ public function testReportWidthWillBeSetFromAutoWhenNotFoundInConfFile()
+ {
+ $phpCodeSnifferConfig = [
+ 'default_standard' => 'PSR2',
+ 'show_warnings' => '0',
+ ];
+
+ $this->setStaticProperty('configData', $phpCodeSnifferConfig);
+
+ $config = new Config();
+
+ // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests.
+ $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer');
+ $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0');
+
+ }//end testReportWidthWillBeSetFromAutoWhenNotFoundInConfFile()
+
+
+ /**
+ * Test that the report width will be set correctly when found in the CodeSniffer.conf file.
+ *
+ * @return void
+ */
+ public function testReportWidthCanBeSetFromConfFile()
+ {
+ $phpCodeSnifferConfig = [
+ 'default_standard' => 'PSR2',
+ 'report_width' => '120',
+ ];
+
+ $this->setStaticProperty('configData', $phpCodeSnifferConfig);
+
+ $config = new Config();
+ $this->assertSame(120, $config->reportWidth);
+
+ }//end testReportWidthCanBeSetFromConfFile()
+
+
+ /**
+ * Test that the report width will be set correctly when passed as a CLI argument.
+ *
+ * @return void
+ */
+ public function testReportWidthCanBeSetFromCLI()
+ {
+ $_SERVER['argv'] = [
+ 'phpcs',
+ '--report-width=100',
+ ];
+
+ $config = new Config();
+ $this->assertSame(100, $config->reportWidth);
+
+ }//end testReportWidthCanBeSetFromCLI()
+
+
+ /**
+ * Test that the report width will be set correctly when multiple report widths are passed on the CLI.
+ *
+ * @return void
+ */
+ public function testReportWidthWhenSetFromCLIFirstValuePrevails()
+ {
+ $_SERVER['argv'] = [
+ 'phpcs',
+ '--report-width=100',
+ '--report-width=200',
+ ];
+
+ $config = new Config();
+ $this->assertSame(100, $config->reportWidth);
+
+ }//end testReportWidthWhenSetFromCLIFirstValuePrevails()
+
+
+ /**
+ * Test that a report width passed as a CLI argument will overrule a report width set in a CodeSniffer.conf file.
+ *
+ * @return void
+ */
+ public function testReportWidthSetFromCLIOverrulesConfFile()
+ {
+ $phpCodeSnifferConfig = [
+ 'default_standard' => 'PSR2',
+ 'report_format' => 'summary',
+ 'show_warnings' => '0',
+ 'show_progress' => '1',
+ 'report_width' => '120',
+ ];
+
+ $this->setStaticProperty('configData', $phpCodeSnifferConfig);
+
+ $cliArgs = [
+ 'phpcs',
+ '--report-width=180',
+ ];
+
+ $config = new Config($cliArgs);
+ $this->assertSame(180, $config->reportWidth);
+
+ }//end testReportWidthSetFromCLIOverrulesConfFile()
+
+
+ /**
+ * Test that the report width will be set to a non-0 positive integer when set to "auto".
+ *
+ * @return void
+ */
+ public function testReportWidthInputHandlingForAuto()
+ {
+ $config = new Config();
+ $config->reportWidth = 'auto';
+
+ // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests.
+ $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer');
+ $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0');
+
+ }//end testReportWidthInputHandlingForAuto()
+
+
+ /**
+ * Test that the report width will be set correctly for various types of input.
+ *
+ * @param mixed $input Input value received.
+ * @param int $expected Expected report width.
+ *
+ * @dataProvider dataReportWidthInputHandling
+ *
+ * @return void
+ */
+ public function testReportWidthInputHandling($input, $expected)
+ {
+ $config = new Config();
+ $config->reportWidth = $input;
+
+ $this->assertSame($expected, $config->reportWidth);
+
+ }//end testReportWidthInputHandling()
+
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function dataReportWidthInputHandling()
+ {
+ return [
+ 'No value (empty string)' => [
+ 'value' => '',
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: invalid input type null' => [
+ 'value' => null,
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: invalid input type false' => [
+ 'value' => false,
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: invalid input type float' => [
+ 'value' => 100.50,
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: invalid string value "invalid"' => [
+ 'value' => 'invalid',
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: invalid string value, non-integer string "50.25"' => [
+ 'value' => '50.25',
+ 'expected' => Config::DEFAULT_REPORT_WIDTH,
+ ],
+ 'Value: valid numeric string value' => [
+ 'value' => '250',
+ 'expected' => 250,
+ ],
+ 'Value: valid int value' => [
+ 'value' => 220,
+ 'expected' => 220,
+ ],
+ 'Value: negative int value becomes positive int' => [
+ 'value' => -180,
+ 'expected' => 180,
+ ],
+ ];
+
+ }//end dataReportWidthInputHandling()
+
+
+ /**
+ * Helper function to set a static property on the Config class.
+ *
+ * @param string $name The name of the property to set.
+ * @param mixed $value The value to set the propert to.
+ *
+ * @return void
+ */
+ public static function setStaticProperty($name, $value)
+ {
+ $property = new ReflectionProperty('PHP_CodeSniffer\Config', $name);
+ $property->setAccessible(true);
+ $property->setValue($value);
+ $property->setAccessible(false);
+
+ }//end setStaticProperty()
+
+
+}//end class
diff --git a/tests/Core/ErrorSuppressionTest.php b/tests/Core/ErrorSuppressionTest.php
index f031b334bb..7181613c62 100644
--- a/tests/Core/ErrorSuppressionTest.php
+++ b/tests/Core/ErrorSuppressionTest.php
@@ -21,1245 +21,1227 @@ class ErrorSuppressionTest extends TestCase
/**
* Test suppressing a single error.
*
+ * @param string $before Annotation to place before the code.
+ * @param string $after Annotation to place after the code.
+ * @param int $expectedErrors Optional. Number of errors expected.
+ * Defaults to 0.
+ *
+ * @dataProvider dataSuppressError
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testSuppressError()
+ public function testSuppressError($before, $after, $expectedErrors=0)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
+ static $config, $ruleset;
- $ruleset = new Ruleset($config);
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
- // Process without suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with inline comment suppression.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- // Process with multi-line inline comment suppression, tab-indented.
- $content = 'process();
+ }//end testSuppressError()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- // Process with inline @ comment suppression.
- $content = 'process();
+ /**
+ * Data provider.
+ *
+ * @see testSuppressError()
+ *
+ * @return array
+ */
+ public function dataSuppressError()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'after' => '',
+ 'expectedErrors' => 1,
+ ],
+
+ // Inline slash comments.
+ 'disable/enable: slash comment' => [
+ 'before' => '// phpcs:disable'.PHP_EOL,
+ 'after' => '// phpcs:enable',
+ ],
+ 'disable/enable: multi-line slash comment, tab indented' => [
+ 'before' => "\t".'// For reasons'.PHP_EOL."\t".'// phpcs:disable'.PHP_EOL."\t",
+ 'after' => "\t".'// phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @' => [
+ 'before' => '// @phpcs:disable'.PHP_EOL,
+ 'after' => '// @phpcs:enable',
+ ],
+ 'disable/enable: slash comment, mixed case' => [
+ 'before' => '// PHPCS:Disable'.PHP_EOL,
+ 'after' => '// pHPcs:enabLE',
+ ],
+
+ // Inline hash comments.
+ 'disable/enable: hash comment' => [
+ 'before' => '# phpcs:disable'.PHP_EOL,
+ 'after' => '# phpcs:enable',
+ ],
+ 'disable/enable: multi-line hash comment, tab indented' => [
+ 'before' => "\t".'# For reasons'.PHP_EOL."\t".'# phpcs:disable'.PHP_EOL."\t",
+ 'after' => "\t".'# phpcs:enable',
+ ],
+ 'disable/enable: hash comment, with @' => [
+ 'before' => '# @phpcs:disable'.PHP_EOL,
+ 'after' => '# @phpcs:enable',
+ ],
+ 'disable/enable: hash comment, mixed case' => [
+ 'before' => '# PHPCS:Disable'.PHP_EOL,
+ 'after' => '# pHPcs:enabLE',
+ ],
+
+ // Inline star (block) comments.
+ 'disable/enable: star comment' => [
+ 'before' => '/* phpcs:disable */'.PHP_EOL,
+ 'after' => '/* phpcs:enable */',
+ ],
+ 'disable/enable: multi-line star comment' => [
+ 'before' => '/*'.PHP_EOL.' phpcs:disable'.PHP_EOL.' */'.PHP_EOL,
+ 'after' => '/*'.PHP_EOL.' phpcs:enable'.PHP_EOL.' */',
+ ],
+ 'disable/enable: multi-line star comment, each line starred' => [
+ 'before' => '/*'.PHP_EOL.' * phpcs:disable'.PHP_EOL.' */'.PHP_EOL,
+ 'after' => '/*'.PHP_EOL.' * phpcs:enable'.PHP_EOL.' */',
+ ],
+ 'disable/enable: multi-line star comment, each line starred, tab indented' => [
+ 'before' => "\t".'/*'.PHP_EOL."\t".' * phpcs:disable'.PHP_EOL."\t".' */'.PHP_EOL."\t",
+ 'after' => "\t".'/*'.PHP_EOL.' * phpcs:enable'.PHP_EOL.' */',
+ ],
+
+ // Docblock comments.
+ 'disable/enable: single line docblock comment' => [
+ 'before' => '/** phpcs:disable */'.PHP_EOL,
+ 'after' => '/** phpcs:enable */',
+ ],
+
+ // Deprecated syntax.
+ 'old style: slash comment' => [
+ 'before' => '// @codingStandardsIgnoreStart'.PHP_EOL,
+ 'after' => '// @codingStandardsIgnoreEnd',
+ ],
+ 'old style: star comment' => [
+ 'before' => '/* @codingStandardsIgnoreStart */'.PHP_EOL,
+ 'after' => '/* @codingStandardsIgnoreEnd */',
+ ],
+ 'old style: multi-line star comment' => [
+ 'before' => '/*'.PHP_EOL.' @codingStandardsIgnoreStart'.PHP_EOL.' */'.PHP_EOL,
+ 'after' => '/*'.PHP_EOL.' @codingStandardsIgnoreEnd'.PHP_EOL.' */',
+ ],
+ 'old style: single line docblock comment' => [
+ 'before' => '/** @codingStandardsIgnoreStart */'.PHP_EOL,
+ 'after' => '/** @codingStandardsIgnoreEnd */',
+ ],
+ ];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ }//end dataSuppressError()
- // Process with inline comment suppression mixed case.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ /**
+ * Test suppressing 1 out of 2 errors.
+ *
+ * @param string $before Annotation to place before the code.
+ * @param string $between Annotation to place between the code.
+ * @param int $expectedErrors Optional. Number of errors expected.
+ * Defaults to 1.
+ *
+ * @dataProvider dataSuppressSomeErrors
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
+ * @return void
+ */
+ public function testSuppressSomeErrors($before, $between, $expectedErrors=1)
+ {
+ static $config, $ruleset;
- // Process with inline comment suppression (deprecated syntax).
- $content = 'process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with block comment suppression.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- // Process with multi-line block comment suppression.
- $content = 'process();
+ }//end testSuppressSomeErrors()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- // Process with multi-line block comment suppression, each line starred.
- $content = 'process();
+ /**
+ * Data provider.
+ *
+ * @see testSuppressSomeErrors()
+ *
+ * @return array
+ */
+ public function dataSuppressSomeErrors()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'between' => '',
+ 'expectedErrors' => 2,
+ ],
+
+ // With suppression.
+ 'disable/enable: slash comment' => [
+ 'before' => '// phpcs:disable',
+ 'between' => '// phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @' => [
+ 'before' => '// @phpcs:disable',
+ 'between' => '// @phpcs:enable',
+ ],
+ 'disable/enable: hash comment' => [
+ 'before' => '# phpcs:disable',
+ 'between' => '# phpcs:enable',
+ ],
+ 'disable/enable: hash comment, with @' => [
+ 'before' => '# @phpcs:disable',
+ 'between' => '# @phpcs:enable',
+ ],
+ 'disable/enable: single line docblock comment' => [
+ 'before' => '/** phpcs:disable */',
+ 'between' => '/** phpcs:enable */',
+ ],
+
+ // Deprecated syntax.
+ 'old style: slash comment' => [
+ 'before' => '// @codingStandardsIgnoreStart',
+ 'between' => '// @codingStandardsIgnoreEnd',
+ ],
+ 'old style: single line docblock comment' => [
+ 'before' => '/** @codingStandardsIgnoreStart */',
+ 'between' => '/** @codingStandardsIgnoreEnd */',
+ ],
+ ];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ }//end dataSuppressSomeErrors()
- // Process with multi-line block comment suppression, tab-indented.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ /**
+ * Test suppressing a single warning.
+ *
+ * @param string $before Annotation to place before the code.
+ * @param string $after Annotation to place after the code.
+ * @param int $expectedWarnings Optional. Number of warnings expected.
+ * Defaults to 0.
+ *
+ * @dataProvider dataSuppressWarning
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
+ * @return void
+ */
+ public function testSuppressWarning($before, $after, $expectedWarnings=0)
+ {
+ static $config, $ruleset;
- // Process with block comment suppression (deprecated syntax).
- $content = 'process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.Commenting.Todo'];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with multi-line block comment suppression (deprecated syntax).
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
- // Process with a docblock suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ }//end testSuppressWarning()
- // Process with a docblock suppression (deprecated syntax).
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ /**
+ * Data provider.
+ *
+ * @see testSuppressWarning()
+ *
+ * @return array
+ */
+ public function dataSuppressWarning()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'after' => '',
+ 'expectedWarnings' => 1,
+ ],
+
+ // With suppression.
+ 'disable/enable: slash comment' => [
+ 'before' => '// phpcs:disable',
+ 'after' => '// phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @' => [
+ 'before' => '// @phpcs:disable',
+ 'after' => '// @phpcs:enable',
+ ],
+ 'disable/enable: single line docblock comment' => [
+ 'before' => '/** phpcs:disable */',
+ 'after' => '/** phpcs:enable */',
+ ],
+
+ // Deprecated syntax.
+ 'old style: slash comment' => [
+ 'before' => '// @codingStandardsIgnoreStart',
+ 'after' => '// @codingStandardsIgnoreEnd',
+ ],
+ 'old style: single line docblock comment' => [
+ 'before' => '/** @codingStandardsIgnoreStart */',
+ 'after' => '/** @codingStandardsIgnoreEnd */',
+ ],
+ ];
- }//end testSuppressError()
+ }//end dataSuppressWarning()
/**
- * Test suppressing 1 out of 2 errors.
+ * Test suppressing a single error using a single line ignore.
+ *
+ * @param string $before Annotation to place before the code.
+ * @param string $after Optional. Annotation to place after the code.
+ * Defaults to an empty string.
+ * @param int $expectedErrors Optional. Number of errors expected.
+ * Defaults to 1.
+ *
+ * @dataProvider dataSuppressLine
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
*
* @return void
*/
- public function testSuppressSomeErrors()
+ public function testSuppressLine($before, $after='', $expectedErrors=1)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
+ static $config, $ruleset;
- $ruleset = new Ruleset($config);
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
- // Process without suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
-
- // Process with suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with @ suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with suppression (deprecated syntax).
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- // Process with a PHPDoc block suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ }//end testSuppressLine()
- // Process with a PHPDoc block suppression (deprecated syntax).
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ /**
+ * Data provider.
+ *
+ * @see testSuppressLine()
+ *
+ * @return array
+ */
+ public function dataSuppressLine()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'after' => '',
+ 'expectedErrors' => 2,
+ ],
+
+ // With suppression on line before.
+ 'ignore: line before, slash comment' => ['before' => '// phpcs:ignore'],
+ 'ignore: line before, slash comment, with @' => ['before' => '// @phpcs:ignore'],
+ 'ignore: line before, hash comment' => ['before' => '# phpcs:ignore'],
+ 'ignore: line before, hash comment, with @' => ['before' => '# @phpcs:ignore'],
+ 'ignore: line before, star comment' => ['before' => '/* phpcs:ignore */'],
+ 'ignore: line before, star comment, with @' => ['before' => '/* @phpcs:ignore */'],
+
+ // With suppression as trailing comment on code line.
+ 'ignore: end of line, slash comment' => [
+ 'before' => '',
+ 'after' => ' // phpcs:ignore',
+ ],
+ 'ignore: end of line, slash comment, with @' => [
+ 'before' => '',
+ 'after' => ' // @phpcs:ignore',
+ ],
+ 'ignore: end of line, hash comment' => [
+ 'before' => '',
+ 'after' => ' # phpcs:ignore',
+ ],
+ 'ignore: end of line, hash comment, with @' => [
+ 'before' => '',
+ 'after' => ' # @phpcs:ignore',
+ ],
+
+ // Deprecated syntax.
+ 'old style: line before, slash comment' => ['before' => '// @codingStandardsIgnoreLine'],
+ 'old style: end of line, slash comment' => [
+ 'before' => '',
+ 'after' => ' // @codingStandardsIgnoreLine',
+ ],
+ ];
- }//end testSuppressSomeErrors()
+ }//end dataSuppressLine()
/**
- * Test suppressing a single warning.
+ * Test suppressing a single error using a single line ignore in the middle of a line.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
*
* @return void
*/
- public function testSuppressWarning()
+ public function testSuppressLineMidLine()
{
$config = new Config();
$config->standards = ['Generic'];
- $config->sniffs = ['Generic.Commenting.Todo'];
+ $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
$ruleset = new Ruleset($config);
- // Process without suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Process with suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with @ suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with suppression (deprecated syntax).
- $content = 'process();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
+ $this->assertSame(0, $file->getErrorCount());
+ $this->assertCount(0, $file->getErrors());
- // Process with a docblock suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with a docblock suppression (deprecated syntax).
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- }//end testSuppressWarning()
+ }//end testSuppressLineMidLine()
/**
- * Test suppressing a single error using a single line ignore.
+ * Test suppressing a single error using a single line ignore within a docblock.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
*
* @return void
*/
- public function testSuppressLine()
+ public function testSuppressLineWithinDocblock()
{
$config = new Config();
$config->standards = ['Generic'];
- $config->sniffs = [
- 'Generic.PHP.LowerCaseConstant',
- 'Generic.Files.LineLength',
- ];
+ $config->sniffs = ['Generic.Files.LineLength'];
$ruleset = new Ruleset($config);
- // Process without suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
-
- // Process with suppression on line before.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with @ suppression on line before.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with suppression on line before.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with @ suppression on line before.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with suppression on line before (deprecated syntax).
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
// Process with @ suppression on line before inside docblock.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with suppression on same line.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with @ suppression on same line.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
-
- // Process with suppression on same line (deprecated syntax).
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
+ $this->assertSame(0, $file->getErrorCount());
+ $this->assertCount(0, $file->getErrors());
- }//end testSuppressLine()
+ }//end testSuppressLineWithinDocblock()
/**
* Test that using a single line ignore does not interfere with other suppressions.
*
+ * @param string $before Annotation to place before the code.
+ * @param string $after Annotation to place after the code.
+ *
+ * @dataProvider dataNestedSuppressLine
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testNestedSuppressLine()
+ public function testNestedSuppressLine($before, $after)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
-
- $ruleset = new Ruleset($config);
+ static $config, $ruleset;
- // Process with disable/enable suppression and no single line suppression.
- $content = 'process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.PHP.LowerCaseConstant'];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with disable/enable @ suppression and no single line suppression.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with disable/enable suppression and no single line suppression (deprecated syntax).
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with line suppression nested within disable/enable suppression.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $this->assertSame(0, $file->getErrorCount());
+ $this->assertCount(0, $file->getErrors());
- // Process with line @ suppression nested within disable/enable @ suppression.
- $content = 'process();
+ }//end testNestedSuppressLine()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- // Process with line suppression nested within disable/enable suppression (deprecated syntax).
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ /**
+ * Data provider.
+ *
+ * @see testNestedSuppressLine()
+ *
+ * @return array
+ */
+ public function dataNestedSuppressLine()
+ {
+ return [
+ // Process with disable/enable suppression and no single line suppression.
+ 'disable/enable: slash comment, no single line suppression' => [
+ 'before' => '// phpcs:disable',
+ 'after' => '// phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @, no single line suppression' => [
+ 'before' => '// @phpcs:disable',
+ 'after' => '// @phpcs:enable',
+ ],
+ 'disable/enable: hash comment, no single line suppression' => [
+ 'before' => '# phpcs:disable',
+ 'after' => '# phpcs:enable',
+ ],
+ 'old style: slash comment, no single line suppression' => [
+ 'before' => '// @codingStandardsIgnoreStart',
+ 'after' => '// @codingStandardsIgnoreEnd',
+ ],
+
+ // Process with line suppression nested within disable/enable suppression.
+ 'disable/enable: slash comment, next line nested single line suppression' => [
+ 'before' => '// phpcs:disable'.PHP_EOL.'// phpcs:ignore',
+ 'after' => '// phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @, next line nested single line suppression' => [
+ 'before' => '// @phpcs:disable'.PHP_EOL.'// @phpcs:ignore',
+ 'after' => '// @phpcs:enable',
+ ],
+ 'disable/enable: hash comment, next line nested single line suppression' => [
+ 'before' => '# @phpcs:disable'.PHP_EOL.'# @phpcs:ignore',
+ 'after' => '# @phpcs:enable',
+ ],
+ 'old style: slash comment, next line nested single line suppression' => [
+ 'before' => '// @codingStandardsIgnoreStart'.PHP_EOL.'// @codingStandardsIgnoreLine',
+ 'after' => '// @codingStandardsIgnoreEnd',
+ ],
+ ];
- }//end testNestedSuppressLine()
+ }//end dataNestedSuppressLine()
/**
* Test suppressing a scope opener.
*
+ * @param string $before Annotation to place before the scope opener.
+ * @param string $after Annotation to place after the scope opener.
+ * @param int $expectedErrors Optional. Number of errors expected.
+ * Defaults to 0.
+ *
+ * @dataProvider dataSuppressScope
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testSuppressScope()
+ public function testSuppressScope($before, $after, $expectedErrors=0)
{
- $config = new Config();
- $config->standards = ['PEAR'];
- $config->sniffs = ['PEAR.NamingConventions.ValidVariableName'];
-
- $ruleset = new Ruleset($config);
-
- // Process without suppression.
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
- $file->process();
+ static $config, $ruleset;
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with suppression.
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
- $file->process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['PEAR'];
+ $config->sniffs = ['PEAR.Functions.FunctionDeclaration'];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ $ruleset = new Ruleset($config);
+ }
- // Process with suppression.
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
- $file->process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with suppression (deprecated syntax).
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
+ $content = 'foo();
+ }
+}
+EOD;
$file = new DummyFile($content, $ruleset, $config);
$file->process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with a docblock suppression.
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
-
- // Process with a docblock @ suppression.
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ }//end testSuppressScope()
- // Process with a docblock suppression (deprecated syntax).
- $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}';
- $file = new DummyFile($content, $ruleset, $config);
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
+ /**
+ * Data provider.
+ *
+ * @see testSuppressScope()
+ *
+ * @return array
+ */
+ public function dataSuppressScope()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'after' => '',
+ 'expectedErrors' => 1,
+ ],
+
+ // Process with suppression.
+ 'disable/enable: slash comment' => [
+ 'before' => '//phpcs:disable',
+ 'after' => '//phpcs:enable',
+ ],
+ 'disable/enable: slash comment, with @' => [
+ 'before' => '//@phpcs:disable',
+ 'after' => '//@phpcs:enable',
+ ],
+ 'disable/enable: hash comment' => [
+ 'before' => '#phpcs:disable',
+ 'after' => '#phpcs:enable',
+ ],
+ 'disable/enable: single line docblock comment' => [
+ 'before' => '/** phpcs:disable */',
+ 'after' => '/** phpcs:enable */',
+ ],
+ 'disable/enable: single line docblock comment, with @' => [
+ 'before' => '/** @phpcs:disable */',
+ 'after' => '/** @phpcs:enable */',
+ ],
+
+ // Deprecated syntax.
+ 'old style: start/end, slash comment' => [
+ 'before' => '//@codingStandardsIgnoreStart',
+ 'after' => '//@codingStandardsIgnoreEnd',
+ ],
+ 'old style: start/end, single line docblock comment' => [
+ 'before' => '/** @codingStandardsIgnoreStart */',
+ 'after' => '/** @codingStandardsIgnoreEnd */',
+ ],
+ ];
- }//end testSuppressScope()
+ }//end dataSuppressScope()
/**
* Test suppressing a whole file.
*
+ * @param string $before Annotation to place before the code.
+ * @param string $after Optional. Annotation to place after the code.
+ * Defaults to an empty string.
+ * @param int $expectedWarnings Optional. Number of warnings expected.
+ * Defaults to 0.
+ *
+ * @dataProvider dataSuppressFile
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testSuppressFile()
+ public function testSuppressFile($before, $after='', $expectedWarnings=0)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = ['Generic.Commenting.Todo'];
-
- $ruleset = new Ruleset($config);
-
- // Process without suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Process with suppression.
- $content = 'process();
+ static $config, $ruleset;
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = ['Generic.Commenting.Todo'];
- // Process with @ suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with suppression (deprecated syntax).
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
+ $ruleset = new Ruleset($config);
+ }
- // Process mixed case.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process late comment.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process late comment (deprecated syntax).
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with a block comment suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with a multi-line block comment suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with a block comment suppression (deprecated syntax).
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with a multi-line block comment suppression (deprecated syntax).
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with docblock suppression.
- $content = 'process();
-
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Process with docblock suppression (deprecated syntax).
- $content = 'process();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
}//end testSuppressFile()
/**
- * Test disabling specific sniffs.
+ * Data provider.
*
- * @return void
+ * @see testSuppressFile()
+ *
+ * @return array
*/
- public function testDisableSelected()
+ public function dataSuppressFile()
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = [
- 'Generic.PHP.LowerCaseConstant',
- 'Generic.Commenting.Todo',
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'after' => '',
+ 'expectedErrors' => 1,
+ ],
+
+ // Process with suppression.
+ 'ignoreFile: start of file, slash comment' => ['before' => '// phpcs:ignoreFile'],
+ 'ignoreFile: start of file, slash comment, with @' => ['before' => '// @phpcs:ignoreFile'],
+ 'ignoreFile: start of file, slash comment, mixed case' => ['before' => '// PHPCS:Ignorefile'],
+ 'ignoreFile: start of file, hash comment' => ['before' => '# phpcs:ignoreFile'],
+ 'ignoreFile: start of file, hash comment, with @' => ['before' => '# @phpcs:ignoreFile'],
+ 'ignoreFile: start of file, single-line star comment' => ['before' => '/* phpcs:ignoreFile */'],
+ 'ignoreFile: start of file, multi-line star comment' => [
+ 'before' => '/*'.PHP_EOL.' phpcs:ignoreFile'.PHP_EOL.' */',
+ ],
+ 'ignoreFile: start of file, single-line docblock comment' => ['before' => '/** phpcs:ignoreFile */'],
+
+ // Process late comment.
+ 'ignoreFile: late comment, slash comment' => [
+ 'before' => '',
+ 'after' => '// phpcs:ignoreFile',
+ ],
+
+ // Deprecated syntax.
+ 'old style: start of file, slash comment' => ['before' => '// @codingStandardsIgnoreFile'],
+ 'old style: start of file, single-line star comment' => ['before' => '/* @codingStandardsIgnoreFile */'],
+ 'old style: start of file, multi-line star comment' => [
+ 'before' => '/*'.PHP_EOL.' @codingStandardsIgnoreFile'.PHP_EOL.' */',
+ ],
+ 'old style: start of file, single-line docblock comment' => ['before' => '/** @codingStandardsIgnoreFile */'],
+
+ // Deprecated syntax, late comment.
+ 'old style: late comment, slash comment' => [
+ 'before' => '',
+ 'after' => '// @codingStandardsIgnoreFile',
+ ],
];
- $ruleset = new Ruleset($config);
+ }//end dataSuppressFile()
- // Suppress a single sniff.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress multiple sniffs.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress adding sniffs.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress a category of sniffs.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress a whole standard.
- $content = 'process();
+ /**
+ * Test disabling specific sniffs.
+ *
+ * @param string $before Annotation to place before the code.
+ * @param int $expectedErrors Optional. Number of errors expected.
+ * Defaults to 0.
+ * @param int $expectedWarnings Optional. Number of warnings expected.
+ * Defaults to 0.
+ *
+ * @dataProvider dataDisableSelected
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
+ * @return void
+ */
+ public function testDisableSelected($before, $expectedErrors=0, $expectedWarnings=0)
+ {
+ static $config, $ruleset;
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress using docblocks.
- $content = 'process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = [
+ 'Generic.PHP.LowerCaseConstant',
+ 'Generic.Commenting.Todo',
+ ];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- $content = 'process();
+ $ruleset = new Ruleset($config);
+ }
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress wrong category using docblocks.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- $content = 'process();
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
}//end testDisableSelected()
/**
- * Test re-enabling specific sniffs that have been disabled.
+ * Data provider.
*
- * @return void
+ * @see testDisableSelected()
+ *
+ * @return array
*/
- public function testEnableSelected()
+ public function dataDisableSelected()
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = [
- 'Generic.PHP.LowerCaseConstant',
- 'Generic.Commenting.Todo',
+ return [
+ // Single sniff.
+ 'disable: single sniff' => [
+ 'before' => '// phpcs:disable Generic.Commenting.Todo',
+ 'expectedErrors' => 1,
+ ],
+ 'disable: single sniff with reason' => [
+ 'before' => '# phpcs:disable Generic.Commenting.Todo -- for reasons',
+ 'expectedErrors' => 1,
+ ],
+ 'disable: single sniff, docblock' => [
+ 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.Commenting.Todo'.PHP_EOL.' */ ',
+ 'expectedErrors' => 1,
+ ],
+ 'disable: single sniff, docblock, with @' => [
+ 'before' => '/**'.PHP_EOL.' * @phpcs:disable Generic.Commenting.Todo'.PHP_EOL.' */ ',
+ 'expectedErrors' => 1,
+ ],
+
+ // Multiple sniffs.
+ 'disable: multiple sniffs in one comment' => ['before' => '// phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant'],
+ 'disable: multiple sniff in multiple comments' => [
+ 'before' => '// phpcs:disable Generic.Commenting.Todo'.PHP_EOL.'// phpcs:disable Generic.PHP.LowerCaseConstant',
+ ],
+
+ // Selectiveness variations.
+ 'disable: complete category' => [
+ 'before' => '// phpcs:disable Generic.Commenting',
+ 'expectedErrors' => 1,
+ ],
+ 'disable: whole standard' => ['before' => '// phpcs:disable Generic'],
+ 'disable: single errorcode' => [
+ 'before' => '# @phpcs:disable Generic.Commenting.Todo.TaskFound',
+ 'expectedErrors' => 1,
+ ],
+ 'disable: single errorcode and a category' => ['before' => '// phpcs:disable Generic.PHP.LowerCaseConstant.Found,Generic.Commenting'],
+
+ // Wrong category/sniff/code.
+ 'disable: wrong error code and category' => [
+ 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.PHP.LowerCaseConstant.Upper,Generic.Comments'.PHP_EOL.' */ ',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: wrong category, docblock' => [
+ 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.Files'.PHP_EOL.' */ ',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: wrong category, docblock, with @' => [
+ 'before' => '/**'.PHP_EOL.' * @phpcs:disable Generic.Files'.PHP_EOL.' */ ',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
];
- $ruleset = new Ruleset($config);
+ }//end dataDisableSelected()
- // Suppress a single sniff and re-enable.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress multiple sniffs and re-enable.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress multiple sniffs and re-enable one.
- $content = 'process();
-
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a category of sniffs and re-enable.
- $content = 'process();
+ /**
+ * Test re-enabling specific sniffs that have been disabled.
+ *
+ * @param string $code Code pattern to check.
+ * @param int $expectedErrors Number of errors expected.
+ * @param int $expectedWarnings Number of warnings expected.
+ *
+ * @dataProvider dataEnableSelected
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
+ * @return void
+ */
+ public function testEnableSelected($code, $expectedErrors, $expectedWarnings)
+ {
+ static $config, $ruleset;
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a whole standard and re-enable.
- $content = 'process();
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = [
+ 'Generic.PHP.LowerCaseConstant',
+ 'Generic.Commenting.Todo',
+ ];
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a whole standard and re-enable a category.
- $content = 'process();
+ $ruleset = new Ruleset($config);
+ }
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a category and re-enable a whole standard.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a sniff and re-enable a category.
- $content = 'process();
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a whole standard and re-enable a sniff.
- $content = 'process();
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a whole standard and re-enable and re-disable a sniff.
- $content = 'process();
+ }//end testEnableSelected()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(2, $numWarnings);
- $this->assertCount(2, $warnings);
-
- // Suppress a whole standard and re-enable 2 specific sniffs independently.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(2, $numWarnings);
- $this->assertCount(2, $warnings);
+ /**
+ * Data provider.
+ *
+ * @see testEnableSelected()
+ *
+ * @return array
+ */
+ public function dataEnableSelected()
+ {
+ return [
+ 'disable/enable: a single sniff' => [
+ 'code' => '
+ // phpcs:disable Generic.Commenting.Todo
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable/enable: multiple sniffs' => [
+ 'code' => '
+ // phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant
+ //TODO: write some code
+ $var = FALSE;',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: multiple sniffs; enable: one' => [
+ 'code' => '
+ # phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant
+ $var = FALSE;
+ //TODO: write some code
+ # phpcs:enable Generic.Commenting.Todo
+ //TODO: write some code
+ $var = FALSE;',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable/enable: complete category' => [
+ 'code' => '
+ // phpcs:disable Generic.Commenting
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable/enable: whole standard' => [
+ 'code' => '
+ // phpcs:disable Generic
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic
+ //TODO: write some code',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: whole standard; enable: category from the standard' => [
+ 'code' => '
+ // phpcs:disable Generic
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting
+ //TODO: write some code',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: a category; enable: the whole standard containing the category' => [
+ 'code' => '
+ # phpcs:disable Generic.Commenting
+ $var = FALSE;
+ //TODO: write some code
+ # phpcs:enable Generic
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: single sniff; enable: the category containing the sniff' => [
+ 'code' => '
+ // phpcs:disable Generic.Commenting.Todo
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: whole standard; enable: single sniff from the standard' => [
+ 'code' => '
+ // phpcs:disable Generic
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo
+ //TODO: write some code',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: whole standard; enable: single sniff from the standard; disable: that same sniff; enable: everything' => [
+ 'code' => '
+ // phpcs:disable Generic
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo
+ //TODO: write some code
+ // phpcs:disable Generic.Commenting.Todo
+ //TODO: write some code
+ // phpcs:enable
+ //TODO: write some code',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 2,
+ ],
+ 'disable: whole standard; enable: single sniff from the standard; enable: other sniff from the standard' => [
+ 'code' => '
+ // phpcs:disable Generic
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo
+ //TODO: write some code
+ $var = FALSE;
+ // phpcs:enable Generic.PHP.LowerCaseConstant
+ //TODO: write some code
+ $var = FALSE;',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 2,
+ ],
+ ];
- }//end testEnableSelected()
+ }//end dataEnableSelected()
/**
* Test ignoring specific sniffs.
*
+ * @param string $before Annotation to place before the code.
+ * @param int $expectedErrors Number of errors expected.
+ * @param int $expectedWarnings Number of warnings expected.
+ *
+ * @dataProvider dataIgnoreSelected
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testIgnoreSelected()
+ public function testIgnoreSelected($before, $expectedErrors, $expectedWarnings)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = [
- 'Generic.PHP.LowerCaseConstant',
- 'Generic.Commenting.Todo',
- ];
+ static $config, $ruleset;
- $ruleset = new Ruleset($config);
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = [
+ 'Generic.PHP.LowerCaseConstant',
+ 'Generic.Commenting.Todo',
+ ];
- // No suppression.
- $content = 'process();
+ $ruleset = new Ruleset($config);
+ }
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
- $this->assertEquals(2, $numWarnings);
- $this->assertCount(2, $warnings);
-
- // Suppress a single sniff.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress multiple sniffs.
- $content = 'process();
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Add to suppression.
- $content = 'process();
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress a category of sniffs.
- $content = 'process();
+ }//end testIgnoreSelected()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a whole standard.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
+ /**
+ * Data provider.
+ *
+ * @see testIgnoreSelected()
+ *
+ * @return array
+ */
+ public function dataIgnoreSelected()
+ {
+ return [
+ 'no suppression' => [
+ 'before' => '',
+ 'expectedErrors' => 2,
+ 'expectedWarnings' => 2,
+ ],
+
+ // With suppression.
+ 'ignore: single sniff' => [
+ 'before' => '// phpcs:ignore Generic.Commenting.Todo',
+ 'expectedErrors' => 2,
+ 'expectedWarnings' => 1,
+ ],
+ 'ignore: multiple sniffs' => [
+ 'before' => '// phpcs:ignore Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: single sniff; ignore: single sniff' => [
+ 'before' => '// phpcs:disable Generic.Commenting.Todo'.PHP_EOL.'// phpcs:ignore Generic.PHP.LowerCaseConstant',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 0,
+ ],
+ 'ignore: category of sniffs' => [
+ 'before' => '# phpcs:ignore Generic.Commenting',
+ 'expectedErrors' => 2,
+ 'expectedWarnings' => 1,
+ ],
+ 'ignore: whole standard' => [
+ 'before' => '// phpcs:ignore Generic',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ ];
- }//end testIgnoreSelected()
+ }//end dataIgnoreSelected()
/**
* Test ignoring specific sniffs.
*
+ * @param string $code Code pattern to check.
+ * @param int $expectedErrors Number of errors expected.
+ * @param int $expectedWarnings Number of warnings expected.
+ *
+ * @dataProvider dataCommenting
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
* @return void
*/
- public function testCommenting()
+ public function testCommenting($code, $expectedErrors, $expectedWarnings)
{
- $config = new Config();
- $config->standards = ['Generic'];
- $config->sniffs = [
- 'Generic.PHP.LowerCaseConstant',
- 'Generic.Commenting.Todo',
- ];
+ static $config, $ruleset;
- $ruleset = new Ruleset($config);
+ if (isset($config, $ruleset) === false) {
+ $config = new Config();
+ $config->standards = ['Generic'];
+ $config->sniffs = [
+ 'Generic.PHP.LowerCaseConstant',
+ 'Generic.Commenting.Todo',
+ ];
- // Suppress a single sniff.
- $content = 'process();
+ $ruleset = new Ruleset($config);
+ }
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a single sniff and re-enable.
- $content = 'process();
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Suppress a single sniff using block comments.
- $content = 'process();
+ $this->assertSame($expectedErrors, $file->getErrorCount());
+ $this->assertCount($expectedErrors, $file->getErrors());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(1, $numErrors);
- $this->assertCount(1, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
-
- // Suppress a single sniff with a multi-line comment.
- $content = 'process();
+ $this->assertSame($expectedWarnings, $file->getWarningCount());
+ $this->assertCount($expectedWarnings, $file->getWarnings());
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(2, $numErrors);
- $this->assertCount(2, $errors);
- $this->assertEquals(1, $numWarnings);
- $this->assertCount(1, $warnings);
-
- // Ignore an enable before a disable.
- $content = 'process();
+ }//end testCommenting()
- $errors = $file->getErrors();
- $numErrors = $file->getErrorCount();
- $warnings = $file->getWarnings();
- $numWarnings = $file->getWarningCount();
- $this->assertEquals(0, $numErrors);
- $this->assertCount(0, $errors);
- $this->assertEquals(0, $numWarnings);
- $this->assertCount(0, $warnings);
- }//end testCommenting()
+ /**
+ * Data provider.
+ *
+ * @see testCommenting()
+ *
+ * @return array
+ */
+ public function dataCommenting()
+ {
+ return [
+ 'ignore: single sniff' => [
+ 'code' => '
+ // phpcs:ignore Generic.Commenting.Todo -- Because reasons
+ $var = FALSE; //TODO: write some code
+ $var = FALSE; //TODO: write some code',
+ 'expectedErrors' => 2,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: single sniff; enable: same sniff - test whitespace handling around reason delimiter' => [
+ 'code' => '
+ // phpcs:disable Generic.Commenting.Todo --Because reasons
+ $var = FALSE;
+ //TODO: write some code
+ // phpcs:enable Generic.Commenting.Todo -- Because reasons
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 1,
+ ],
+ 'disable: single sniff, multi-line comment' => [
+ 'code' => '
+ /*
+ Disable some checks
+ phpcs:disable Generic.Commenting.Todo
+ */
+ $var = FALSE;
+ //TODO: write some code',
+ 'expectedErrors' => 1,
+ 'expectedWarnings' => 0,
+ ],
+ 'ignore: single sniff, multi-line slash comment' => [
+ 'code' => '
+ // Turn off a check for the next line of code.
+ // phpcs:ignore Generic.Commenting.Todo
+ $var = FALSE; //TODO: write some code
+ $var = FALSE; //TODO: write some code',
+ 'expectedErrors' => 2,
+ 'expectedWarnings' => 1,
+ ],
+ 'enable before disable, sniff not in standard' => [
+ 'code' => '
+ // phpcs:enable Generic.PHP.NoSilencedErrors -- Because reasons
+ $var = @delete( $filename );
+ ',
+ 'expectedErrors' => 0,
+ 'expectedWarnings' => 0,
+ ],
+ ];
+
+ }//end dataCommenting()
}//end class
diff --git a/tests/Core/File/FindEndOfStatementTest.inc b/tests/Core/File/FindEndOfStatementTest.inc
index 1d72d7d741..8351679891 100644
--- a/tests/Core/File/FindEndOfStatementTest.inc
+++ b/tests/Core/File/FindEndOfStatementTest.inc
@@ -39,6 +39,8 @@ $a = [
/* testStaticArrowFunction */
static fn ($a) => $a;
+return 0;
+
/* testArrowFunctionReturnValue */
fn(): array => [a($a, $b)];
@@ -52,4 +54,52 @@ $foo = foo(
fn() => [$row[0], $row[3]]
);
-return 0;
+$match = match ($a) {
+ /* testMatchCase */
+ 1 => 'foo',
+ /* testMatchDefault */
+ default => 'bar'
+};
+
+$match = match ($a) {
+ /* testMatchMultipleCase */
+ 1, 2, => $a * $b,
+ /* testMatchDefaultComma */
+ default, => 'something'
+};
+
+match ($pressedKey) {
+ /* testMatchFunctionCall */
+ Key::RETURN_ => save($value, $user)
+};
+
+$result = match (true) {
+ /* testMatchFunctionCallArm */
+ str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en',
+ str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr',
+ default => 'pl'
+};
+
+/* testMatchClosure */
+$result = match ($key) {
+ 1 => function($a, $b) {},
+ 2 => function($b, $c) {},
+};
+
+/* testMatchArray */
+$result = match ($key) {
+ 1 => [1,2,3],
+ 2 => [1 => one(), 2 => two()],
+};
+
+/* testNestedMatch */
+$result = match ($key) {
+ 1 => match ($key) {
+ 1 => 'one',
+ 2 => 'two',
+ },
+ 2 => match ($key) {
+ 1 => 'two',
+ 2 => 'one',
+ },
+};
diff --git a/tests/Core/File/FindEndOfStatementTest.php b/tests/Core/File/FindEndOfStatementTest.php
index 1fbc8a6871..7bff26b566 100644
--- a/tests/Core/File/FindEndOfStatementTest.php
+++ b/tests/Core/File/FindEndOfStatementTest.php
@@ -237,4 +237,179 @@ public function testArrowFunctionWithArrayAsArgument()
}//end testArrowFunctionWithArrayAsArgument()
+ /**
+ * Test simple match expression case.
+ *
+ * @return void
+ */
+ public function testMatchCase()
+ {
+ $start = $this->getTargetToken('/* testMatchCase */', T_LNUMBER);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 5), $found);
+
+ $start = $this->getTargetToken('/* testMatchCase */', T_CONSTANT_ENCAPSED_STRING);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 1), $found);
+
+ }//end testMatchCase()
+
+
+ /**
+ * Test simple match expression default case.
+ *
+ * @return void
+ */
+ public function testMatchDefault()
+ {
+ $start = $this->getTargetToken('/* testMatchDefault */', T_MATCH_DEFAULT);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 4), $found);
+
+ $start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame($start, $found);
+
+ }//end testMatchDefault()
+
+
+ /**
+ * Test multiple comma-separated match expression case values.
+ *
+ * @return void
+ */
+ public function testMatchMultipleCase()
+ {
+ $start = $this->getTargetToken('/* testMatchMultipleCase */', T_LNUMBER);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+ $this->assertSame(($start + 13), $found);
+
+ $start += 6;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+ $this->assertSame(($start + 7), $found);
+
+ }//end testMatchMultipleCase()
+
+
+ /**
+ * Test match expression default case with trailing comma.
+ *
+ * @return void
+ */
+ public function testMatchDefaultComma()
+ {
+ $start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_DEFAULT);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 5), $found);
+
+ }//end testMatchDefaultComma()
+
+
+ /**
+ * Test match expression with function call.
+ *
+ * @return void
+ */
+ public function testMatchFunctionCall()
+ {
+ $start = $this->getTargetToken('/* testMatchFunctionCall */', T_STRING);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 12), $found);
+
+ $start += 8;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 1), $found);
+
+ }//end testMatchFunctionCall()
+
+
+ /**
+ * Test match expression with function call in the arm.
+ *
+ * @return void
+ */
+ public function testMatchFunctionCallArm()
+ {
+ // Check the first case.
+ $start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_STRING);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 21), $found);
+
+ // Check the second case.
+ $start += 24;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 21), $found);
+
+ }//end testMatchFunctionCallArm()
+
+
+ /**
+ * Test match expression with closure.
+ *
+ * @return void
+ */
+ public function testMatchClosure()
+ {
+ $start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 14), $found);
+
+ $start += 17;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 14), $found);
+
+ }//end testMatchClosure()
+
+
+ /**
+ * Test match expression with array declaration.
+ *
+ * @return void
+ */
+ public function testMatchArray()
+ {
+ $start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 11), $found);
+
+ $start += 14;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 22), $found);
+
+ }//end testMatchArray()
+
+
+ /**
+ * Test nested match expressions.
+ *
+ * @return void
+ */
+ public function testNestedMatch()
+ {
+ $start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER);
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 30), $found);
+
+ $start += 21;
+ $found = self::$phpcsFile->findEndOfStatement($start);
+
+ $this->assertSame(($start + 5), $found);
+
+ }//end testNestedMatch()
+
+
}//end class
diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.inc b/tests/Core/File/FindImplementedInterfaceNamesTest.inc
index 3885b27e1d..44c0f64321 100644
--- a/tests/Core/File/FindImplementedInterfaceNamesTest.inc
+++ b/tests/Core/File/FindImplementedInterfaceNamesTest.inc
@@ -24,3 +24,12 @@ class testFECNClassThatExtendsAndImplements extends testFECNClass implements Int
/* testClassThatImplementsAndExtends */
class testFECNClassThatImplementsAndExtends implements \InterfaceA, InterfaceB extends testFECNClass {}
+
+/* testBackedEnumWithoutImplements */
+enum Suit:string {}
+
+/* testEnumImplements */
+enum Suit implements Colorful {}
+
+/* testBackedEnumImplements */
+enum Suit: string implements Colorful, \Deck {}
diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.php b/tests/Core/File/FindImplementedInterfaceNamesTest.php
index 834e083202..2395032897 100644
--- a/tests/Core/File/FindImplementedInterfaceNamesTest.php
+++ b/tests/Core/File/FindImplementedInterfaceNamesTest.php
@@ -27,7 +27,7 @@ class FindImplementedInterfaceNamesTest extends AbstractMethodUnitTest
*/
public function testFindImplementedInterfaceNames($identifier, $expected)
{
- $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE]);
+ $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE, T_ENUM]);
$result = self::$phpcsFile->findImplementedInterfaceNames($OOToken);
$this->assertSame($expected, $result);
@@ -81,6 +81,21 @@ public function dataImplementedInterface()
'InterfaceB',
],
],
+ [
+ '/* testBackedEnumWithoutImplements */',
+ false,
+ ],
+ [
+ '/* testEnumImplements */',
+ ['Colorful'],
+ ],
+ [
+ '/* testBackedEnumImplements */',
+ [
+ 'Colorful',
+ '\Deck',
+ ],
+ ],
];
}//end dataImplementedInterface()
diff --git a/tests/Core/File/FindStartOfStatementTest.inc b/tests/Core/File/FindStartOfStatementTest.inc
new file mode 100644
index 0000000000..ce9dfad3f9
--- /dev/null
+++ b/tests/Core/File/FindStartOfStatementTest.inc
@@ -0,0 +1,123 @@
+ $foo + $bar, 'b' => true];
+
+/* testUseGroup */
+use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
+
+$a = [
+ /* testArrowFunctionArrayValue */
+ 'a' => fn() => return 1,
+ 'b' => fn() => return 1,
+];
+
+/* testStaticArrowFunction */
+static fn ($a) => $a;
+
+/* testArrowFunctionReturnValue */
+fn(): array => [a($a, $b)];
+
+/* testArrowFunctionAsArgument */
+$foo = foo(
+ fn() => bar()
+);
+
+/* testArrowFunctionWithArrayAsArgument */
+$foo = foo(
+ fn() => [$row[0], $row[3]]
+);
+
+$match = match ($a) {
+ /* testMatchCase */
+ 1 => 'foo',
+ /* testMatchDefault */
+ default => 'bar'
+};
+
+$match = match ($a) {
+ /* testMatchMultipleCase */
+ 1, 2, => $a * $b,
+ /* testMatchDefaultComma */
+ default, => 'something'
+};
+
+match ($pressedKey) {
+ /* testMatchFunctionCall */
+ Key::RETURN_ => save($value, $user)
+};
+
+$result = match (true) {
+ /* testMatchFunctionCallArm */
+ str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en',
+ str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr',
+ default => 'pl'
+};
+
+/* testMatchClosure */
+$result = match ($key) {
+ 1 => function($a, $b) {},
+ 2 => function($b, $c) {},
+};
+
+/* testMatchArray */
+$result = match ($key) {
+ 1 => [1,2,3],
+ 2 => [1 => one($a, $b), 2 => two($b, $c)],
+ 3 => [],
+};
+
+/* testNestedMatch */
+$result = match ($key) {
+ 1 => match ($key) {
+ 1 => 'one',
+ 2 => 'two',
+ },
+ 2 => match ($key) {
+ 1 => 'two',
+ 2 => 'one',
+ },
+};
+
+return 0;
+
+/* testOpenTag */
+?>
+Test
+', foo(), '';
+
+/* testOpenTagWithEcho */
+?>
+Test
+= '', foo(), '
';
diff --git a/tests/Core/File/FindStartOfStatementTest.php b/tests/Core/File/FindStartOfStatementTest.php
new file mode 100644
index 0000000000..ff859eca1c
--- /dev/null
+++ b/tests/Core/File/FindStartOfStatementTest.php
@@ -0,0 +1,503 @@
+
+ * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\File;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class FindStartOfStatementTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test a simple assignment.
+ *
+ * @return void
+ */
+ public function testSimpleAssignment()
+ {
+ $start = $this->getTargetToken('/* testSimpleAssignment */', T_SEMICOLON);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 5), $found);
+
+ }//end testSimpleAssignment()
+
+
+ /**
+ * Test a function call.
+ *
+ * @return void
+ */
+ public function testFunctionCall()
+ {
+ $start = $this->getTargetToken('/* testFunctionCall */', T_CLOSE_PARENTHESIS);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 6), $found);
+
+ }//end testFunctionCall()
+
+
+ /**
+ * Test a function call.
+ *
+ * @return void
+ */
+ public function testFunctionCallArgument()
+ {
+ $start = $this->getTargetToken('/* testFunctionCallArgument */', T_VARIABLE, '$b');
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame($start, $found);
+
+ }//end testFunctionCallArgument()
+
+
+ /**
+ * Test a direct call to a control structure.
+ *
+ * @return void
+ */
+ public function testControlStructure()
+ {
+ $start = $this->getTargetToken('/* testControlStructure */', T_CLOSE_CURLY_BRACKET);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 6), $found);
+
+ }//end testControlStructure()
+
+
+ /**
+ * Test the assignment of a closure.
+ *
+ * @return void
+ */
+ public function testClosureAssignment()
+ {
+ $start = $this->getTargetToken('/* testClosureAssignment */', T_CLOSE_CURLY_BRACKET);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 12), $found);
+
+ }//end testClosureAssignment()
+
+
+ /**
+ * Test using a heredoc in a function argument.
+ *
+ * @return void
+ */
+ public function testHeredocFunctionArg()
+ {
+ // Find the start of the function.
+ $start = $this->getTargetToken('/* testHeredocFunctionArg */', T_SEMICOLON);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 10), $found);
+
+ // Find the start of the heredoc.
+ $start -= 4;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 4), $found);
+
+ // Find the start of the last arg.
+ $start += 2;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame($start, $found);
+
+ }//end testHeredocFunctionArg()
+
+
+ /**
+ * Test parts of a switch statement.
+ *
+ * @return void
+ */
+ public function testSwitch()
+ {
+ // Find the start of the switch.
+ $start = $this->getTargetToken('/* testSwitch */', T_CLOSE_CURLY_BRACKET);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 47), $found);
+
+ // Find the start of default case.
+ $start -= 5;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 6), $found);
+
+ // Find the start of the second case.
+ $start -= 12;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 5), $found);
+
+ // Find the start of the first case.
+ $start -= 13;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 8), $found);
+
+ // Test inside the first case.
+ $start--;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 1), $found);
+
+ }//end testSwitch()
+
+
+ /**
+ * Test statements that are array values.
+ *
+ * @return void
+ */
+ public function testStatementAsArrayValue()
+ {
+ // Test short array syntax.
+ $start = $this->getTargetToken('/* testStatementAsArrayValue */', T_STRING, 'Datetime');
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 2), $found);
+
+ // Test long array syntax.
+ $start += 12;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 2), $found);
+
+ // Test same statement outside of array.
+ $start++;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 9), $found);
+
+ // Test with an array index.
+ $start += 17;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 5), $found);
+
+ }//end testStatementAsArrayValue()
+
+
+ /**
+ * Test a use group.
+ *
+ * @return void
+ */
+ public function testUseGroup()
+ {
+ $start = $this->getTargetToken('/* testUseGroup */', T_SEMICOLON);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 23), $found);
+
+ }//end testUseGroup()
+
+
+ /**
+ * Test arrow function as array value.
+ *
+ * @return void
+ */
+ public function testArrowFunctionArrayValue()
+ {
+ $start = $this->getTargetToken('/* testArrowFunctionArrayValue */', T_COMMA);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 9), $found);
+
+ }//end testArrowFunctionArrayValue()
+
+
+ /**
+ * Test static arrow function.
+ *
+ * @return void
+ */
+ public function testStaticArrowFunction()
+ {
+ $start = $this->getTargetToken('/* testStaticArrowFunction */', T_SEMICOLON);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 11), $found);
+
+ }//end testStaticArrowFunction()
+
+
+ /**
+ * Test arrow function with return value.
+ *
+ * @return void
+ */
+ public function testArrowFunctionReturnValue()
+ {
+ $start = $this->getTargetToken('/* testArrowFunctionReturnValue */', T_SEMICOLON);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 18), $found);
+
+ }//end testArrowFunctionReturnValue()
+
+
+ /**
+ * Test arrow function used as a function argument.
+ *
+ * @return void
+ */
+ public function testArrowFunctionAsArgument()
+ {
+ $start = $this->getTargetToken('/* testArrowFunctionAsArgument */', T_FN);
+ $start += 8;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 8), $found);
+
+ }//end testArrowFunctionAsArgument()
+
+
+ /**
+ * Test arrow function with arrays used as a function argument.
+ *
+ * @return void
+ */
+ public function testArrowFunctionWithArrayAsArgument()
+ {
+ $start = $this->getTargetToken('/* testArrowFunctionWithArrayAsArgument */', T_FN);
+ $start += 17;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 17), $found);
+
+ }//end testArrowFunctionWithArrayAsArgument()
+
+
+ /**
+ * Test simple match expression case.
+ *
+ * @return void
+ */
+ public function testMatchCase()
+ {
+ $start = $this->getTargetToken('/* testMatchCase */', T_COMMA);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 1), $found);
+
+ }//end testMatchCase()
+
+
+ /**
+ * Test simple match expression default case.
+ *
+ * @return void
+ */
+ public function testMatchDefault()
+ {
+ $start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING, "'bar'");
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame($start, $found);
+
+ }//end testMatchDefault()
+
+
+ /**
+ * Test multiple comma-separated match expression case values.
+ *
+ * @return void
+ */
+ public function testMatchMultipleCase()
+ {
+ $start = $this->getTargetToken('/* testMatchMultipleCase */', T_MATCH_ARROW);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 6), $found);
+
+ $start += 6;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 4), $found);
+
+ }//end testMatchMultipleCase()
+
+
+ /**
+ * Test match expression default case with trailing comma.
+ *
+ * @return void
+ */
+ public function testMatchDefaultComma()
+ {
+ $start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_ARROW);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 3), $found);
+
+ $start += 2;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame($start, $found);
+
+ }//end testMatchDefaultComma()
+
+
+ /**
+ * Test match expression with function call.
+ *
+ * @return void
+ */
+ public function testMatchFunctionCall()
+ {
+ $start = $this->getTargetToken('/* testMatchFunctionCall */', T_CLOSE_PARENTHESIS);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 6), $found);
+
+ }//end testMatchFunctionCall()
+
+
+ /**
+ * Test match expression with function call in the arm.
+ *
+ * @return void
+ */
+ public function testMatchFunctionCallArm()
+ {
+ // Check the first case.
+ $start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_MATCH_ARROW);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 18), $found);
+
+ // Check the second case.
+ $start += 24;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 18), $found);
+
+ }//end testMatchFunctionCallArm()
+
+
+ /**
+ * Test match expression with closure.
+ *
+ * @return void
+ */
+ public function testMatchClosure()
+ {
+ $start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER);
+ $start += 14;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 10), $found);
+
+ $start += 17;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 10), $found);
+
+ }//end testMatchClosure()
+
+
+ /**
+ * Test match expression with array declaration.
+ *
+ * @return void
+ */
+ public function testMatchArray()
+ {
+ // Start of first case statement.
+ $start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER);
+ $found = self::$phpcsFile->findStartOfStatement($start);
+ $this->assertSame($start, $found);
+
+ // Comma after first statement.
+ $start += 11;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+ $this->assertSame(($start - 7), $found);
+
+ // Start of second case statement.
+ $start += 3;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+ $this->assertSame($start, $found);
+
+ // Comma after first statement.
+ $start += 30;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+ $this->assertSame(($start - 26), $found);
+
+ }//end testMatchArray()
+
+
+ /**
+ * Test nested match expressions.
+ *
+ * @return void
+ */
+ public function testNestedMatch()
+ {
+ $start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER);
+ $start += 30;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 26), $found);
+
+ $start -= 4;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 1), $found);
+
+ $start -= 3;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 2), $found);
+
+ }//end testNestedMatch()
+
+
+ /**
+ * Test PHP open tag.
+ *
+ * @return void
+ */
+ public function testOpenTag()
+ {
+ $start = $this->getTargetToken('/* testOpenTag */', T_OPEN_TAG);
+ $start += 2;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 1), $found);
+
+ }//end testOpenTag()
+
+
+ /**
+ * Test PHP short open echo tag.
+ *
+ * @return void
+ */
+ public function testOpenTagWithEcho()
+ {
+ $start = $this->getTargetToken('/* testOpenTagWithEcho */', T_OPEN_TAG_WITH_ECHO);
+ $start += 3;
+ $found = self::$phpcsFile->findStartOfStatement($start);
+
+ $this->assertSame(($start - 1), $found);
+
+ }//end testOpenTagWithEcho()
+
+
+}//end class
diff --git a/tests/Core/File/GetClassPropertiesTest.inc b/tests/Core/File/GetClassPropertiesTest.inc
new file mode 100644
index 0000000000..2490a09657
--- /dev/null
+++ b/tests/Core/File/GetClassPropertiesTest.inc
@@ -0,0 +1,58 @@
+
+ * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\File;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class GetClassPropertiesTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test receiving an expected exception when a non class token is passed.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param array $tokenType The type of token to look for after the marker.
+ *
+ * @dataProvider dataNotAClassException
+ *
+ * @expectedException PHP_CodeSniffer\Exceptions\RuntimeException
+ * @expectedExceptionMessage $stackPtr must be of type T_CLASS
+ *
+ * @return void
+ */
+ public function testNotAClassException($testMarker, $tokenType)
+ {
+ $target = $this->getTargetToken($testMarker, $tokenType);
+ self::$phpcsFile->getClassProperties($target);
+
+ }//end testNotAClassException()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotAClassException() For the array format.
+ *
+ * @return array
+ */
+ public function dataNotAClassException()
+ {
+ return [
+ 'interface' => [
+ '/* testNotAClass */',
+ \T_INTERFACE,
+ ],
+ 'anon-class' => [
+ '/* testAnonClass */',
+ \T_ANON_CLASS,
+ ],
+ 'enum' => [
+ '/* testEnum */',
+ \T_ENUM,
+ ],
+ ];
+
+ }//end dataNotAClassException()
+
+
+ /**
+ * Test retrieving the properties for a class declaration.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param array $expected Expected function output.
+ *
+ * @dataProvider dataGetClassProperties
+ *
+ * @return void
+ */
+ public function testGetClassProperties($testMarker, $expected)
+ {
+ $class = $this->getTargetToken($testMarker, \T_CLASS);
+ $result = self::$phpcsFile->getClassProperties($class);
+ $this->assertSame($expected, $result);
+
+ }//end testGetClassProperties()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testGetClassProperties() For the array format.
+ *
+ * @return array
+ */
+ public function dataGetClassProperties()
+ {
+ return [
+ 'no-properties' => [
+ '/* testClassWithoutProperties */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_readonly' => false,
+ ],
+ ],
+ 'abstract' => [
+ '/* testAbstractClass */',
+ [
+ 'is_abstract' => true,
+ 'is_final' => false,
+ 'is_readonly' => false,
+ ],
+ ],
+ 'final' => [
+ '/* testFinalClass */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => true,
+ 'is_readonly' => false,
+ ],
+ ],
+ 'readonly' => [
+ '/* testReadonlyClass */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_readonly' => true,
+ ],
+ ],
+ 'final-readonly' => [
+ '/* testFinalReadonlyClass */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => true,
+ 'is_readonly' => true,
+ ],
+ ],
+ 'readonly-final' => [
+ '/* testReadonlyFinalClass */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => true,
+ 'is_readonly' => true,
+ ],
+ ],
+ 'abstract-readonly' => [
+ '/* testAbstractReadonlyClass */',
+ [
+ 'is_abstract' => true,
+ 'is_final' => false,
+ 'is_readonly' => true,
+ ],
+ ],
+ 'readonly-abstract' => [
+ '/* testReadonlyAbstractClass */',
+ [
+ 'is_abstract' => true,
+ 'is_final' => false,
+ 'is_readonly' => true,
+ ],
+ ],
+ 'comments-and-new-lines' => [
+ '/* testWithCommentsAndNewLines */',
+ [
+ 'is_abstract' => true,
+ 'is_final' => false,
+ 'is_readonly' => false,
+ ],
+ ],
+ 'no-properties-with-docblock' => [
+ '/* testWithDocblockWithoutProperties */',
+ [
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_readonly' => false,
+ ],
+ ],
+ 'abstract-final-parse-error' => [
+ '/* testParseErrorAbstractFinal */',
+ [
+ 'is_abstract' => true,
+ 'is_final' => true,
+ 'is_readonly' => false,
+ ],
+ ],
+ ];
+
+ }//end dataGetClassProperties()
+
+
+}//end class
diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc
index af52ff75df..f40b6021f4 100644
--- a/tests/Core/File/GetMemberPropertiesTest.inc
+++ b/tests/Core/File/GetMemberPropertiesTest.inc
@@ -179,3 +179,126 @@ function_call( 'param', new class {
/* testNestedMethodParam 2 */
public function __construct( $open, $post_id ) {}
}, 10, 2 );
+
+class PHP8Mixed {
+ /* testPHP8MixedTypeHint */
+ public static miXed $mixed;
+
+ /* testPHP8MixedTypeHintNullable */
+ // Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
+ private ?mixed $nullableMixed;
+}
+
+class NSOperatorInType {
+ /* testNamespaceOperatorTypeHint */
+ public ?namespace\Name $prop;
+}
+
+$anon = class() {
+ /* testPHP8UnionTypesSimple */
+ public int|float $unionTypeSimple;
+
+ /* testPHP8UnionTypesTwoClasses */
+ private MyClassA|\Package\MyClassB $unionTypesTwoClasses;
+
+ /* testPHP8UnionTypesAllBaseTypes */
+ protected array|bool|int|float|NULL|object|string $unionTypesAllBaseTypes;
+
+ /* testPHP8UnionTypesAllPseudoTypes */
+ // Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+ var false|mixed|self|parent|iterable|Resource $unionTypesAllPseudoTypes;
+
+ /* testPHP8UnionTypesIllegalTypes */
+ // Intentional fatal error - types which are not allowed for properties, but that's not the concern of the method.
+ public callable|static|void $unionTypesIllegalTypes;
+
+ /* testPHP8UnionTypesNullable */
+ // Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+ public ?int|float $unionTypesNullable;
+
+ /* testPHP8PseudoTypeNull */
+ // Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+ public null $pseudoTypeNull;
+
+ /* testPHP8PseudoTypeFalse */
+ // Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+ public false $pseudoTypeFalse;
+
+ /* testPHP8PseudoTypeFalseAndBool */
+ // Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+ public bool|FALSE $pseudoTypeFalseAndBool;
+
+ /* testPHP8ObjectAndClass */
+ // Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+ public object|ClassName $objectAndClass;
+
+ /* testPHP8PseudoTypeIterableAndArray */
+ // Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+ public iterable|array|Traversable $pseudoTypeIterableAndArray;
+
+ /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+ // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+ public int |string| /*comment*/ INT $duplicateTypeInUnion;
+
+ /* testPHP81Readonly */
+ public readonly int $readonly;
+
+ /* testPHP81ReadonlyWithNullableType */
+ public readonly ?array $array;
+
+ /* testPHP81ReadonlyWithUnionType */
+ public readonly string|int $readonlyWithUnionType;
+
+ /* testPHP81ReadonlyWithUnionTypeWithNull */
+ protected ReadOnly string|null $readonlyWithUnionTypeWithNull;
+
+ /* testPHP81OnlyReadonlyWithUnionType */
+ readonly string|int $onlyReadonly;
+};
+
+$anon = class {
+ /* testPHP8PropertySingleAttribute */
+ #[PropertyWithAttribute]
+ public string $foo;
+
+ /* testPHP8PropertyMultipleAttributes */
+ #[PropertyWithAttribute(foo: 'bar'), MyAttribute]
+ protected ?int|float $bar;
+
+ /* testPHP8PropertyMultilineAttribute */
+ #[
+ PropertyWithAttribute(/* comment */ 'baz')
+ ]
+ private mixed $baz;
+};
+
+enum Suit
+{
+ /* testEnumProperty */
+ protected $anonymous;
+}
+
+enum Direction implements ArrayAccess
+{
+ case Up;
+ case Down;
+
+ /* testEnumMethodParamNotProperty */
+ public function offsetGet($val) { ... }
+}
+
+$anon = class() {
+ /* testPHP81IntersectionTypes */
+ public Foo&Bar $intersectionType;
+
+ /* testPHP81MoreIntersectionTypes */
+ public Foo&Bar&Baz $moreIntersectionTypes;
+
+ /* testPHP81IllegalIntersectionTypes */
+ // Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method.
+ public int&string $illegalIntersectionType;
+
+ /* testPHP81NulltableIntersectionType */
+ // Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method.
+ public ?Foo&Bar $nullableIntersectionType;
+};
diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php
index a4bff22133..934d8e2890 100644
--- a/tests/Core/File/GetMemberPropertiesTest.php
+++ b/tests/Core/File/GetMemberPropertiesTest.php
@@ -51,6 +51,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -61,6 +62,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '?int',
'nullable_type' => true,
],
@@ -71,6 +73,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -81,6 +84,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => 'string',
'nullable_type' => false,
],
@@ -91,6 +95,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -101,6 +106,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => 'bool',
'nullable_type' => false,
],
@@ -111,6 +117,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -121,6 +128,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => 'array',
'nullable_type' => false,
],
@@ -131,6 +139,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -141,6 +150,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '?string',
'nullable_type' => true,
],
@@ -151,6 +161,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -161,6 +172,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => false,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -171,6 +183,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -181,6 +194,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -191,6 +205,18 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
+ 'type' => '',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testNoPrefix */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -201,6 +227,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -211,6 +238,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -221,6 +249,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -231,6 +260,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => 'float',
'nullable_type' => false,
],
@@ -241,6 +271,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => 'float',
'nullable_type' => false,
],
@@ -251,6 +282,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '?string',
'nullable_type' => true,
],
@@ -261,26 +293,18 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '?string',
'nullable_type' => true,
],
],
- [
- '/* testNoPrefix */',
- [
- 'scope' => 'public',
- 'scope_specified' => false,
- 'is_static' => false,
- 'type' => '',
- 'nullable_type' => false,
- ],
- ],
[
'/* testGroupProtectedStatic 1 */',
[
'scope' => 'protected',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -291,6 +315,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -301,6 +326,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -311,6 +337,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -321,6 +348,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -331,6 +359,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -341,6 +370,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -351,6 +381,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -361,6 +392,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -371,6 +403,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -381,6 +414,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '?array',
'nullable_type' => true,
],
@@ -391,6 +425,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '\MyNamespace\MyClass',
'nullable_type' => false,
],
@@ -401,6 +436,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '?ClassName',
'nullable_type' => true,
],
@@ -411,6 +447,7 @@ public function dataGetMemberProperties()
'scope' => 'protected',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '?Folder\ClassName',
'nullable_type' => true,
],
@@ -421,6 +458,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '\MyNamespace\MyClass\Foo',
'nullable_type' => false,
],
@@ -431,6 +469,7 @@ public function dataGetMemberProperties()
'scope' => 'private',
'scope_specified' => true,
'is_static' => true,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -445,6 +484,7 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
@@ -455,10 +495,309 @@ public function dataGetMemberProperties()
'scope' => 'public',
'scope_specified' => true,
'is_static' => false,
+ 'is_readonly' => false,
'type' => '',
'nullable_type' => false,
],
],
+ [
+ '/* testPHP8MixedTypeHint */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => true,
+ 'is_readonly' => false,
+ 'type' => 'miXed',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8MixedTypeHintNullable */',
+ [
+ 'scope' => 'private',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => '?mixed',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testNamespaceOperatorTypeHint */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => '?namespace\Name',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesSimple */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'int|float',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesTwoClasses */',
+ [
+ 'scope' => 'private',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'MyClassA|\Package\MyClassB',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesAllBaseTypes */',
+ [
+ 'scope' => 'protected',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'array|bool|int|float|NULL|object|string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesAllPseudoTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'false|mixed|self|parent|iterable|Resource',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesIllegalTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ // Missing static, but that's OK as not an allowed syntax.
+ 'type' => 'callable||void',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8UnionTypesNullable */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => '?int|float',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeNull */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'null',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeFalse */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'false',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeFalseAndBool */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'bool|FALSE',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8ObjectAndClass */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'object|ClassName',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PseudoTypeIterableAndArray */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'iterable|array|Traversable',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'int|string|INT',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81Readonly */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => 'int',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81ReadonlyWithNullableType */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => '?array',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testPHP81ReadonlyWithUnionType */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => 'string|int',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81ReadonlyWithUnionTypeWithNull */',
+ [
+ 'scope' => 'protected',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => 'string|null',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81OnlyReadonlyWithUnionType */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => 'string|int',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PropertySingleAttribute */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP8PropertyMultipleAttributes */',
+ [
+ 'scope' => 'protected',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => '?int|float',
+ 'nullable_type' => true,
+ ],
+ ],
+ [
+ '/* testPHP8PropertyMultilineAttribute */',
+ [
+ 'scope' => 'private',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'mixed',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testEnumProperty */',
+ [],
+ ],
+ [
+ '/* testPHP81IntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81MoreIntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'Foo&Bar&Baz',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81IllegalIntersectionTypes */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => 'int&string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81NulltableIntersectionType */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'type' => '?Foo&Bar',
+ 'nullable_type' => true,
+ ],
+ ],
];
}//end dataGetMemberProperties()
@@ -500,6 +839,7 @@ public function dataNotClassProperty()
['/* testGlobalVariable */'],
['/* testNestedMethodParam 1 */'],
['/* testNestedMethodParam 2 */'],
+ ['/* testEnumMethodParamNotProperty */'],
];
}//end dataNotClassProperty()
diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc
index 86eff8dd4a..c0ef6e0621 100644
--- a/tests/Core/File/GetMethodParametersTest.inc
+++ b/tests/Core/File/GetMethodParametersTest.inc
@@ -31,3 +31,139 @@ function myFunction($a = 10 & 20) {}
/* testArrowFunction */
fn(int $a, ...$b) => $b;
+
+/* testPHP8MixedTypeHint */
+function mixedTypeHint(mixed &...$var1) {}
+
+/* testPHP8MixedTypeHintNullable */
+// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
+function mixedTypeHintNullable(?Mixed $var1) {}
+
+/* testNamespaceOperatorTypeHint */
+function namespaceOperatorTypeHint(?namespace\Name $var1) {}
+
+/* testPHP8UnionTypesSimple */
+function unionTypeSimple(int|float $number, self|parent &...$obj) {}
+
+/* testPHP8UnionTypesWithSpreadOperatorAndReference */
+function globalFunctionWithSpreadAndReference(float|null &$paramA, string|int ...$paramB) {}
+
+/* testPHP8UnionTypesSimpleWithBitwiseOrInDefault */
+$fn = fn(int|float $var = CONSTANT_A | CONSTANT_B) => $var;
+
+/* testPHP8UnionTypesTwoClasses */
+function unionTypesTwoClasses(MyClassA|\Package\MyClassB $var) {}
+
+/* testPHP8UnionTypesAllBaseTypes */
+function unionTypesAllBaseTypes(array|bool|callable|int|float|null|object|string $var) {}
+
+/* testPHP8UnionTypesAllPseudoTypes */
+// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+function unionTypesAllPseudoTypes(false|mixed|self|parent|iterable|Resource $var) {}
+
+/* testPHP8UnionTypesNullable */
+// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+$closure = function (?int|float $number) {};
+
+/* testPHP8PseudoTypeNull */
+// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeNull(null $var = null) {}
+
+/* testPHP8PseudoTypeFalse */
+// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeFalse(false $var = false) {}
+
+/* testPHP8PseudoTypeFalseAndBool */
+// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+function pseudoTypeFalseAndBool(bool|false $var = false) {}
+
+/* testPHP8ObjectAndClass */
+// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+function objectAndClass(object|ClassName $var) {}
+
+/* testPHP8PseudoTypeIterableAndArray */
+// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+function pseudoTypeIterableAndArray(iterable|array|Traversable $var) {}
+
+/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+function duplicateTypeInUnion( int | string /*comment*/ | INT $var) {}
+
+class ConstructorPropertyPromotionNoTypes {
+ /* testPHP8ConstructorPropertyPromotionNoTypes */
+ public function __construct(
+ public $x = 0.0,
+ protected $y = '',
+ private $z = null,
+ ) {}
+}
+
+class ConstructorPropertyPromotionWithTypes {
+ /* testPHP8ConstructorPropertyPromotionWithTypes */
+ public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {}
+}
+
+class ConstructorPropertyPromotionAndNormalParams {
+ /* testPHP8ConstructorPropertyPromotionAndNormalParam */
+ public function __construct(public int $promotedProp, ?int $normalArg) {}
+}
+
+class ConstructorPropertyPromotionWithReadOnly {
+ /* testPHP81ConstructorPropertyPromotionWithReadOnly */
+ public function __construct(public readonly ?int $promotedProp, readonly private string|bool &$promotedToo) {}
+}
+
+class ConstructorPropertyPromotionWithOnlyReadOnly {
+ /* testPHP81ConstructorPropertyPromotionWithOnlyReadOnly */
+ public function __construct(readonly Foo&Bar $promotedProp, readonly ?bool $promotedToo,) {}
+}
+
+/* testPHP8ConstructorPropertyPromotionGlobalFunction */
+// Intentional fatal error. Property promotion not allowed in non-constructor, but that's not the concern of this method.
+function globalFunction(private $x) {}
+
+abstract class ConstructorPropertyPromotionAbstractMethod {
+ /* testPHP8ConstructorPropertyPromotionAbstractMethod */
+ // Intentional fatal error.
+ // 1. Property promotion not allowed in abstract method, but that's not the concern of this method.
+ // 2. Variadic arguments not allowed in property promotion, but that's not the concern of this method.
+ // 3. The callable type is not supported for properties, but that's not the concern of this method.
+ abstract public function __construct(public callable $y, private ...$x);
+}
+
+/* testCommentsInParameter */
+function commentsInParams(
+ // Leading comment.
+ ?MyClass /*-*/ & /*-*/.../*-*/ $param /*-*/ = /*-*/ 'default value' . /*-*/ 'second part' // Trailing comment.
+) {}
+
+/* testParameterAttributesInFunctionDeclaration */
+class ParametersWithAttributes(
+ public function __construct(
+ #[\MyExample\MyAttribute] private string $constructorPropPromTypedParamSingleAttribute,
+ #[MyAttr([1, 2])]
+ Type|false
+ $typedParamSingleAttribute,
+ #[MyAttribute(1234), MyAttribute(5678)] ?int $nullableTypedParamMultiAttribute,
+ #[WithoutArgument] #[SingleArgument(0)] $nonTypedParamTwoAttributes,
+ #[MyAttribute(array("key" => "value"))]
+ &...$otherParam,
+ ) {}
+}
+
+/* testPHP8IntersectionTypes */
+function intersectionTypes(Foo&Bar $obj1, Boo&Bar $obj2) {}
+
+/* testPHP81IntersectionTypesWithSpreadOperatorAndReference */
+function globalFunctionWithSpreadAndReference(Boo&Bar &$paramA, Foo&Bar ...$paramB) {}
+
+/* testPHP81MoreIntersectionTypes */
+function moreIntersectionTypes(MyClassA&\Package\MyClassB&\Package\MyClassC $var) {}
+
+/* testPHP81IllegalIntersectionTypes */
+// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method.
+$closure = function (string&int $numeric_string) {};
+
+/* testPHP81NullableIntersectionTypes */
+// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method.
+$closure = function (?Foo&Bar $object) {};
diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php
index 1e8595da5c..b5bd61813c 100644
--- a/tests/Core/File/GetMethodParametersTest.php
+++ b/tests/Core/File/GetMethodParametersTest.php
@@ -26,6 +26,7 @@ public function testPassByReference()
$expected[0] = [
'name' => '$var',
'content' => '&$var',
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => false,
'type_hint' => '',
@@ -48,6 +49,7 @@ public function testArrayHint()
$expected[0] = [
'name' => '$var',
'content' => 'array $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'array',
@@ -59,6 +61,87 @@ public function testArrayHint()
}//end testArrayHint()
+ /**
+ * Verify variable.
+ *
+ * @return void
+ */
+ public function testVariable()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => '$var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testVariable()
+
+
+ /**
+ * Verify default value parsing with a single function param.
+ *
+ * @return void
+ */
+ public function testSingleDefaultValue()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var1',
+ 'content' => '$var1=self::CONSTANT',
+ 'has_attributes' => false,
+ 'default' => 'self::CONSTANT',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testSingleDefaultValue()
+
+
+ /**
+ * Verify default value parsing.
+ *
+ * @return void
+ */
+ public function testDefaultValues()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var1',
+ 'content' => '$var1=1',
+ 'has_attributes' => false,
+ 'default' => '1',
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$var2',
+ 'content' => "\$var2='value'",
+ 'has_attributes' => false,
+ 'default' => "'value'",
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testDefaultValues()
+
+
/**
* Verify type hint parsing.
*
@@ -70,6 +153,7 @@ public function testTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => 'foo $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'foo',
@@ -79,6 +163,7 @@ public function testTypeHint()
$expected[1] = [
'name' => '$var2',
'content' => 'bar $var2',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'bar',
@@ -101,6 +186,7 @@ public function testSelfTypeHint()
$expected[0] = [
'name' => '$var',
'content' => 'self $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'self',
@@ -123,6 +209,7 @@ public function testNullableTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => '?int $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?int',
@@ -132,6 +219,7 @@ public function testNullableTypeHint()
$expected[1] = [
'name' => '$var2',
'content' => '?\bar $var2',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?\bar',
@@ -144,16 +232,18 @@ public function testNullableTypeHint()
/**
- * Verify variable.
+ * Verify "bitwise and" in default value !== pass-by-reference.
*
* @return void
*/
- public function testVariable()
+ public function testBitwiseAndConstantExpressionDefaultValue()
{
$expected = [];
$expected[0] = [
- 'name' => '$var',
- 'content' => '$var',
+ 'name' => '$a',
+ 'content' => '$a = 10 & 20',
+ 'default' => '10 & 20',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -162,116 +252,913 @@ public function testVariable()
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
- }//end testVariable()
+ }//end testBitwiseAndConstantExpressionDefaultValue()
/**
- * Verify default value parsing with a single function param.
+ * Verify that arrow functions are supported.
*
* @return void
*/
- public function testSingleDefaultValue()
+ public function testArrowFunction()
{
$expected = [];
$expected[0] = [
- 'name' => '$var1',
- 'content' => '$var1=self::CONSTANT',
- 'default' => 'self::CONSTANT',
+ 'name' => '$a',
+ 'content' => 'int $a',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
+ 'type_hint' => 'int',
+ 'nullable_type' => false,
+ ];
+
+ $expected[1] = [
+ 'name' => '$b',
+ 'content' => '...$b',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => true,
'type_hint' => '',
'nullable_type' => false,
];
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
- }//end testSingleDefaultValue()
+ }//end testArrowFunction()
/**
- * Verify default value parsing.
+ * Verify recognition of PHP8 mixed type declaration.
*
* @return void
*/
- public function testDefaultValues()
+ public function testPHP8MixedTypeHint()
{
$expected = [];
$expected[0] = [
'name' => '$var1',
- 'content' => '$var1=1',
- 'default' => '1',
+ 'content' => 'mixed &...$var1',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => 'mixed',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8MixedTypeHint()
+
+
+ /**
+ * Verify recognition of PHP8 mixed type declaration with nullability.
+ *
+ * @return void
+ */
+ public function testPHP8MixedTypeHintNullable()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var1',
+ 'content' => '?Mixed $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
- 'type_hint' => '',
+ 'type_hint' => '?Mixed',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8MixedTypeHintNullable()
+
+
+ /**
+ * Verify recognition of type declarations using the namespace operator.
+ *
+ * @return void
+ */
+ public function testNamespaceOperatorTypeHint()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var1',
+ 'content' => '?namespace\Name $var1',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?namespace\Name',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testNamespaceOperatorTypeHint()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimple()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$number',
+ 'content' => 'int|float $number',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int|float',
'nullable_type' => false,
];
$expected[1] = [
- 'name' => '$var2',
- 'content' => "\$var2='value'",
- 'default' => "'value'",
+ 'name' => '$obj',
+ 'content' => 'self|parent &...$obj',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => 'self|parent',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesSimple()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration when the variable has either a spread operator or a reference.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesWithSpreadOperatorAndReference()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$paramA',
+ 'content' => 'float|null &$paramA',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => false,
+ 'type_hint' => 'float|null',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$paramB',
+ 'content' => 'string|int ...$paramB',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => true,
+ 'type_hint' => 'string|int',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesWithSpreadOperatorAndReference()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with a bitwise or in the default value.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimpleWithBitwiseOrInDefault()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'int|float $var = CONSTANT_A | CONSTANT_B',
+ 'default' => 'CONSTANT_A | CONSTANT_B',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
- 'type_hint' => '',
+ 'type_hint' => 'int|float',
'nullable_type' => false,
];
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
- }//end testDefaultValues()
+ }//end testPHP8UnionTypesSimpleWithBitwiseOrInDefault()
/**
- * Verify "bitwise and" in default value !== pass-by-reference.
+ * Verify recognition of PHP8 union type declaration with two classes.
*
* @return void
*/
- public function testBitwiseAndConstantExpressionDefaultValue()
+ public function testPHP8UnionTypesTwoClasses()
{
$expected = [];
$expected[0] = [
- 'name' => '$a',
- 'content' => '$a = 10 & 20',
- 'default' => '10 & 20',
+ 'name' => '$var',
+ 'content' => 'MyClassA|\Package\MyClassB $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
- 'type_hint' => '',
+ 'type_hint' => 'MyClassA|\Package\MyClassB',
'nullable_type' => false,
];
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
- }//end testBitwiseAndConstantExpressionDefaultValue()
+ }//end testPHP8UnionTypesTwoClasses()
/**
- * Verify that arrow functions are supported.
+ * Verify recognition of PHP8 union type declaration with all base types.
*
* @return void
*/
- public function testArrowFunction()
+ public function testPHP8UnionTypesAllBaseTypes()
{
$expected = [];
$expected[0] = [
- 'name' => '$a',
- 'content' => 'int $a',
+ 'name' => '$var',
+ 'content' => 'array|bool|callable|int|float|null|object|string $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
- 'type_hint' => 'int',
+ 'type_hint' => 'array|bool|callable|int|float|null|object|string',
'nullable_type' => false,
];
- $expected[1] = [
- 'name' => '$b',
- 'content' => '...$b',
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllBaseTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all pseudo types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllPseudoTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'false|mixed|self|parent|iterable|Resource $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
- 'variable_length' => true,
- 'type_hint' => '',
+ 'variable_length' => false,
+ 'type_hint' => 'false|mixed|self|parent|iterable|Resource',
'nullable_type' => false,
];
$this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
- }//end testArrowFunction()
+ }//end testPHP8UnionTypesAllPseudoTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesNullable()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$number',
+ 'content' => '?int|float $number',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int|float',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesNullable()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type null.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeNull()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'null $var = null',
+ 'default' => 'null',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'null',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeNull()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type false.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalse()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'false $var = false',
+ 'default' => 'false',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'false',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalse()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalseAndBool()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'bool|false $var = false',
+ 'default' => 'false',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'bool|false',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalseAndBool()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name.
+ *
+ * @return void
+ */
+ public function testPHP8ObjectAndClass()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'object|ClassName $var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'object|ClassName',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ObjectAndClass()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeIterableAndArray()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'iterable|array|Traversable $var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'iterable|array|Traversable',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeIterableAndArray()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) duplicate types.
+ *
+ * @return void
+ */
+ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'int | string /*comment*/ | INT $var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int|string|INT',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+
+
+ /**
+ * Verify recognition of PHP8 constructor property promotion without type declaration, with defaults.
+ *
+ * @return void
+ */
+ public function testPHP8ConstructorPropertyPromotionNoTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$x',
+ 'content' => 'public $x = 0.0',
+ 'default' => '0.0',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ 'property_visibility' => 'public',
+ 'property_readonly' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$y',
+ 'content' => 'protected $y = \'\'',
+ 'default' => "''",
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ 'property_visibility' => 'protected',
+ 'property_readonly' => false,
+ ];
+ $expected[2] = [
+ 'name' => '$z',
+ 'content' => 'private $z = null',
+ 'default' => 'null',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ 'property_readonly' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ConstructorPropertyPromotionNoTypes()
+
+
+ /**
+ * Verify recognition of PHP8 constructor property promotion with type declarations.
+ *
+ * @return void
+ */
+ public function testPHP8ConstructorPropertyPromotionWithTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$x',
+ 'content' => 'protected float|int $x',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'float|int',
+ 'nullable_type' => false,
+ 'property_visibility' => 'protected',
+ 'property_readonly' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$y',
+ 'content' => 'public ?string &$y = \'test\'',
+ 'default' => "'test'",
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => false,
+ 'type_hint' => '?string',
+ 'nullable_type' => true,
+ 'property_visibility' => 'public',
+ 'property_readonly' => false,
+ ];
+ $expected[2] = [
+ 'name' => '$z',
+ 'content' => 'private mixed $z',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'mixed',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ 'property_readonly' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ConstructorPropertyPromotionWithTypes()
+
+
+ /**
+ * Verify recognition of PHP8 constructor with both property promotion as well as normal parameters.
+ *
+ * @return void
+ */
+ public function testPHP8ConstructorPropertyPromotionAndNormalParam()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$promotedProp',
+ 'content' => 'public int $promotedProp',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'int',
+ 'nullable_type' => false,
+ 'property_visibility' => 'public',
+ 'property_readonly' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$normalArg',
+ 'content' => '?int $normalArg',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ConstructorPropertyPromotionAndNormalParam()
+
+
+ /**
+ * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly keyword.
+ *
+ * @return void
+ */
+ public function testPHP81ConstructorPropertyPromotionWithReadOnly()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$promotedProp',
+ 'content' => 'public readonly ?int $promotedProp',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int',
+ 'nullable_type' => true,
+ 'property_visibility' => 'public',
+ 'property_readonly' => true,
+ ];
+ $expected[1] = [
+ 'name' => '$promotedToo',
+ 'content' => 'readonly private string|bool &$promotedToo',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => false,
+ 'type_hint' => 'string|bool',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ 'property_readonly' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81ConstructorPropertyPromotionWithReadOnly()
+
+
+ /**
+ * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly
+ * keyword without explicit visibility.
+ *
+ * @return void
+ */
+ public function testPHP81ConstructorPropertyPromotionWithOnlyReadOnly()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$promotedProp',
+ 'content' => 'readonly Foo&Bar $promotedProp',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Foo&Bar',
+ 'nullable_type' => false,
+ 'property_visibility' => 'public',
+ 'property_readonly' => true,
+ ];
+ $expected[1] = [
+ 'name' => '$promotedToo',
+ 'content' => 'readonly ?bool $promotedToo',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?bool',
+ 'nullable_type' => true,
+ 'property_visibility' => 'public',
+ 'property_readonly' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81ConstructorPropertyPromotionWithOnlyReadOnly()
+
+
+ /**
+ * Verify behaviour when a non-constructor function uses PHP 8 property promotion syntax.
+ *
+ * @return void
+ */
+ public function testPHP8ConstructorPropertyPromotionGlobalFunction()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$x',
+ 'content' => 'private $x',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ConstructorPropertyPromotionGlobalFunction()
+
+
+ /**
+ * Verify behaviour when an abstract constructor uses PHP 8 property promotion syntax.
+ *
+ * @return void
+ */
+ public function testPHP8ConstructorPropertyPromotionAbstractMethod()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$y',
+ 'content' => 'public callable $y',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'callable',
+ 'nullable_type' => false,
+ 'property_visibility' => 'public',
+ ];
+ $expected[1] = [
+ 'name' => '$x',
+ 'content' => 'private ...$x',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => true,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ConstructorPropertyPromotionAbstractMethod()
+
+
+ /**
+ * Verify and document behaviour when there are comments within a parameter declaration.
+ *
+ * @return void
+ */
+ public function testCommentsInParameter()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$param',
+ 'content' => '// Leading comment.
+ ?MyClass /*-*/ & /*-*/.../*-*/ $param /*-*/ = /*-*/ \'default value\' . /*-*/ \'second part\' // Trailing comment.',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => '?MyClass',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testCommentsInParameter()
+
+
+ /**
+ * Verify behaviour when parameters have attributes attached.
+ *
+ * @return void
+ */
+ public function testParameterAttributesInFunctionDeclaration()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$constructorPropPromTypedParamSingleAttribute',
+ 'content' => '#[\MyExample\MyAttribute] private string $constructorPropPromTypedParamSingleAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'string',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ ];
+ $expected[1] = [
+ 'name' => '$typedParamSingleAttribute',
+ 'content' => '#[MyAttr([1, 2])]
+ Type|false
+ $typedParamSingleAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Type|false',
+ 'nullable_type' => false,
+ ];
+ $expected[2] = [
+ 'name' => '$nullableTypedParamMultiAttribute',
+ 'content' => '#[MyAttribute(1234), MyAttribute(5678)] ?int $nullableTypedParamMultiAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int',
+ 'nullable_type' => true,
+ ];
+ $expected[3] = [
+ 'name' => '$nonTypedParamTwoAttributes',
+ 'content' => '#[WithoutArgument] #[SingleArgument(0)] $nonTypedParamTwoAttributes',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+ $expected[4] = [
+ 'name' => '$otherParam',
+ 'content' => '#[MyAttribute(array("key" => "value"))]
+ &...$otherParam',
+ 'has_attributes' => true,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testParameterAttributesInFunctionDeclaration()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8IntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$obj1',
+ 'content' => 'Foo&Bar $obj1',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$obj2',
+ 'content' => 'Boo&Bar $obj2',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Boo&Bar',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8IntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8 intersection type declaration when the variable has either a spread operator or a reference.
+ *
+ * @return void
+ */
+ public function testPHP81IntersectionTypesWithSpreadOperatorAndReference()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$paramA',
+ 'content' => 'Boo&Bar &$paramA',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => false,
+ 'type_hint' => 'Boo&Bar',
+ 'nullable_type' => false,
+ ];
+ $expected[1] = [
+ 'name' => '$paramB',
+ 'content' => 'Foo&Bar ...$paramB',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => true,
+ 'type_hint' => 'Foo&Bar',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IntersectionTypesWithSpreadOperatorAndReference()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with more types.
+ *
+ * @return void
+ */
+ public function testPHP81MoreIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$var',
+ 'content' => 'MyClassA&\Package\MyClassB&\Package\MyClassC $var',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'MyClassA&\Package\MyClassB&\Package\MyClassC',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81MoreIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with illegal simple types.
+ *
+ * @return void
+ */
+ public function testPHP81IllegalIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$numeric_string',
+ 'content' => 'string&int $numeric_string',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'string&int',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IllegalIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP81NullableIntersectionTypes()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$object',
+ 'content' => '?Foo&Bar $object',
+ 'has_attributes' => false,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?Foo&Bar',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NullableIntersectionTypes()
/**
@@ -284,7 +1171,7 @@ public function testArrowFunction()
*/
private function getMethodParametersTestHelper($commentString, $expected)
{
- $function = $this->getTargetToken($commentString, [T_FUNCTION, T_FN]);
+ $function = $this->getTargetToken($commentString, [T_FUNCTION, T_CLOSURE, T_FN]);
$found = self::$phpcsFile->getMethodParameters($function);
$this->assertArraySubset($expected, $found, true);
diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc
index b04202fcd2..0c592369a5 100644
--- a/tests/Core/File/GetMethodPropertiesTest.inc
+++ b/tests/Core/File/GetMethodPropertiesTest.inc
@@ -33,7 +33,7 @@ class MyClass {
/* testMessyNullableReturnMethod */
public function myFunction() /* comment
- */ :
+ */ :
/* comment */ ? //comment
array {}
@@ -73,3 +73,80 @@ class ReturnMe {
return $this;
}
}
+
+/* testPHP8MixedTypeHint */
+function mixedTypeHint() :mixed {}
+
+/* testPHP8MixedTypeHintNullable */
+// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method.
+function mixedTypeHintNullable(): ?mixed {}
+
+/* testNamespaceOperatorTypeHint */
+function namespaceOperatorTypeHint() : ?namespace\Name {}
+
+/* testPHP8UnionTypesSimple */
+function unionTypeSimple($number) : int|float {}
+
+/* testPHP8UnionTypesTwoClasses */
+$fn = fn($var): MyClassA|\Package\MyClassB => $var;
+
+/* testPHP8UnionTypesAllBaseTypes */
+function unionTypesAllBaseTypes() : array|bool|callable|int|float|null|Object|string {}
+
+/* testPHP8UnionTypesAllPseudoTypes */
+// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method.
+function unionTypesAllPseudoTypes($var) : false|MIXED|self|parent|static|iterable|Resource|void {}
+
+/* testPHP8UnionTypesNullable */
+// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method.
+$closure = function () use($a) :?int|float {};
+
+/* testPHP8PseudoTypeNull */
+// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeNull(): null {}
+
+/* testPHP8PseudoTypeFalse */
+// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method.
+function pseudoTypeFalse(): false {}
+
+/* testPHP8PseudoTypeFalseAndBool */
+// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method.
+function pseudoTypeFalseAndBool(): bool|false {}
+
+/* testPHP8ObjectAndClass */
+// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method.
+function objectAndClass(): object|ClassName {}
+
+/* testPHP8PseudoTypeIterableAndArray */
+// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method.
+interface FooBar {
+ public function pseudoTypeIterableAndArray(): iterable|array|Traversable;
+}
+
+/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */
+// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method.
+function duplicateTypeInUnion(): int | /*comment*/ string | INT {}
+
+/* testPHP81NeverType */
+function never(): never {}
+
+/* testPHP81NullableNeverType */
+// Intentional fatal error - nullability is not allowed with never, but that's not the concern of the method.
+function nullableNever(): ?never {}
+
+/* testPHP8IntersectionTypes */
+function intersectionTypes(): Foo&Bar {}
+
+/* testPHP81MoreIntersectionTypes */
+function moreIntersectionTypes(): MyClassA&\Package\MyClassB&\Package\MyClassC {}
+
+/* testPHP81IntersectionArrowFunction */
+$fn = fn($var): MyClassA&\Package\MyClassB => $var;
+
+/* testPHP81IllegalIntersectionTypes */
+// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method.
+$closure = function (): string&int {};
+
+/* testPHP81NullableIntersectionTypes */
+// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method.
+$closure = function (): ?Foo&Bar {};
diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php
index b801962637..66f4eea3ea 100644
--- a/tests/Core/File/GetMethodPropertiesTest.php
+++ b/tests/Core/File/GetMethodPropertiesTest.php
@@ -406,6 +406,489 @@ public function testReturnTypeStatic()
}//end testReturnTypeStatic()
+ /**
+ * Test a function with return type "mixed".
+ *
+ * @return void
+ */
+ public function testPHP8MixedTypeHint()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'mixed',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8MixedTypeHint()
+
+
+ /**
+ * Test a function with return type "mixed" and nullability.
+ *
+ * @return void
+ */
+ public function testPHP8MixedTypeHintNullable()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?mixed',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8MixedTypeHintNullable()
+
+
+ /**
+ * Test a function with return type using the namespace operator.
+ *
+ * @return void
+ */
+ public function testNamespaceOperatorTypeHint()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?namespace\Name',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testNamespaceOperatorTypeHint()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesSimple()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'int|float',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesSimple()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with two classes.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesTwoClasses()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'MyClassA|\Package\MyClassB',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesTwoClasses()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all base types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllBaseTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'array|bool|callable|int|float|null|Object|string',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllBaseTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with all pseudo types.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesAllPseudoTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'false|MIXED|self|parent|static|iterable|Resource|void',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesAllPseudoTypes()
+
+
+ /**
+ * Verify recognition of PHP8 union type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP8UnionTypesNullable()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?int|float',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8UnionTypesNullable()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type null.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeNull()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'null',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeNull()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) single type false.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalse()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'false',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalse()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeFalseAndBool()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'bool|false',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeFalseAndBool()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name.
+ *
+ * @return void
+ */
+ public function testPHP8ObjectAndClass()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'object|ClassName',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8ObjectAndClass()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable.
+ *
+ * @return void
+ */
+ public function testPHP8PseudoTypeIterableAndArray()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'return_type' => 'iterable|array|Traversable',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => false,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8PseudoTypeIterableAndArray()
+
+
+ /**
+ * Verify recognition of PHP8 type declaration with (illegal) duplicate types.
+ *
+ * @return void
+ */
+ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'int|string|INT',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment()
+
+
+ /**
+ * Verify recognition of PHP8.1 type "never".
+ *
+ * @return void
+ */
+ public function testPHP81NeverType()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'never',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NeverType()
+
+
+ /**
+ * Verify recognition of PHP8.1 type "never" with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP81NullableNeverType()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?never',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NullableNeverType()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration.
+ *
+ * @return void
+ */
+ public function testPHP8IntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'Foo&Bar',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP8IntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with more types.
+ *
+ * @return void
+ */
+ public function testPHP81MoreIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'MyClassA&\Package\MyClassB&\Package\MyClassC',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81MoreIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration in arrow function.
+ *
+ * @return void
+ */
+ public function testPHP81IntersectionArrowFunction()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'MyClassA&\Package\MyClassB',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IntersectionArrowFunction()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with illegal simple types.
+ *
+ * @return void
+ */
+ public function testPHP81IllegalIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => 'string&int',
+ 'nullable_return_type' => false,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81IllegalIntersectionTypes()
+
+
+ /**
+ * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability.
+ *
+ * @return void
+ */
+ public function testPHP81NullableIntersectionTypes()
+ {
+ $expected = [
+ 'scope' => 'public',
+ 'scope_specified' => false,
+ 'return_type' => '?Foo&Bar',
+ 'nullable_return_type' => true,
+ 'is_abstract' => false,
+ 'is_final' => false,
+ 'is_static' => false,
+ 'has_body' => true,
+ ];
+
+ $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testPHP81NullableIntersectionTypes()
+
+
/**
* Test helper.
*
diff --git a/tests/Core/File/IsReferenceTest.inc b/tests/Core/File/IsReferenceTest.inc
index c8f5dc2243..f71e2639d8 100644
--- a/tests/Core/File/IsReferenceTest.inc
+++ b/tests/Core/File/IsReferenceTest.inc
@@ -12,7 +12,7 @@ $a = [ $something & $somethingElse ];
$a = [ $first, $something & self::$somethingElse ];
/* testBitwiseAndD */
-$a = array $first, $something & $somethingElse );
+$a = array( $first, $something & $somethingElse );
/* testBitwiseAndE */
$a = [ 'a' => $first, 'b' => $something & $somethingElse ];
@@ -51,7 +51,7 @@ function myFunction(array &$one) {}
$closure = function (\MyClass &$one) {};
/* testFunctionPassByReferenceG */
-$closure = function myFunc($param, &...$moreParams) {};
+$closure = function ($param, &...$moreParams) {};
/* testForeachValueByReference */
foreach( $array as $key => &$value ) {}
@@ -140,5 +140,11 @@ $closure = function() use (&$var){};
/* testArrowFunctionReturnByReference */
fn&($x) => $x;
+/* testArrowFunctionPassByReferenceA */
+$fn = fn(array &$one) => 1;
+
+/* testArrowFunctionPassByReferenceB */
+$fn = fn($param, &...$moreParams) => 1;
+
/* testClosureReturnByReference */
$closure = function &($param) use ($value) {};
diff --git a/tests/Core/File/IsReferenceTest.php b/tests/Core/File/IsReferenceTest.php
index d6c37c6c1f..ea2dddbad3 100644
--- a/tests/Core/File/IsReferenceTest.php
+++ b/tests/Core/File/IsReferenceTest.php
@@ -16,7 +16,7 @@ class IsReferenceTest extends AbstractMethodUnitTest
/**
- * Test a class that extends another.
+ * Test correctly identifying whether a "bitwise and" token is a reference or not.
*
* @param string $identifier Comment which precedes the test case.
* @param bool $expected Expected function output.
@@ -228,6 +228,14 @@ public function dataIsReference()
'/* testArrowFunctionReturnByReference */',
true,
],
+ [
+ '/* testArrowFunctionPassByReferenceA */',
+ true,
+ ],
+ [
+ '/* testArrowFunctionPassByReferenceB */',
+ true,
+ ],
[
'/* testClosureReturnByReference */',
true,
diff --git a/tests/Core/Filters/Filter/AcceptTest.php b/tests/Core/Filters/Filter/AcceptTest.php
index 5d928c38b8..2c9f57cd39 100644
--- a/tests/Core/Filters/Filter/AcceptTest.php
+++ b/tests/Core/Filters/Filter/AcceptTest.php
@@ -111,9 +111,13 @@ public function dataExcludePatterns()
'/path/to/src/Main.php',
'/path/to/src/Something/Main.php',
'/path/to/src/Somethingelse/Main.php',
+ '/path/to/src/SomethingelseEvenLonger/Main.php',
'/path/to/src/Other/Main.php',
],
- ['/path/to/src/Main.php'],
+ [
+ '/path/to/src/Main.php',
+ '/path/to/src/SomethingelseEvenLonger/Main.php',
+ ],
],
// Test ignoring standard/sniff specific exclude patterns.
diff --git a/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php
new file mode 100644
index 0000000000..7eeed212f3
--- /dev/null
+++ b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php
@@ -0,0 +1,28 @@
+magic[$name] = $value;
+ }
+
+ public function __get($name)
+ {
+ if (isset($this->magic[$name])) {
+ return $this->magic[$name];
+ }
+
+ return null;
+ }
+
+ public function register()
+ {
+ return [T_WHITESPACE];
+ }
+
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ // Do something.
+ }
+}
diff --git a/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php
new file mode 100644
index 0000000000..6ffffad3e2
--- /dev/null
+++ b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php
@@ -0,0 +1,26 @@
+assertObjectHasAttribute('sniffCodes', self::$ruleset);
- $this->assertCount(14, self::$ruleset->sniffCodes);
+ $this->assertCount(48, self::$ruleset->sniffCodes);
}//end testHasSniffCodes()
/**
- * Test that sniffs are correctly registered, independently on the syntax used to include the sniff.
+ * Test that sniffs are correctly registered, independently of the syntax used to include the sniff.
*
* @param string $key Expected array key.
* @param string $value Expected array value.
@@ -147,6 +147,54 @@ public function testRegisteredSniffCodes($key, $value)
public function dataRegisteredSniffCodes()
{
return [
+ [
+ 'PSR2.Classes.ClassDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff',
+ ],
+ [
+ 'PSR2.Classes.PropertyDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\PropertyDeclarationSniff',
+ ],
+ [
+ 'PSR2.ControlStructures.ControlStructureSpacing',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\ControlStructureSpacingSniff',
+ ],
+ [
+ 'PSR2.ControlStructures.ElseIfDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\ElseIfDeclarationSniff',
+ ],
+ [
+ 'PSR2.ControlStructures.SwitchDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\SwitchDeclarationSniff',
+ ],
+ [
+ 'PSR2.Files.ClosingTag',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\ClosingTagSniff',
+ ],
+ [
+ 'PSR2.Files.EndFileNewline',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\EndFileNewlineSniff',
+ ],
+ [
+ 'PSR2.Methods.FunctionCallSignature',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff',
+ ],
+ [
+ 'PSR2.Methods.FunctionClosingBrace',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionClosingBraceSniff',
+ ],
+ [
+ 'PSR2.Methods.MethodDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\MethodDeclarationSniff',
+ ],
+ [
+ 'PSR2.Namespaces.NamespaceDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\NamespaceDeclarationSniff',
+ ],
+ [
+ 'PSR2.Namespaces.UseDeclaration',
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff',
+ ],
[
'PSR1.Classes.ClassDeclaration',
'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff',
@@ -180,8 +228,100 @@ public function dataRegisteredSniffCodes()
'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\UpperCaseConstantNameSniff',
],
[
- 'Zend.NamingConventions.ValidVariableName',
- 'PHP_CodeSniffer\Standards\Zend\Sniffs\NamingConventions\ValidVariableNameSniff',
+ 'Generic.Files.LineEndings',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineEndingsSniff',
+ ],
+ [
+ 'Generic.Files.LineLength',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff',
+ ],
+ [
+ 'Squiz.WhiteSpace.SuperfluousWhitespace',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SuperfluousWhitespaceSniff',
+ ],
+ [
+ 'Generic.Formatting.DisallowMultipleStatements',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\DisallowMultipleStatementsSniff',
+ ],
+ [
+ 'Generic.WhiteSpace.ScopeIndent',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\ScopeIndentSniff',
+ ],
+ [
+ 'Generic.WhiteSpace.DisallowTabIndent',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\DisallowTabIndentSniff',
+ ],
+ [
+ 'Generic.PHP.LowerCaseKeyword',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseKeywordSniff',
+ ],
+ [
+ 'Generic.PHP.LowerCaseConstant',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseConstantSniff',
+ ],
+ [
+ 'Squiz.Scope.MethodScope',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Scope\MethodScopeSniff',
+ ],
+ [
+ 'Squiz.WhiteSpace.ScopeKeywordSpacing',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ScopeKeywordSpacingSniff',
+ ],
+ [
+ 'Squiz.Functions.FunctionDeclaration',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\FunctionDeclarationSniff',
+ ],
+ [
+ 'Squiz.Functions.LowercaseFunctionKeywords',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\LowercaseFunctionKeywordsSniff',
+ ],
+ [
+ 'Squiz.Functions.FunctionDeclarationArgumentSpacing',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\FunctionDeclarationArgumentSpacingSniff',
+ ],
+ [
+ 'PEAR.Functions.ValidDefaultValue',
+ 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\ValidDefaultValueSniff',
+ ],
+ [
+ 'Squiz.Functions.MultiLineFunctionDeclaration',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\MultiLineFunctionDeclarationSniff',
+ ],
+ [
+ 'Generic.Functions.FunctionCallArgumentSpacing',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Functions\FunctionCallArgumentSpacingSniff',
+ ],
+ [
+ 'Squiz.ControlStructures.ControlSignature',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ControlSignatureSniff',
+ ],
+ [
+ 'Squiz.WhiteSpace.ControlStructureSpacing',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ControlStructureSpacingSniff',
+ ],
+ [
+ 'Squiz.WhiteSpace.ScopeClosingBrace',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ScopeClosingBraceSniff',
+ ],
+ [
+ 'Squiz.ControlStructures.ForEachLoopDeclaration',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ForEachLoopDeclarationSniff',
+ ],
+ [
+ 'Squiz.ControlStructures.ForLoopDeclaration',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ForLoopDeclarationSniff',
+ ],
+ [
+ 'Squiz.ControlStructures.LowercaseDeclaration',
+ 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\LowercaseDeclarationSniff',
+ ],
+ [
+ 'Generic.ControlStructures.InlineControlStructure',
+ 'PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures\InlineControlStructureSniff',
+ ],
+ [
+ 'PSR12.Operators.OperatorSpacing',
+ 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff',
],
[
'Generic.Arrays.ArrayIndent',
@@ -191,10 +331,6 @@ public function dataRegisteredSniffCodes()
'Generic.Metrics.CyclomaticComplexity',
'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\CyclomaticComplexitySniff',
],
- [
- 'Generic.Files.LineLength',
- 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff',
- ],
[
'Generic.NamingConventions.CamelCapsFunctionName',
'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff',
@@ -242,49 +378,54 @@ public function testSettingProperties($sniffClass, $propertyName, $expectedValue
public function dataSettingProperties()
{
return [
- 'ClassDeclarationSniff' => [
- 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff',
- 'setforallsniffs',
- true,
+ 'Set property for complete standard: PSR2 ClassDeclaration' => [
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff',
+ 'indent',
+ '20',
],
- 'SideEffectsSniff' => [
- 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Files\SideEffectsSniff',
- 'setforallsniffs',
- true,
+ 'Set property for complete standard: PSR2 SwitchDeclaration' => [
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\SwitchDeclarationSniff',
+ 'indent',
+ '20',
],
- 'ValidVariableNameSniff' => [
- 'PHP_CodeSniffer\Standards\Zend\Sniffs\NamingConventions\ValidVariableNameSniff',
- 'setforallincategory',
- true,
+ 'Set property for complete standard: PSR2 FunctionCallSignature' => [
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff',
+ 'indent',
+ '20',
],
- 'ArrayIndentSniff' => [
+ 'Set property for complete category: PSR12 OperatorSpacing' => [
+ 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff',
+ 'ignoreSpacingBeforeAssignments',
+ false,
+ ],
+ 'Set property for individual sniff: Generic ArrayIndent' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays\ArrayIndentSniff',
'indent',
'2',
],
- 'LineLengthSniff' => [
+ 'Set property for individual sniff using sniff file inclusion: Generic LineLength' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff',
'lineLimit',
'10',
],
- 'CamelCapsFunctionNameSniff' => [
+ 'Set property for individual sniff using sniff file inclusion: CamelCapsFunctionName' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff',
'strict',
false,
],
- 'NestingLevelSniff-nestingLevel' => [
+ 'Set property for individual sniff via included ruleset: NestingLevel - nestingLevel' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff',
'nestingLevel',
'2',
],
- 'NestingLevelSniff-setforsniffsinincludedruleset' => [
+ 'Set property for all sniffs in an included ruleset: NestingLevel - absoluteNestingLevel' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff',
- 'setforsniffsinincludedruleset',
+ 'absoluteNestingLevel',
true,
],
// Testing that setting a property at error code level does *not* work.
- 'CyclomaticComplexitySniff' => [
+ 'Set property for error code will not change the sniff property value: CyclomaticComplexity' => [
'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\CyclomaticComplexitySniff',
'complexity',
10,
@@ -294,4 +435,53 @@ public function dataSettingProperties()
}//end dataSettingProperties()
+ /**
+ * Test that setting properties for standards, categories on sniffs which don't support the property will
+ * silently ignore the property and not set it.
+ *
+ * @param string $sniffClass The name of the sniff class.
+ * @param string $propertyName The name of the property which should not be set.
+ *
+ * @dataProvider dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails
+ *
+ * @return void
+ */
+ public function testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails($sniffClass, $propertyName)
+ {
+ $this->assertObjectHasAttribute('sniffs', self::$ruleset, 'Ruleset does not have property sniffs');
+ $this->assertArrayHasKey($sniffClass, self::$ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs');
+
+ $sniffObject = self::$ruleset->sniffs[$sniffClass];
+ $this->assertObjectNotHasAttribute($propertyName, $sniffObject, 'Property '.$propertyName.' registered for sniff '.$sniffClass.' which does not support it');
+
+ }//end testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails()
+
+
+ /**
+ * Data provider.
+ *
+ * @see self::testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails()
+ *
+ * @return array
+ */
+ public function dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails()
+ {
+ return [
+ 'Set property for complete standard: PSR2 ClassDeclaration' => [
+ 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff',
+ 'setforallsniffs',
+ ],
+ 'Set property for complete standard: PSR2 FunctionCallSignature' => [
+ 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff',
+ 'setforallsniffs',
+ ],
+ 'Set property for complete category: PSR12 OperatorSpacing' => [
+ 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff',
+ 'setforallincategory',
+ ],
+ ];
+
+ }//end dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails()
+
+
}//end class
diff --git a/tests/Core/Ruleset/RuleInclusionTest.xml b/tests/Core/Ruleset/RuleInclusionTest.xml
index 06ce040e71..e1812bbba5 100644
--- a/tests/Core/Ruleset/RuleInclusionTest.xml
+++ b/tests/Core/Ruleset/RuleInclusionTest.xml
@@ -1,15 +1,17 @@
-
+
+
-
+
+
@@ -38,8 +40,9 @@
+
-
+
diff --git a/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml b/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml
new file mode 100644
index 0000000000..21dba9da8f
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml b/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml
new file mode 100644
index 0000000000..03424db26e
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml b/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml
new file mode 100644
index 0000000000..4e4fd59e65
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml b/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml
new file mode 100644
index 0000000000..cb4f489d37
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml
new file mode 100644
index 0000000000..044414c0f5
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml
new file mode 100644
index 0000000000..759c6c655a
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml b/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml
new file mode 100644
index 0000000000..d9f808e383
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml b/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml
new file mode 100644
index 0000000000..435601affb
--- /dev/null
+++ b/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Core/Ruleset/SetSniffPropertyTest.php b/tests/Core/Ruleset/SetSniffPropertyTest.php
new file mode 100644
index 0000000000..9dbfc7bc88
--- /dev/null
+++ b/tests/Core/Ruleset/SetSniffPropertyTest.php
@@ -0,0 +1,413 @@
+
+ * @copyright 2022 Juliette Reinders Folmer. All rights reserved.
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Ruleset;
+
+use PHP_CodeSniffer\Config;
+use PHP_CodeSniffer\Ruleset;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * These tests specifically focus on the changes made to work around the PHP 8.2 dynamic properties deprecation.
+ *
+ * @covers \PHP_CodeSniffer\Ruleset::setSniffProperty
+ */
+class SetSniffPropertyTest extends TestCase
+{
+
+
+ /**
+ * Initialize the test.
+ *
+ * @return void
+ */
+ public function setUp()
+ {
+ if ($GLOBALS['PHP_CODESNIFFER_PEAR'] === true) {
+ // PEAR installs test and sniff files into different locations
+ // so these tests will not pass as they directly reference files
+ // by relative location.
+ $this->markTestSkipped('Test cannot run from a PEAR install');
+ }
+
+ }//end setUp()
+
+
+ /**
+ * Test that setting a property via the ruleset works in all situations which allow for it.
+ *
+ * @param string $name Name of the test. Used for the sniff name, the ruleset file name etc.
+ *
+ * @dataProvider dataSniffPropertiesGetSetWhenAllowed
+ *
+ * @return void
+ */
+ public function testSniffPropertiesGetSetWhenAllowed($name)
+ {
+ $sniffCode = "Fixtures.Category.{$name}";
+ $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff';
+ $properties = [
+ 'arbitrarystring' => 'arbitraryvalue',
+ 'arbitraryarray' => [
+ 'mykey' => 'myvalue',
+ 'otherkey' => 'othervalue',
+ ],
+ ];
+
+ // Set up the ruleset.
+ $standard = __DIR__."/{$name}Test.xml";
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ // Verify that the sniff has been registered.
+ $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered');
+
+ // Verify that our target sniff has been registered.
+ $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered');
+ $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class');
+
+ // Test that the property as declared in the ruleset has been set on the sniff.
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs');
+
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ foreach ($properties as $name => $expectedValue) {
+ $this->assertSame($expectedValue, $sniffObject->$name, 'Property value not set to expected value');
+ }
+
+ }//end testSniffPropertiesGetSetWhenAllowed()
+
+
+ /**
+ * Data provider.
+ *
+ * @see self::testSniffPropertiesGetSetWhenAllowed()
+ *
+ * @return array
+ */
+ public function dataSniffPropertiesGetSetWhenAllowed()
+ {
+ return [
+ 'Property allowed as explicitly declared' => ['SetPropertyAllowedAsDeclared'],
+ 'Property allowed as sniff extends stdClass' => ['SetPropertyAllowedViaStdClass'],
+ 'Property allowed as sniff has magic __set() method' => ['SetPropertyAllowedViaMagicMethod'],
+ ];
+
+ }//end dataSniffPropertiesGetSetWhenAllowed()
+
+
+ /**
+ * Test that setting a property for a category will apply it correctly to those sniffs which support the
+ * property, but won't apply it to sniffs which don't.
+ *
+ * Note: this test intentionally uses the `PEAR.Functions` category as two sniffs in that category
+ * have a public property with the same name (`indent`) and one sniff doesn't, which makes it a great
+ * test case for this.
+ *
+ * @return void
+ */
+ public function testSetPropertyAppliesPropertyToMultipleSniffsInCategory()
+ {
+ $propertyName = 'indent';
+ $expectedValue = '10';
+
+ // Set up the ruleset.
+ $standard = __DIR__.'/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml';
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ // Test that the two sniffs which support the property have received the value.
+ $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\FunctionCallSignatureSniff';
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs');
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ $this->assertSame($expectedValue, $sniffObject->$propertyName, 'Property value not set to expected value for '.$sniffClass);
+
+ $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\FunctionDeclarationSniff';
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs');
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ $this->assertSame($expectedValue, $sniffObject->$propertyName, 'Property value not set to expected value for '.$sniffClass);
+
+ // Test that the property doesn't get set for the one sniff which doesn't support the property.
+ $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\ValidDefaultValueSniff';
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs');
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ $this->assertObjectNotHasAttribute($propertyName, $sniffObject, 'Property registered for sniff '.$sniffClass.' which does not support it');
+
+ }//end testSetPropertyAppliesPropertyToMultipleSniffsInCategory()
+
+
+ /**
+ * Test that attempting to set a non-existent property directly on a sniff will throw an error
+ * when the sniff does not explicitly declare the property, extends stdClass or has magic methods.
+ *
+ * @return void
+ */
+ public function testSetPropertyThrowsErrorOnInvalidProperty()
+ {
+ $exceptionClass = 'PHP_CodeSniffer\Exceptions\RuntimeException';
+ $exceptionMsg = 'Ruleset invalid. Property "indentation" does not exist on sniff Generic.Arrays.ArrayIndent';
+ if (method_exists($this, 'expectException') === true) {
+ $this->expectException($exceptionClass);
+ $this->expectExceptionMessage($exceptionMsg);
+ } else {
+ // PHPUnit < 5.2.0.
+ $this->setExpectedException($exceptionClass, $exceptionMsg);
+ }
+
+ // Set up the ruleset.
+ $standard = __DIR__.'/SetPropertyThrowsErrorOnInvalidPropertyTest.xml';
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ }//end testSetPropertyThrowsErrorOnInvalidProperty()
+
+
+ /**
+ * Test that attempting to set a non-existent property directly on a sniff will throw an error
+ * when the sniff does not explicitly declare the property, extends stdClass or has magic methods,
+ * even though the sniff has the PHP 8.2 `#[AllowDynamicProperties]` attribute set.
+ *
+ * @return void
+ */
+ public function testSetPropertyThrowsErrorWhenPropertyOnlyAllowedViaAttribute()
+ {
+ $exceptionClass = 'PHP_CodeSniffer\Exceptions\RuntimeException';
+ $exceptionMsg = 'Ruleset invalid. Property "arbitrarystring" does not exist on sniff Fixtures.Category.SetPropertyNotAllowedViaAttribute';
+ if (method_exists($this, 'expectException') === true) {
+ $this->expectException($exceptionClass);
+ $this->expectExceptionMessage($exceptionMsg);
+ } else {
+ // PHPUnit < 5.2.0.
+ $this->setExpectedException($exceptionClass, $exceptionMsg);
+ }
+
+ // Set up the ruleset.
+ $standard = __DIR__.'/SetPropertyNotAllowedViaAttributeTest.xml';
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ }//end testSetPropertyThrowsErrorWhenPropertyOnlyAllowedViaAttribute()
+
+
+ /**
+ * Test that attempting to set a non-existent property on a sniff when the property directive is
+ * for the whole standard, does not yield an error.
+ *
+ * @doesNotPerformAssertions
+ *
+ * @return void
+ */
+ public function testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandard()
+ {
+ // Set up the ruleset.
+ $standard = __DIR__.'/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml';
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ }//end testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandard()
+
+
+ /**
+ * Test that attempting to set a non-existent property on a sniff when the property directive is
+ * for a whole category, does not yield an error.
+ *
+ * @doesNotPerformAssertions
+ *
+ * @return void
+ */
+ public function testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategory()
+ {
+ // Set up the ruleset.
+ $standard = __DIR__.'/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml';
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ }//end testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategory()
+
+
+ /**
+ * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method
+ * sets the property correctly when using the new $settings array format.
+ *
+ * @return void
+ */
+ public function testDirectCallWithNewArrayFormatSetsProperty()
+ {
+ $name = 'SetPropertyAllowedAsDeclared';
+ $sniffCode = "Fixtures.Category.{$name}";
+ $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff';
+
+ // Set up the ruleset.
+ $standard = __DIR__."/{$name}Test.xml";
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ $propertyName = 'arbitrarystring';
+ $propertyValue = 'new value';
+
+ $ruleset->setSniffProperty(
+ $sniffClass,
+ $propertyName,
+ [
+ 'scope' => 'sniff',
+ 'value' => $propertyValue,
+ ]
+ );
+
+ // Verify that the sniff has been registered.
+ $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered');
+
+ // Verify that our target sniff has been registered.
+ $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered');
+ $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class');
+
+ // Test that the property as declared in the ruleset has been set on the sniff.
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs');
+
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ $this->assertSame($propertyValue, $sniffObject->$propertyName, 'Property value not set to expected value');
+
+ }//end testDirectCallWithNewArrayFormatSetsProperty()
+
+
+ /**
+ * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method
+ * sets the property correctly when using the old $settings array format.
+ *
+ * Tested by silencing the deprecation notice as otherwise the test would fail on the deprecation notice.
+ *
+ * @param mixed $propertyValue Value for the property to set.
+ *
+ * @dataProvider dataDirectCallWithOldArrayFormatSetsProperty
+ *
+ * @return void
+ */
+ public function testDirectCallWithOldArrayFormatSetsProperty($propertyValue)
+ {
+ $name = 'SetPropertyAllowedAsDeclared';
+ $sniffCode = "Fixtures.Category.{$name}";
+ $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff';
+
+ // Set up the ruleset.
+ $standard = __DIR__."/{$name}Test.xml";
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ $propertyName = 'arbitrarystring';
+
+ @$ruleset->setSniffProperty(
+ $sniffClass,
+ $propertyName,
+ $propertyValue
+ );
+
+ // Verify that the sniff has been registered.
+ $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered');
+
+ // Verify that our target sniff has been registered.
+ $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered');
+ $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class');
+
+ // Test that the property as declared in the ruleset has been set on the sniff.
+ $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs');
+
+ $sniffObject = $ruleset->sniffs[$sniffClass];
+ $this->assertSame($propertyValue, $sniffObject->$propertyName, 'Property value not set to expected value');
+
+ }//end testDirectCallWithOldArrayFormatSetsProperty()
+
+
+ /**
+ * Data provider.
+ *
+ * @see self::testDirectCallWithOldArrayFormatSetsProperty()
+ *
+ * @return array
+ */
+ public function dataDirectCallWithOldArrayFormatSetsProperty()
+ {
+ return [
+ 'Property value is not an array (boolean)' => [false],
+ 'Property value is not an array (string)' => ['a string'],
+ 'Property value is an empty array' => [[]],
+ 'Property value is an array without keys' => [
+ [
+ 'value',
+ false,
+ ],
+ ],
+ 'Property value is an array without the "scope" or "value" keys' => [
+ [
+ 'key1' => 'value',
+ 'key2' => false,
+ ],
+ ],
+ 'Property value is an array without the "scope" key' => [
+ [
+ 'key1' => 'value',
+ 'value' => true,
+ ],
+ ],
+ 'Property value is an array without the "value" key' => [
+ [
+ 'scope' => 'value',
+ 'key2' => 1234,
+ ],
+ ],
+ ];
+
+ }//end dataDirectCallWithOldArrayFormatSetsProperty()
+
+
+ /**
+ * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method
+ * throws a deprecation notice when using the old $settings array format.
+ *
+ * Note: as PHPUnit stops as soon as it sees the deprecation notice, the setting of the property
+ * value is not tested here.
+ *
+ * @return void
+ */
+ public function testDirectCallWithOldArrayFormatThrowsDeprecationNotice()
+ {
+ $exceptionClass = 'PHPUnit\Framework\Error\Deprecated';
+ if (class_exists($exceptionClass) === false) {
+ $exceptionClass = 'PHPUnit_Framework_Error_Deprecated';
+ }
+
+ $exceptionMsg = 'the format of the $settings parameter has changed from (mixed) $value to array(\'scope\' => \'sniff|standard\', \'value\' => $value). Please update your integration code. See PR #3629 for more information.';
+
+ if (method_exists($this, 'expectException') === true) {
+ $this->expectException($exceptionClass);
+ $this->expectExceptionMessage($exceptionMsg);
+ } else {
+ // PHPUnit < 5.2.0.
+ $this->setExpectedException($exceptionClass, $exceptionMsg);
+ }
+
+ $name = 'SetPropertyAllowedAsDeclared';
+ $sniffCode = "Fixtures.Category.{$name}";
+ $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff';
+
+ // Set up the ruleset.
+ $standard = __DIR__."/{$name}Test.xml";
+ $config = new Config(["--standard=$standard"]);
+ $ruleset = new Ruleset($config);
+
+ $propertyName = 'arbitrarystring';
+
+ $ruleset->setSniffProperty(
+ $sniffClass,
+ 'arbitrarystring',
+ ['key' => 'value']
+ );
+
+ }//end testDirectCallWithOldArrayFormatThrowsDeprecationNotice()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.inc b/tests/Core/Tokenizer/ArrayKeywordTest.inc
new file mode 100644
index 0000000000..ce5c553cf6
--- /dev/null
+++ b/tests/Core/Tokenizer/ArrayKeywordTest.inc
@@ -0,0 +1,35 @@
+ 10);
+
+/* testArrayWithComment */
+$var = Array /*comment*/ (1 => 10);
+
+/* testNestingArray */
+$var = array(
+ /* testNestedArray */
+ array(
+ 'key' => 'value',
+
+ /* testClosureReturnType */
+ 'closure' => function($a) use($global) : Array {},
+ ),
+);
+
+/* testFunctionDeclarationParamType */
+function foo(array $a) {}
+
+/* testFunctionDeclarationReturnType */
+function foo($a) : int|array|null {}
+
+class Bar {
+ /* testClassConst */
+ const ARRAY = [];
+
+ /* testClassMethod */
+ public function array() {}
+}
diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.php b/tests/Core/Tokenizer/ArrayKeywordTest.php
new file mode 100644
index 0000000000..237258a62a
--- /dev/null
+++ b/tests/Core/Tokenizer/ArrayKeywordTest.php
@@ -0,0 +1,170 @@
+
+ * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class ArrayKeywordTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the array keyword is correctly tokenized as `T_ARRAY`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent Optional. The token content to look for.
+ *
+ * @dataProvider dataArrayKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testArrayKeyword($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_ARRAY, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_ARRAY (code)');
+ $this->assertSame('T_ARRAY', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_ARRAY (type)');
+
+ $this->assertArrayHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is not set');
+ $this->assertArrayHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is not set');
+ $this->assertArrayHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is not set');
+
+ }//end testArrayKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testArrayKeyword()
+ *
+ * @return array
+ */
+ public function dataArrayKeyword()
+ {
+ return [
+ 'empty array' => ['/* testEmptyArray */'],
+ 'array with space before parenthesis' => ['/* testArrayWithSpace */'],
+ 'array with comment before parenthesis' => [
+ '/* testArrayWithComment */',
+ 'Array',
+ ],
+ 'nested: outer array' => ['/* testNestingArray */'],
+ 'nested: inner array' => ['/* testNestedArray */'],
+ ];
+
+ }//end dataArrayKeyword()
+
+
+ /**
+ * Test that the array keyword when used in a type declaration is correctly tokenized as `T_STRING`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent Optional. The token content to look for.
+ *
+ * @dataProvider dataArrayType
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testArrayType($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
+ $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
+ $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');
+
+ }//end testArrayType()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testArrayType()
+ *
+ * @return array
+ */
+ public function dataArrayType()
+ {
+ return [
+ 'closure return type' => [
+ '/* testClosureReturnType */',
+ 'Array',
+ ],
+ 'function param type' => ['/* testFunctionDeclarationParamType */'],
+ 'function union return type' => ['/* testFunctionDeclarationReturnType */'],
+ ];
+
+ }//end dataArrayType()
+
+
+ /**
+ * Verify that the retokenization of `T_ARRAY` tokens to `T_STRING` is handled correctly
+ * for tokens with the contents 'array' which aren't in actual fact the array keyword.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotArrayKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testNotArrayKeyword($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
+ $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
+ $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');
+
+ }//end testNotArrayKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotArrayKeyword()
+ *
+ * @return array
+ */
+ public function dataNotArrayKeyword()
+ {
+ return [
+ 'class-constant-name' => [
+ '/* testClassConst */',
+ 'ARRAY',
+ ],
+ 'class-method-name' => ['/* testClassMethod */'],
+ ];
+
+ }//end dataNotArrayKeyword()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/AttributesTest.inc b/tests/Core/Tokenizer/AttributesTest.inc
new file mode 100644
index 0000000000..e539adf8a7
--- /dev/null
+++ b/tests/Core/Tokenizer/AttributesTest.inc
@@ -0,0 +1,90 @@
+ 'foobar'])]
+function attribute_with_params_on_function_test() {}
+
+/* testAttributeWithShortClosureParameter */
+#[AttributeWithParams(static fn ($value) => ! $value)]
+function attribute_with_short_closure_param_test() {}
+
+/* testTwoAttributeOnTheSameLine */
+#[CustomAttribute] #[AttributeWithParams('foo')]
+function two_attribute_on_same_line_test() {}
+
+/* testAttributeAndCommentOnTheSameLine */
+#[CustomAttribute] // This is a comment
+function attribute_and_line_comment_on_same_line_test() {}
+
+/* testAttributeGrouping */
+#[CustomAttribute, AttributeWithParams('foo'), AttributeWithParams('foo', bar: ['bar' => 'foobar'])]
+function attribute_grouping_test() {}
+
+/* testAttributeMultiline */
+#[
+ CustomAttribute,
+ AttributeWithParams('foo'),
+ AttributeWithParams('foo', bar: ['bar' => 'foobar'])
+]
+function attribute_multiline_test() {}
+
+/* testAttributeMultilineWithComment */
+#[
+ CustomAttribute, // comment
+ AttributeWithParams(/* another comment */ 'foo'),
+ AttributeWithParams('foo', bar: ['bar' => 'foobar'])
+]
+function attribute_multiline_with_comment_test() {}
+
+/* testSingleAttributeOnParameter */
+function single_attribute_on_parameter_test(#[ParamAttribute] int $param) {}
+
+/* testMultipleAttributesOnParameter */
+function multiple_attributes_on_parameter_test(#[ParamAttribute, AttributeWithParams(/* another comment */ 'foo')] int $param) {}
+
+/* testFqcnAttribute */
+#[Boo\QualifiedName, \Foo\FullyQualifiedName('foo')]
+function fqcn_attrebute_test() {}
+
+/* testNestedAttributes */
+#[Boo\QualifiedName(fn (#[AttributeOne('boo')] $value) => (string) $value)]
+function nested_attributes_test() {}
+
+/* testMultilineAttributesOnParameter */
+function multiline_attributes_on_parameter_test(#[
+ AttributeWithParams(
+ 'foo'
+ )
+ ] int $param) {}
+
+/* testAttributeContainingTextLookingLikeCloseTag */
+#[DeprecationReason('reason: ')]
+function attribute_containing_text_looking_like_close_tag() {}
+
+/* testAttributeContainingMultilineTextLookingLikeCloseTag */
+#[DeprecationReason(
+ 'reason: '
+)]
+function attribute_containing_mulitline_text_looking_like_close_tag() {}
+
+/* testInvalidAttribute */
+#[ThisIsNotAnAttribute
+function invalid_attribute_test() {}
diff --git a/tests/Core/Tokenizer/AttributesTest.php b/tests/Core/Tokenizer/AttributesTest.php
new file mode 100644
index 0000000000..8ac826f2f6
--- /dev/null
+++ b/tests/Core/Tokenizer/AttributesTest.php
@@ -0,0 +1,658 @@
+
+ * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class AttributesTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that attributes are parsed correctly.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param int $length The number of tokens between opener and closer.
+ * @param array $tokenCodes The codes of tokens inside the attributes.
+ *
+ * @dataProvider dataAttribute
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testAttribute($testMarker, $length, $tokenCodes)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $attribute = $this->getTargetToken($testMarker, T_ATTRIBUTE);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(($attribute + $length), $closer);
+
+ $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']);
+
+ $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']);
+ $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']);
+
+ $map = array_map(
+ function ($token) use ($attribute, $length) {
+ $this->assertArrayHasKey('attribute_closer', $token);
+ $this->assertSame(($attribute + $length), $token['attribute_closer']);
+
+ return $token['code'];
+ },
+ array_slice($tokens, ($attribute + 1), ($length - 1))
+ );
+
+ $this->assertSame($tokenCodes, $map);
+
+ }//end testAttribute()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testAttribute()
+ *
+ * @return array
+ */
+ public function dataAttribute()
+ {
+ return [
+ [
+ '/* testAttribute */',
+ 2,
+ [ T_STRING ],
+ ],
+ [
+ '/* testAttributeWithParams */',
+ 7,
+ [
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_STRING,
+ T_DOUBLE_COLON,
+ T_STRING,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testAttributeWithNamedParam */',
+ 10,
+ [
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_PARAM_NAME,
+ T_COLON,
+ T_WHITESPACE,
+ T_STRING,
+ T_DOUBLE_COLON,
+ T_STRING,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testAttributeOnFunction */',
+ 2,
+ [ T_STRING ],
+ ],
+ [
+ '/* testAttributeOnFunctionWithParams */',
+ 17,
+ [
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_PARAM_NAME,
+ T_COLON,
+ T_WHITESPACE,
+ T_OPEN_SHORT_ARRAY,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_WHITESPACE,
+ T_DOUBLE_ARROW,
+ T_WHITESPACE,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_SHORT_ARRAY,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testAttributeWithShortClosureParameter */',
+ 17,
+ [
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_STATIC,
+ T_WHITESPACE,
+ T_FN,
+ T_WHITESPACE,
+ T_OPEN_PARENTHESIS,
+ T_VARIABLE,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ T_FN_ARROW,
+ T_WHITESPACE,
+ T_BOOLEAN_NOT,
+ T_WHITESPACE,
+ T_VARIABLE,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testAttributeGrouping */',
+ 26,
+ [
+ T_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_PARENTHESIS,
+ T_COMMA,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_PARAM_NAME,
+ T_COLON,
+ T_WHITESPACE,
+ T_OPEN_SHORT_ARRAY,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_WHITESPACE,
+ T_DOUBLE_ARROW,
+ T_WHITESPACE,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_SHORT_ARRAY,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testAttributeMultiline */',
+ 31,
+ [
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_PARENTHESIS,
+ T_COMMA,
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_PARAM_NAME,
+ T_COLON,
+ T_WHITESPACE,
+ T_OPEN_SHORT_ARRAY,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_WHITESPACE,
+ T_DOUBLE_ARROW,
+ T_WHITESPACE,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_SHORT_ARRAY,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ ],
+ ],
+ [
+ '/* testFqcnAttribute */',
+ 13,
+ [
+ T_STRING,
+ T_NS_SEPARATOR,
+ T_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_NS_SEPARATOR,
+ T_STRING,
+ T_NS_SEPARATOR,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ ];
+
+ }//end dataAttribute()
+
+
+ /**
+ * Test that multiple attributes on the same line are parsed correctly.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testTwoAttributesOnTheSameLine()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $attribute = $this->getTargetToken('/* testTwoAttributeOnTheSameLine */', T_ATTRIBUTE);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']);
+ $this->assertSame(T_ATTRIBUTE, $tokens[($closer + 2)]['code']);
+ $this->assertArrayHasKey('attribute_closer', $tokens[($closer + 2)]);
+
+ }//end testTwoAttributesOnTheSameLine()
+
+
+ /**
+ * Test that attribute followed by a line comment is parsed correctly.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testAttributeAndLineComment()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $attribute = $this->getTargetToken('/* testAttributeAndCommentOnTheSameLine */', T_ATTRIBUTE);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']);
+ $this->assertSame(T_COMMENT, $tokens[($closer + 2)]['code']);
+
+ }//end testAttributeAndLineComment()
+
+
+ /**
+ * Test that attributes on function declaration parameters are parsed correctly.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param int $position The token position (starting from T_FUNCTION) of T_ATTRIBUTE token.
+ * @param int $length The number of tokens between opener and closer.
+ * @param array $tokenCodes The codes of tokens inside the attributes.
+ *
+ * @dataProvider dataAttributeOnParameters
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testAttributeOnParameters($testMarker, $position, $length, array $tokenCodes)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $function = $this->getTargetToken($testMarker, T_FUNCTION);
+ $attribute = ($function + $position);
+
+ $this->assertSame(T_ATTRIBUTE, $tokens[$attribute]['code']);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $this->assertSame(($attribute + $length), $tokens[$attribute]['attribute_closer']);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']);
+ $this->assertSame(T_STRING, $tokens[($closer + 2)]['code']);
+ $this->assertSame('int', $tokens[($closer + 2)]['content']);
+
+ $this->assertSame(T_VARIABLE, $tokens[($closer + 4)]['code']);
+ $this->assertSame('$param', $tokens[($closer + 4)]['content']);
+
+ $map = array_map(
+ function ($token) use ($attribute, $length) {
+ $this->assertArrayHasKey('attribute_closer', $token);
+ $this->assertSame(($attribute + $length), $token['attribute_closer']);
+
+ return $token['code'];
+ },
+ array_slice($tokens, ($attribute + 1), ($length - 1))
+ );
+
+ $this->assertSame($tokenCodes, $map);
+
+ }//end testAttributeOnParameters()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testAttributeOnParameters()
+ *
+ * @return array
+ */
+ public function dataAttributeOnParameters()
+ {
+ return [
+ [
+ '/* testSingleAttributeOnParameter */',
+ 4,
+ 2,
+ [T_STRING],
+ ],
+ [
+ '/* testMultipleAttributesOnParameter */',
+ 4,
+ 10,
+ [
+ T_STRING,
+ T_COMMA,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_COMMENT,
+ T_WHITESPACE,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_PARENTHESIS,
+ ],
+ ],
+ [
+ '/* testMultilineAttributesOnParameter */',
+ 4,
+ 13,
+ [
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_WHITESPACE,
+ T_WHITESPACE,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ T_WHITESPACE,
+ ],
+ ],
+ ];
+
+ }//end dataAttributeOnParameters()
+
+
+ /**
+ * Test that an attribute containing text which looks like a PHP close tag is tokenized correctly.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param int $length The number of tokens between opener and closer.
+ * @param array $expectedTokensAttribute The codes of tokens inside the attributes.
+ * @param array $expectedTokensAfter The codes of tokens after the attributes.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @dataProvider dataAttributeOnTextLookingLikeCloseTag
+ *
+ * @return void
+ */
+ public function testAttributeContainingTextLookingLikeCloseTag($testMarker, $length, array $expectedTokensAttribute, array $expectedTokensAfter)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $attribute = $this->getTargetToken($testMarker, T_ATTRIBUTE);
+
+ $this->assertSame('T_ATTRIBUTE', $tokens[$attribute]['type']);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(($attribute + $length), $closer);
+ $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']);
+ $this->assertSame('T_ATTRIBUTE_END', $tokens[$closer]['type']);
+
+ $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']);
+ $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']);
+
+ $i = ($attribute + 1);
+ foreach ($expectedTokensAttribute as $item) {
+ list($expectedType, $expectedContents) = $item;
+ $this->assertSame($expectedType, $tokens[$i]['type']);
+ $this->assertSame($expectedContents, $tokens[$i]['content']);
+ $this->assertArrayHasKey('attribute_opener', $tokens[$i]);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$i]);
+ ++$i;
+ }
+
+ $i = ($closer + 1);
+ foreach ($expectedTokensAfter as $expectedCode) {
+ $this->assertSame($expectedCode, $tokens[$i]['code']);
+ ++$i;
+ }
+
+ }//end testAttributeContainingTextLookingLikeCloseTag()
+
+
+ /**
+ * Data provider.
+ *
+ * @see dataAttributeOnTextLookingLikeCloseTag()
+ *
+ * @return array
+ */
+ public function dataAttributeOnTextLookingLikeCloseTag()
+ {
+ return [
+ [
+ '/* testAttributeContainingTextLookingLikeCloseTag */',
+ 5,
+ [
+ [
+ 'T_STRING',
+ 'DeprecationReason',
+ ],
+ [
+ 'T_OPEN_PARENTHESIS',
+ '(',
+ ],
+ [
+ 'T_CONSTANT_ENCAPSED_STRING',
+ "'reason: '",
+ ],
+ [
+ 'T_CLOSE_PARENTHESIS',
+ ')',
+ ],
+ [
+ 'T_ATTRIBUTE_END',
+ ']',
+ ],
+ ],
+ [
+ T_WHITESPACE,
+ T_FUNCTION,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ T_OPEN_CURLY_BRACKET,
+ T_CLOSE_CURLY_BRACKET,
+ ],
+ ],
+ [
+ '/* testAttributeContainingMultilineTextLookingLikeCloseTag */',
+ 8,
+ [
+ [
+ 'T_STRING',
+ 'DeprecationReason',
+ ],
+ [
+ 'T_OPEN_PARENTHESIS',
+ '(',
+ ],
+ [
+ 'T_WHITESPACE',
+ "\n",
+ ],
+ [
+ 'T_WHITESPACE',
+ " ",
+ ],
+ [
+ 'T_CONSTANT_ENCAPSED_STRING',
+ "'reason: '",
+ ],
+ [
+ 'T_WHITESPACE',
+ "\n",
+ ],
+ [
+ 'T_CLOSE_PARENTHESIS',
+ ')',
+ ],
+ [
+ 'T_ATTRIBUTE_END',
+ ']',
+ ],
+ ],
+ [
+ T_WHITESPACE,
+ T_FUNCTION,
+ T_WHITESPACE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ T_OPEN_CURLY_BRACKET,
+ T_CLOSE_CURLY_BRACKET,
+ ],
+ ],
+ ];
+
+ }//end dataAttributeOnTextLookingLikeCloseTag()
+
+
+ /**
+ * Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testInvalidAttribute()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $attribute = $this->getTargetToken('/* testInvalidAttribute */', T_ATTRIBUTE);
+
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+ $this->assertNull($tokens[$attribute]['attribute_closer']);
+
+ }//end testInvalidAttribute()
+
+
+ /**
+ * Test that nested attributes are parsed correctly.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
+ *
+ * @return void
+ */
+ public function testNestedAttributes()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $tokenCodes = [
+ T_STRING,
+ T_NS_SEPARATOR,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_FN,
+ T_WHITESPACE,
+ T_OPEN_PARENTHESIS,
+ T_ATTRIBUTE,
+ T_STRING,
+ T_OPEN_PARENTHESIS,
+ T_CONSTANT_ENCAPSED_STRING,
+ T_CLOSE_PARENTHESIS,
+ T_ATTRIBUTE_END,
+ T_WHITESPACE,
+ T_VARIABLE,
+ T_CLOSE_PARENTHESIS,
+ T_WHITESPACE,
+ T_FN_ARROW,
+ T_WHITESPACE,
+ T_STRING_CAST,
+ T_WHITESPACE,
+ T_VARIABLE,
+ T_CLOSE_PARENTHESIS,
+ ];
+
+ $attribute = $this->getTargetToken('/* testNestedAttributes */', T_ATTRIBUTE);
+ $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
+
+ $closer = $tokens[$attribute]['attribute_closer'];
+ $this->assertSame(($attribute + 24), $closer);
+
+ $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']);
+
+ $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']);
+ $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']);
+
+ $this->assertArrayNotHasKey('nested_attributes', $tokens[$attribute]);
+ $this->assertArrayHasKey('nested_attributes', $tokens[($attribute + 8)]);
+ $this->assertSame([$attribute => ($attribute + 24)], $tokens[($attribute + 8)]['nested_attributes']);
+
+ $test = function (array $tokens, $length, $nestedMap) use ($attribute) {
+ foreach ($tokens as $token) {
+ $this->assertArrayHasKey('attribute_closer', $token);
+ $this->assertSame(($attribute + $length), $token['attribute_closer']);
+ $this->assertSame($nestedMap, $token['nested_attributes']);
+ }
+ };
+
+ $test(array_slice($tokens, ($attribute + 1), 7), 24, [$attribute => $attribute + 24]);
+ $test(array_slice($tokens, ($attribute + 8), 1), 8 + 5, [$attribute => $attribute + 24]);
+
+ // Length here is 8 (nested attribute offset) + 5 (real length).
+ $test(
+ array_slice($tokens, ($attribute + 9), 4),
+ 8 + 5,
+ [
+ $attribute => $attribute + 24,
+ $attribute + 8 => $attribute + 13,
+ ]
+ );
+
+ $test(array_slice($tokens, ($attribute + 13), 1), 8 + 5, [$attribute => $attribute + 24]);
+ $test(array_slice($tokens, ($attribute + 14), 10), 24, [$attribute => $attribute + 24]);
+
+ $map = array_map(
+ static function ($token) {
+ return $token['code'];
+ },
+ array_slice($tokens, ($attribute + 1), 23)
+ );
+
+ $this->assertSame($tokenCodes, $map);
+
+ }//end testNestedAttributes()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillEnumTest.inc b/tests/Core/Tokenizer/BackfillEnumTest.inc
new file mode 100644
index 0000000000..82bfe24fd5
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillEnumTest.inc
@@ -0,0 +1,95 @@
+enum = 'foo';
+ }
+}
+
+/* testEnumUsedAsFunctionName */
+function enum()
+{
+}
+
+/* testDeclarationContainingComment */
+enum /* comment */ Name
+{
+ case SOME_CASE;
+}
+
+enum /* testEnumUsedAsEnumName */ Enum
+{
+}
+
+/* testEnumUsedAsNamespaceName */
+namespace Enum;
+/* testEnumUsedAsPartOfNamespaceName */
+namespace My\Enum\Collection;
+/* testEnumUsedInObjectInitialization */
+$obj = new Enum;
+/* testEnumAsFunctionCall */
+$var = enum($a, $b);
+/* testEnumAsFunctionCallWithNamespace */
+var = namespace\enum();
+/* testClassConstantFetchWithEnumAsClassName */
+echo Enum::CONSTANT;
+/* testClassConstantFetchWithEnumAsConstantName */
+echo ClassName::ENUM;
+
+/* testParseErrorMissingName */
+enum {
+ case SOME_CASE;
+}
+
+/* testParseErrorLiveCoding */
+// This must be the last test in the file.
+enum
diff --git a/tests/Core/Tokenizer/BackfillEnumTest.php b/tests/Core/Tokenizer/BackfillEnumTest.php
new file mode 100644
index 0000000000..33cff3a2c7
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillEnumTest.php
@@ -0,0 +1,229 @@
+
+ * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class BackfillEnumTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the "enum" keyword is tokenized as such.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $testContent The token content to look for.
+ * @param int $openerOffset Offset to find expected scope opener.
+ * @param int $closerOffset Offset to find expected scope closer.
+ *
+ * @dataProvider dataEnums
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testEnums($testMarker, $testContent, $openerOffset, $closerOffset)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $enum = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent);
+
+ $this->assertSame(T_ENUM, $tokens[$enum]['code']);
+ $this->assertSame('T_ENUM', $tokens[$enum]['type']);
+
+ $this->assertArrayHasKey('scope_condition', $tokens[$enum]);
+ $this->assertArrayHasKey('scope_opener', $tokens[$enum]);
+ $this->assertArrayHasKey('scope_closer', $tokens[$enum]);
+
+ $this->assertSame($enum, $tokens[$enum]['scope_condition']);
+
+ $scopeOpener = $tokens[$enum]['scope_opener'];
+ $scopeCloser = $tokens[$enum]['scope_closer'];
+
+ $expectedScopeOpener = ($enum + $openerOffset);
+ $expectedScopeCloser = ($enum + $closerOffset);
+
+ $this->assertSame($expectedScopeOpener, $scopeOpener);
+ $this->assertArrayHasKey('scope_condition', $tokens[$scopeOpener]);
+ $this->assertArrayHasKey('scope_opener', $tokens[$scopeOpener]);
+ $this->assertArrayHasKey('scope_closer', $tokens[$scopeOpener]);
+ $this->assertSame($enum, $tokens[$scopeOpener]['scope_condition']);
+ $this->assertSame($scopeOpener, $tokens[$scopeOpener]['scope_opener']);
+ $this->assertSame($scopeCloser, $tokens[$scopeOpener]['scope_closer']);
+
+ $this->assertSame($expectedScopeCloser, $scopeCloser);
+ $this->assertArrayHasKey('scope_condition', $tokens[$scopeCloser]);
+ $this->assertArrayHasKey('scope_opener', $tokens[$scopeCloser]);
+ $this->assertArrayHasKey('scope_closer', $tokens[$scopeCloser]);
+ $this->assertSame($enum, $tokens[$scopeCloser]['scope_condition']);
+ $this->assertSame($scopeOpener, $tokens[$scopeCloser]['scope_opener']);
+ $this->assertSame($scopeCloser, $tokens[$scopeCloser]['scope_closer']);
+
+ }//end testEnums()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testEnums()
+ *
+ * @return array
+ */
+ public function dataEnums()
+ {
+ return [
+ [
+ '/* testPureEnum */',
+ 'enum',
+ 4,
+ 12,
+ ],
+ [
+ '/* testBackedIntEnum */',
+ 'enum',
+ 7,
+ 29,
+ ],
+ [
+ '/* testBackedStringEnum */',
+ 'enum',
+ 8,
+ 30,
+ ],
+ [
+ '/* testComplexEnum */',
+ 'enum',
+ 11,
+ 72,
+ ],
+ [
+ '/* testEnumWithEnumAsClassName */',
+ 'enum',
+ 6,
+ 7,
+ ],
+ [
+ '/* testEnumIsCaseInsensitive */',
+ 'EnUm',
+ 4,
+ 5,
+ ],
+ [
+ '/* testDeclarationContainingComment */',
+ 'enum',
+ 6,
+ 14,
+ ],
+ ];
+
+ }//end dataEnums()
+
+
+ /**
+ * Test that "enum" when not used as the keyword is still tokenized as `T_STRING`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotEnums
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testNotEnums($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent);
+ $this->assertSame(T_STRING, $tokens[$target]['code']);
+ $this->assertSame('T_STRING', $tokens[$target]['type']);
+
+ }//end testNotEnums()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotEnums()
+ *
+ * @return array
+ */
+ public function dataNotEnums()
+ {
+ return [
+ [
+ '/* testEnumAsClassNameAfterEnumKeyword */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumUsedAsClassName */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumUsedAsClassConstantName */',
+ 'ENUM',
+ ],
+ [
+ '/* testEnumUsedAsMethodName */',
+ 'enum',
+ ],
+ [
+ '/* testEnumUsedAsPropertyName */',
+ 'enum',
+ ],
+ [
+ '/* testEnumUsedAsFunctionName */',
+ 'enum',
+ ],
+ [
+ '/* testEnumUsedAsEnumName */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumUsedAsNamespaceName */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumUsedAsPartOfNamespaceName */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumUsedInObjectInitialization */',
+ 'Enum',
+ ],
+ [
+ '/* testEnumAsFunctionCall */',
+ 'enum',
+ ],
+ [
+ '/* testEnumAsFunctionCallWithNamespace */',
+ 'enum',
+ ],
+ [
+ '/* testClassConstantFetchWithEnumAsClassName */',
+ 'Enum',
+ ],
+ [
+ '/* testClassConstantFetchWithEnumAsConstantName */',
+ 'ENUM',
+ ],
+ [
+ '/* testParseErrorMissingName */',
+ 'enum',
+ ],
+ [
+ '/* testParseErrorLiveCoding */',
+ 'enum',
+ ],
+ ];
+
+ }//end dataNotEnums()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc
new file mode 100644
index 0000000000..eb907ed390
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc
@@ -0,0 +1,31 @@
+
+ * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class BackfillExplicitOctalNotationTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that explicitly-defined octal values are tokenized as a single number and not as a number and a string.
+ *
+ * @param string $marker The comment which prefaces the target token in the test file.
+ * @param string $value The expected content of the token
+ * @param int|string $nextToken The expected next token.
+ * @param string $nextContent The expected content of the next token.
+ *
+ * @dataProvider dataExplicitOctalNotation
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testExplicitOctalNotation($marker, $value, $nextToken, $nextContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $number = $this->getTargetToken($marker, [T_LNUMBER]);
+
+ $this->assertSame($value, $tokens[$number]['content'], 'Content of integer token does not match expectation');
+
+ $this->assertSame($nextToken, $tokens[($number + 1)]['code'], 'Next token is not the expected type, but '.$tokens[($number + 1)]['type']);
+ $this->assertSame($nextContent, $tokens[($number + 1)]['content'], 'Next token did not have the expected contents');
+
+ }//end testExplicitOctalNotation()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testExplicitOctalNotation()
+ *
+ * @return array
+ */
+ public function dataExplicitOctalNotation()
+ {
+ return [
+ [
+ 'marker' => '/* testExplicitOctal */',
+ 'value' => '0o137041',
+ 'nextToken' => T_SEMICOLON,
+ 'nextContent' => ';',
+ ],
+ [
+ 'marker' => '/* testExplicitOctalCapitalised */',
+ 'value' => '0O137041',
+ 'nextToken' => T_SEMICOLON,
+ 'nextContent' => ';',
+ ],
+ [
+ 'marker' => '/* testExplicitOctalWithNumericSeparator */',
+ 'value' => '0o137_041',
+ 'nextToken' => T_SEMICOLON,
+ 'nextContent' => ';',
+ ],
+ [
+ 'marker' => '/* testInvalid1 */',
+ 'value' => '0',
+ 'nextToken' => T_STRING,
+ 'nextContent' => 'o_137',
+ ],
+ [
+ 'marker' => '/* testInvalid2 */',
+ 'value' => '0',
+ 'nextToken' => T_STRING,
+ 'nextContent' => 'O_41',
+ ],
+ [
+ 'marker' => '/* testInvalid3 */',
+ 'value' => '0',
+ 'nextToken' => T_STRING,
+ 'nextContent' => 'o91',
+ ],
+ [
+ 'marker' => '/* testInvalid4 */',
+ 'value' => '0O2',
+ 'nextToken' => T_LNUMBER,
+ 'nextContent' => '82',
+ ],
+ [
+ 'marker' => '/* testInvalid5 */',
+ 'value' => '0o2',
+ 'nextToken' => T_LNUMBER,
+ 'nextContent' => '8_2',
+ ],
+ [
+ 'marker' => '/* testInvalid6 */',
+ 'value' => '0o2',
+ 'nextToken' => T_STRING,
+ 'nextContent' => '_82',
+ ],
+ [
+ 'marker' => '/* testInvalid7 */',
+ 'value' => '0',
+ 'nextToken' => T_STRING,
+ 'nextContent' => 'o',
+ ],
+ ];
+
+ }//end dataExplicitOctalNotation()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc
index 7714a78680..13f165b77f 100644
--- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc
+++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc
@@ -23,6 +23,9 @@ function fn() {}
/* testNestedOuter */
$fn = fn($x) => /* testNestedInner */ fn($y) => $x * $y + $z;
+/* testNestedSharedCloserOuter */
+$foo = foo(fn() => /* testNestedSharedCloserInner */ fn() => bar() === true);
+
/* testFunctionCall */
$extended = fn($c) => $callable($factory($c), $c);
@@ -40,6 +43,9 @@ $extended = fn($c) => $callable(function() {
echo 'done';
}, $c);
+/* testArrayIndex */
+$found = in_array_cb($needle, $haystack, fn($array, $needle) => $array[2] === $needle);
+
$result = array_map(
/* testReturnType */
static fn(int $number) : int => $number + 1,
@@ -57,12 +63,20 @@ $a = [
'a' => fn() => return 1,
];
+/* testArrayValueNoTrailingComma */
+$a = [
+ 'a' => fn() => foo()
+];
+
/* testYield */
$a = fn($x) => yield 'k' => $x;
/* testNullableNamespace */
$a = fn(?\DateTime $x) : ?\DateTime => $x;
+/* testNamespaceOperatorInTypes */
+$fn = fn(namespace\Foo $a) : ?namespace\Foo => $a;
+
/* testSelfReturnType */
fn(self $a) : self => $a;
@@ -75,9 +89,61 @@ fn(callable $a) : callable => $a;
/* testArrayReturnType */
fn(array $a) : array => $a;
+/* testStaticReturnType */
+fn(array $a) : static => $a;
+
+/* testUnionParamType */
+$arrowWithUnionParam = fn(int|float $param) : SomeClass => new SomeClass($param);
+
+/* testUnionReturnType */
+$arrowWithUnionReturn = fn($param) : int|float => $param | 10;
+
/* testTernary */
$fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b';
+/* testTernaryWithTypes */
+$fn = fn(int|null $a) : array|null => $a ? null : [];
+
+function matchInArrow($x) {
+ /* testWithMatchValue */
+ $fn = fn($x) => match(true) {
+ 1, 2, 3, 4, 5 => 'foo',
+ default => 'bar',
+ };
+}
+
+function matchInArrowAndMore($x) {
+ /* testWithMatchValueAndMore */
+ $fn = fn($x) => match(true) {
+ 1, 2, 3, 4, 5 => 'foo',
+ default => 'bar',
+ } . 'suffix';
+}
+
+function arrowFunctionInMatchWithTrailingComma($x) {
+ return match ($x) {
+ /* testInMatchNotLastValue */
+ 1 => fn($y) => callMe($y),
+ /* testInMatchLastValueWithTrailingComma */
+ default => fn($y) => callThem($y),
+ };
+}
+
+function arrowFunctionInMatchNoTrailingComma1($x) {
+ return match ($x) {
+ 1 => fn($y) => callMe($y),
+ /* testInMatchLastValueNoTrailingComma1 */
+ default => fn($y) => callThem($y)
+ };
+}
+
+function arrowFunctionInMatchNoTrailingComma2($x) {
+ return match ($x) {
+ /* testInMatchLastValueNoTrailingComma2 */
+ default => fn($y) => 5 * $y
+ };
+}
+
/* testConstantDeclaration */
const FN = 'a';
@@ -124,6 +190,9 @@ $a = MyNS\Sub\Fn($param);
/* testNonArrowNamespaceOperatorFunctionCall */
$a = namespace\fn($param);
+/* testNonArrowFunctionNameWithUnionTypes */
+function fn(int|float $param) : string|null {}
+
/* testLiveCoding */
// Intentional parse error. This has to be the last test in the file.
$fn = fn
diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php
index 4dbb8b43c4..4d0f4c0649 100644
--- a/tests/Core/Tokenizer/BackfillFnTokenTest.php
+++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php
@@ -24,22 +24,10 @@ class BackfillFnTokenTest extends AbstractMethodUnitTest
*/
public function testSimple()
{
- $tokens = self::$phpcsFile->getTokens();
-
foreach (['/* testStandard */', '/* testMixedCase */'] as $comment) {
$token = $this->getTargetToken($comment, T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 5, 12);
}
}//end testSimple()
@@ -54,21 +42,9 @@ public function testSimple()
*/
public function testWhitespace()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testWhitespace */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 6), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 13), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 6), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 13), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 6), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 13), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 6, 13);
}//end testWhitespace()
@@ -82,21 +58,9 @@ public function testWhitespace()
*/
public function testComment()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testComment */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 15), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 15), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 15), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 8, 15);
}//end testComment()
@@ -110,21 +74,9 @@ public function testComment()
*/
public function testHeredoc()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testHeredoc */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 4), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 4), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 4), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 4, 9);
}//end testHeredoc()
@@ -138,21 +90,9 @@ public function testHeredoc()
*/
public function testNestedOuter()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testNestedOuter */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 25), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 25), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 25), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 5, 25);
}//end testNestedOuter()
@@ -169,46 +109,72 @@ public function testNestedInner()
$tokens = self::$phpcsFile->getTokens();
$token = $this->getTargetToken('/* testNestedInner */', T_FN);
- $this->backfillHelper($token);
+ $this->backfillHelper($token, true);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 16), 'Scope closer is not the semicolon token');
+ $expectedScopeOpener = ($token + 5);
+ $expectedScopeCloser = ($token + 16);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer is not the semicolon token');
$opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 16), 'Opener scope closer is not the semicolon token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer is not the semicolon token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 16), 'Closer scope closer is not the semicolon token');
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame(($token - 4), $tokens[$closer]['scope_opener'], 'Closer scope opener is not the arrow token of the "outer" arrow function (shared scope closer)');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer is not the semicolon token');
}//end testNestedInner()
/**
- * Test arrow functions that call functions.
+ * Test nested arrow functions with a shared closer.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
* @return void
*/
- public function testFunctionCall()
+ public function testNestedSharedCloser()
{
$tokens = self::$phpcsFile->getTokens();
- $token = $this->getTargetToken('/* testFunctionCall */', T_FN);
+ $token = $this->getTargetToken('/* testNestedSharedCloserOuter */', T_FN);
$this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 4, 20);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 17), 'Scope closer is not the semicolon token');
+ $token = $this->getTargetToken('/* testNestedSharedCloserInner */', T_FN);
+ $this->backfillHelper($token, true);
+
+ $expectedScopeOpener = ($token + 4);
+ $expectedScopeCloser = ($token + 12);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for "inner" arrow function is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for "inner" arrow function is not the TRUE token');
$opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 17), 'Opener scope closer is not the semicolon token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for "inner" arrow function is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for "inner" arrow function is not the semicolon token');
+
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame(($token - 4), $tokens[$closer]['scope_opener'], 'Closer scope opener for "inner" arrow function is not the arrow token of the "outer" arrow function (shared scope closer)');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for "inner" arrow function is not the TRUE token');
+
+ }//end testNestedSharedCloser()
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 17), 'Closer scope closer is not the semicolon token');
+
+ /**
+ * Test arrow functions that call functions.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testFunctionCall()
+ {
+ $token = $this->getTargetToken('/* testFunctionCall */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 5, 17);
}//end testFunctionCall()
@@ -222,21 +188,9 @@ public function testFunctionCall()
*/
public function testChainedFunctionCall()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testChainedFunctionCall */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer is not the bracket token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer is not the bracket token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer is not the bracket token');
+ $this->scopePositionTestHelper($token, 5, 12, 'bracket');
}//end testChainedFunctionCall()
@@ -250,21 +204,9 @@ public function testChainedFunctionCall()
*/
public function testFunctionArgument()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testFunctionArgument */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 15), 'Scope closer is not the comma token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 15), 'Opener scope closer is not the comma token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 15), 'Closer scope closer is not the comma token');
+ $this->scopePositionTestHelper($token, 8, 15, 'comma');
}//end testFunctionArgument()
@@ -278,23 +220,27 @@ public function testFunctionArgument()
*/
public function testClosure()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testClosure */', T_FN);
$this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 5, 60, 'comma');
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 60), 'Scope closer is not the comma token');
+ }//end testClosure()
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 60), 'Opener scope closer is not the comma token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 60), 'Closer scope closer is not the comma token');
+ /**
+ * Test arrow functions using an array index.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testArrayIndex()
+ {
+ $token = $this->getTargetToken('/* testArrayIndex */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 8, 17, 'comma');
- }//end testClosure()
+ }//end testArrayIndex()
/**
@@ -306,21 +252,9 @@ public function testClosure()
*/
public function testReturnType()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testReturnType */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 11), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the comma token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 11), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the comma token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 11), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the comma token');
+ $this->scopePositionTestHelper($token, 11, 18, 'comma');
}//end testReturnType()
@@ -334,21 +268,9 @@ public function testReturnType()
*/
public function testReference()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testReference */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 6), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 6), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 6), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 6, 9);
}//end testReference()
@@ -362,21 +284,9 @@ public function testReference()
*/
public function testGrouped()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testGrouped */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 8), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 8), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 8), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 5, 8);
}//end testGrouped()
@@ -390,23 +300,27 @@ public function testGrouped()
*/
public function testArrayValue()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testArrayValue */', T_FN);
$this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 4, 9, 'comma');
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 4), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the comma token');
+ }//end testArrayValue()
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 4), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the comma token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 4), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the comma token');
+ /**
+ * Test arrow functions that are used as array values with no trailing comma.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testArrayValueNoTrailingComma()
+ {
+ $token = $this->getTargetToken('/* testArrayValueNoTrailingComma */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 4, 8, 'closing parenthesis');
- }//end testArrayValue()
+ }//end testArrayValueNoTrailingComma()
/**
@@ -418,21 +332,9 @@ public function testArrayValue()
*/
public function testYield()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testYield */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 14), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 14), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 14), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 5, 14);
}//end testYield()
@@ -446,27 +348,31 @@ public function testYield()
*/
public function testNullableNamespace()
{
- $tokens = self::$phpcsFile->getTokens();
-
$token = $this->getTargetToken('/* testNullableNamespace */', T_FN);
$this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 15, 18);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 15), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the semicolon token');
+ }//end testNullableNamespace()
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 15), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the semicolon token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 15), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the semicolon token');
+ /**
+ * Test arrow functions that use the namespace operator in the return type.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testNamespaceOperatorInTypes()
+ {
+ $token = $this->getTargetToken('/* testNamespaceOperatorInTypes */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 16, 19);
- }//end testNullableNamespace()
+ }//end testNamespaceOperatorInTypes()
/**
- * Test arrow functions that use self/parent/callable return types.
+ * Test arrow functions that use self/parent/callable/array/static return types.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
@@ -481,27 +387,63 @@ public function testKeywordReturnTypes()
'Parent',
'Callable',
'Array',
+ 'Static',
];
foreach ($testMarkers as $marker) {
$token = $this->getTargetToken('/* test'.$marker.'ReturnType */', T_FN);
$this->backfillHelper($token);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 11), "Scope opener is not the arrow token (for $marker)");
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 14), "Scope closer is not the semicolon token(for $marker)");
+ $expectedScopeOpener = ($token + 11);
+ $expectedScopeCloser = ($token + 14);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], "Scope opener is not the arrow token (for $marker)");
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], "Scope closer is not the semicolon token(for $marker)");
$opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 11), "Opener scope opener is not the arrow token(for $marker)");
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 14), "Opener scope closer is not the semicolon token(for $marker)");
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], "Opener scope opener is not the arrow token(for $marker)");
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], "Opener scope closer is not the semicolon token(for $marker)");
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 11), "Closer scope opener is not the arrow token(for $marker)");
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 14), "Closer scope closer is not the semicolon token(for $marker)");
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], "Closer scope opener is not the arrow token(for $marker)");
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], "Closer scope closer is not the semicolon token(for $marker)");
}
}//end testKeywordReturnTypes()
+ /**
+ * Test arrow function with a union parameter type.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testUnionParamType()
+ {
+ $token = $this->getTargetToken('/* testUnionParamType */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 13, 21);
+
+ }//end testUnionParamType()
+
+
+ /**
+ * Test arrow function with a union return type.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testUnionReturnType()
+ {
+ $token = $this->getTargetToken('/* testUnionReturnType */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 11, 18);
+
+ }//end testUnionReturnType()
+
+
/**
* Test arrow functions used in ternary operators.
*
@@ -515,73 +457,178 @@ public function testTernary()
$token = $this->getTargetToken('/* testTernary */', T_FN);
$this->backfillHelper($token);
-
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 40), 'Scope closer is not the semicolon token');
-
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 40), 'Opener scope closer is not the semicolon token');
-
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 40), 'Closer scope closer is not the semicolon token');
+ $this->scopePositionTestHelper($token, 5, 40);
$token = $this->getTargetToken('/* testTernaryThen */', T_FN);
$this->backfillHelper($token);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener for THEN is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer for THEN is not the semicolon token');
+ $expectedScopeOpener = ($token + 8);
+ $expectedScopeCloser = ($token + 12);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for THEN is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for THEN is not the semicolon token');
$opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener for THEN is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer for THEN is not the semicolon token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for THEN is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for THEN is not the semicolon token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener for THEN is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer for THEN is not the semicolon token');
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'Closer scope opener for THEN is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for THEN is not the semicolon token');
$token = $this->getTargetToken('/* testTernaryElse */', T_FN);
- $this->backfillHelper($token);
+ $this->backfillHelper($token, true);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener for ELSE is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 11), 'Scope closer for ELSE is not the semicolon token');
+ $expectedScopeOpener = ($token + 8);
+ $expectedScopeCloser = ($token + 11);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for ELSE is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for ELSE is not the semicolon token');
$opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener for ELSE is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 11), 'Opener scope closer for ELSE is not the semicolon token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for ELSE is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for ELSE is not the semicolon token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener for ELSE is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 11), 'Closer scope closer for ELSE is not the semicolon token');
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame(($token - 24), $tokens[$closer]['scope_opener'], 'Closer scope opener for ELSE is not the arrow token of the "outer" arrow function (shared scope closer)');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for ELSE is not the semicolon token');
}//end testTernary()
/**
- * Test arrow function nested within a method declaration.
+ * Test typed arrow functions used in ternary operators.
*
* @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
*
* @return void
*/
- public function testNestedInMethod()
+ public function testTernaryWithTypes()
{
$tokens = self::$phpcsFile->getTokens();
- $token = $this->getTargetToken('/* testNestedInMethod */', T_FN);
+ $token = $this->getTargetToken('/* testTernaryWithTypes */', T_FN);
$this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 15, 27);
- $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token');
- $this->assertSame($tokens[$token]['scope_closer'], ($token + 17), 'Scope closer is not the semicolon token');
+ }//end testTernaryWithTypes()
- $opener = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token');
- $this->assertSame($tokens[$opener]['scope_closer'], ($token + 17), 'Opener scope closer is not the semicolon token');
- $closer = $tokens[$token]['scope_opener'];
- $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token');
- $this->assertSame($tokens[$closer]['scope_closer'], ($token + 17), 'Closer scope closer is not the semicolon token');
+ /**
+ * Test arrow function returning a match control structure.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testWithMatchValue()
+ {
+ $token = $this->getTargetToken('/* testWithMatchValue */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 5, 44);
+
+ }//end testWithMatchValue()
+
+
+ /**
+ * Test arrow function returning a match control structure with something behind it.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testWithMatchValueAndMore()
+ {
+ $token = $this->getTargetToken('/* testWithMatchValueAndMore */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 5, 48);
+
+ }//end testWithMatchValueAndMore()
+
+
+ /**
+ * Test match control structure returning arrow functions.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
+ * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
+ * @param string $expectedCloserType The type of token expected for the scope closer.
+ * @param string $expectedCloserFriendlyName A friendly name for the type of token expected for the scope closer
+ * to be used in the error message for failing tests.
+ *
+ * @dataProvider dataInMatchValue
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testInMatchValue($testMarker, $openerOffset, $closerOffset, $expectedCloserType, $expectedCloserFriendlyName)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, $openerOffset, $closerOffset, $expectedCloserFriendlyName);
+
+ $this->assertSame($expectedCloserType, $tokens[($token + $closerOffset)]['type'], 'Mismatched scope closer type');
+
+ }//end testInMatchValue()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testInMatchValue()
+ *
+ * @return array
+ */
+ public function dataInMatchValue()
+ {
+ return [
+ 'not_last_value' => [
+ '/* testInMatchNotLastValue */',
+ 5,
+ 11,
+ 'T_COMMA',
+ 'comma',
+ ],
+ 'last_value_with_trailing_comma' => [
+ '/* testInMatchLastValueWithTrailingComma */',
+ 5,
+ 11,
+ 'T_COMMA',
+ 'comma',
+ ],
+ 'last_value_without_trailing_comma_1' => [
+ '/* testInMatchLastValueNoTrailingComma1 */',
+ 5,
+ 10,
+ 'T_CLOSE_PARENTHESIS',
+ 'close parenthesis',
+ ],
+ 'last_value_without_trailing_comma_2' => [
+ '/* testInMatchLastValueNoTrailingComma2 */',
+ 5,
+ 11,
+ 'T_VARIABLE',
+ '$y variable',
+ ],
+ ];
+
+ }//end dataInMatchValue()
+
+
+ /**
+ * Test arrow function nested within a method declaration.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testNestedInMethod()
+ {
+ $token = $this->getTargetToken('/* testNestedInMethod */', T_FN);
+ $this->backfillHelper($token);
+ $this->scopePositionTestHelper($token, 5, 17);
}//end testNestedInMethod()
@@ -652,15 +699,16 @@ public function dataNotAnArrowFunction()
'Fn',
],
['/* testNonArrowObjectMethodCall */'],
- [
- '/* testNonArrowNamespacedFunctionCall */',
- 'Fn',
- ],
[
'/* testNonArrowObjectMethodCallUpper */',
'FN',
],
+ [
+ '/* testNonArrowNamespacedFunctionCall */',
+ 'Fn',
+ ],
['/* testNonArrowNamespaceOperatorFunctionCall */'],
+ ['/* testNonArrowFunctionNameWithUnionTypes */'],
['/* testLiveCoding */'],
];
@@ -670,11 +718,16 @@ public function dataNotAnArrowFunction()
/**
* Helper function to check that all token keys are correctly set for T_FN tokens.
*
- * @param string $token The T_FN token to check.
+ * @param int $token The T_FN token to check.
+ * @param bool $skipScopeCloserCheck Whether to skip the scope closer check.
+ * This should be set to "true" when testing nested arrow functions,
+ * where the "inner" arrow function shares a scope closer with the
+ * "outer" arrow function, as the 'scope_condition' for the scope closer
+ * of the "inner" arrow function will point to the "outer" arrow function.
*
* @return void
*/
- private function backfillHelper($token)
+ private function backfillHelper($token, $skipScopeCloserCheck=false)
{
$tokens = self::$phpcsFile->getTokens();
@@ -692,12 +745,16 @@ private function backfillHelper($token)
$this->assertTrue(array_key_exists('scope_opener', $tokens[$opener]), 'Opener scope opener is not set');
$this->assertTrue(array_key_exists('scope_closer', $tokens[$opener]), 'Opener scope closer is not set');
$this->assertSame($tokens[$opener]['scope_condition'], $token, 'Opener scope condition is not the T_FN token');
+ $this->assertSame(T_FN_ARROW, $tokens[$opener]['code'], 'Arrow scope opener not tokenized as T_FN_ARROW (code)');
+ $this->assertSame('T_FN_ARROW', $tokens[$opener]['type'], 'Arrow scope opener not tokenized as T_FN_ARROW (type)');
- $closer = $tokens[$token]['scope_opener'];
+ $closer = $tokens[$token]['scope_closer'];
$this->assertTrue(array_key_exists('scope_condition', $tokens[$closer]), 'Closer scope condition is not set');
$this->assertTrue(array_key_exists('scope_opener', $tokens[$closer]), 'Closer scope opener is not set');
$this->assertTrue(array_key_exists('scope_closer', $tokens[$closer]), 'Closer scope closer is not set');
- $this->assertSame($tokens[$closer]['scope_condition'], $token, 'Closer scope condition is not the T_FN token');
+ if ($skipScopeCloserCheck === false) {
+ $this->assertSame($tokens[$closer]['scope_condition'], $token, 'Closer scope condition is not the T_FN token');
+ }
$opener = $tokens[$token]['parenthesis_opener'];
$this->assertTrue(array_key_exists('parenthesis_owner', $tokens[$opener]), 'Opening parenthesis owner is not set');
@@ -710,4 +767,36 @@ private function backfillHelper($token)
}//end backfillHelper()
+ /**
+ * Helper function to check that the scope opener/closer positions are correctly set for T_FN tokens.
+ *
+ * @param int $token The T_FN token to check.
+ * @param int $openerOffset The expected offset of the scope opener in relation to
+ * the fn keyword.
+ * @param int $closerOffset The expected offset of the scope closer in relation to
+ * the fn keyword.
+ * @param string $expectedCloserType Optional. The type of token expected for the scope closer.
+ *
+ * @return void
+ */
+ private function scopePositionTestHelper($token, $openerOffset, $closerOffset, $expectedCloserType='semicolon')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $expectedScopeOpener = ($token + $openerOffset);
+ $expectedScopeCloser = ($token + $closerOffset);
+
+ $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer is not the '.$expectedCloserType.' token');
+
+ $opener = $tokens[$token]['scope_opener'];
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer is not the '.$expectedCloserType.' token');
+
+ $closer = $tokens[$token]['scope_closer'];
+ $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'Closer scope opener is not the arrow token');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer is not the '.$expectedCloserType.' token');
+
+ }//end scopePositionTestHelper()
+
+
}//end class
diff --git a/tests/Core/Tokenizer/BackfillMatchTokenTest.inc b/tests/Core/Tokenizer/BackfillMatchTokenTest.inc
new file mode 100644
index 0000000000..095a4d7dfc
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillMatchTokenTest.inc
@@ -0,0 +1,319 @@
+ 'Zero',
+ 1 => 'One',
+ 2 => 'Two',
+ };
+}
+
+function matchNoTrailingComma($bool) {
+ /* testMatchNoTrailingComma */
+ echo match ($bool) {
+ true => "true\n",
+ false => "false\n"
+ };
+}
+
+function matchWithDefault($i) {
+ /* testMatchWithDefault */
+ return match ($i) {
+ 1 => 1,
+ 2 => 2,
+ default => 'default',
+ };
+}
+
+function matchExpressionInCondition($i) {
+ /* testMatchExpressionInCondition */
+ return match (true) {
+ $i >= 50 => '50+',
+ $i >= 40 => '40-50',
+ $i >= 30 => '30-40',
+ $i >= 20 => '20-30',
+ $i >= 10 => '10-20',
+ default => '0-10',
+ };
+}
+
+function matchMultiCase($day) {
+ /* testMatchMultiCase */
+ return match ($day) {
+ 1, 7 => false,
+ 2, 3, 4, 5, 6 => true,
+ };
+}
+
+function matchMultiCaseTrailingCommaInCase($bool) {
+ /* testMatchMultiCaseTrailingCommaInCase */
+ echo match ($bool) {
+ false,
+ 0,
+ => "false\n",
+ true,
+ 1,
+ => "true\n",
+ default,
+ => "not bool\n",
+ };
+}
+
+assert((function () {
+ /* testMatchInClosureNotLowercase */
+ Match ('foo') {
+ 'foo', 'bar' => false,
+ 'baz' => 'a',
+ default => 'b',
+ };
+})());
+
+function matchInArrowFunction($x) {
+ /* testMatchInArrowFunction */
+ $fn = fn($x) => match(true) {
+ 1, 2, 3, 4, 5 => 'foo',
+ default => 'bar',
+ };
+}
+
+function arrowFunctionInMatchNoTrailingComma($x) {
+ /* testArrowFunctionInMatchNoTrailingComma */
+ return match ($x) {
+ 1 => fn($y) => callMe($y),
+ default => fn($y) => callThem($y)
+ };
+}
+
+/* testMatchInFunctionCallParamNotLowercase */
+var_dump(MATCH ( 'foo' ) {
+ 'foo' => dump_and_return('foo'),
+ 'bar' => dump_and_return('bar'),
+});
+
+/* testMatchInMethodCallParam */
+Test::usesValue(match(true) { true => $i });
+
+/* testMatchDiscardResult */
+match (1) {
+ 1 => print "Executed\n",
+};
+
+/* testMatchWithDuplicateConditionsWithComments */
+echo match /*comment*/ ( $value /*comment*/ ) {
+ // Comment.
+ 2, 1 => '2, 1',
+ 1 => 1,
+ 3 => 3,
+ 4 => 4,
+ 5 => 5,
+};
+
+/* testNestedMatchOuter */
+$x = match ($y) {
+ /* testNestedMatchInner */
+ default => match ($z) { 1 => 1 },
+};
+
+/* testMatchInTernaryCondition */
+$x = match ($test) { 1 => 'a', 2 => 'b' } ?
+ /* testMatchInTernaryThen */ match ($test) { 1 => 'a', 2 => 'b' } :
+ /* testMatchInTernaryElse */ match ($notTest) { 3 => 'a', 4 => 'b' };
+
+/* testMatchInArrayValue */
+$array = array(
+ match ($test) { 1 => 'a', 2 => 'b' },
+);
+
+/* testMatchInArrayKey */
+$array = [
+ match ($test) { 1 => 'a', 2 => 'b' } => 'dynamic keys, woho!',
+];
+
+/* testMatchreturningArray */
+$matcher = match ($x) {
+ 0 => array( 0 => 1, 'a' => 2, 'b' => 3 ),
+ 1 => [1, 2, 3],
+ 2 => array( 1, [1, 2, 3], 2, 3),
+ 3 => [ 0 => 1, 'a' => array(1, 2, 3), 'b' => 2, 3],
+};
+
+/* testSwitchContainingMatch */
+switch ($something) {
+ /* testMatchWithDefaultNestedInSwitchCase1 */
+ case 'foo':
+ $var = [1, 2, 3];
+ $var = match ($i) {
+ 1 => 1,
+ default => 'default',
+ };
+ continue 2;
+
+ /* testMatchWithDefaultNestedInSwitchCase2 */
+ case 'bar' ;
+ $i = callMe($a, $b);
+ return match ($i) {
+ 1 => 1,
+ default => 'default',
+ };
+
+ /* testMatchWithDefaultNestedInSwitchDefault */
+ default:
+ echo 'something', match ($i) {
+ 1 => 1,
+ default => 'default',
+ };
+ break;
+}
+
+/* testMatchContainingSwitch */
+$x = match ($y) {
+ 5, 8 => function($z) {
+ /* testSwitchNestedInMatch1 */
+ switch($z) {
+ case 'a':
+ $var = [1, 2, 3];
+ return 'a';
+ /* testSwitchDefaultNestedInMatchCase */
+ default:
+ $i = callMe($a, $b);
+ return 'default1';
+ }
+ },
+ default => function($z) {
+ /* testSwitchNestedInMatch2 */
+ switch($z) {
+ case 'a';
+ $i = callMe($a, $b);
+ return 'b';
+ /* testSwitchDefaultNestedInMatchDefault */
+ default;
+ $var = [1, 2, 3];
+ return 'default2';
+ }
+ }
+};
+
+/* testMatchNoCases */
+// Intentional fatal error.
+$x = match (true) {};
+
+/* testMatchMultiDefault */
+// Intentional fatal error.
+echo match (1) {
+ default => 'foo',
+ 1 => 'bar',
+ 2 => 'baz',
+ default => 'qux',
+};
+
+/* testNoMatchStaticMethodCall */
+$a = Foo::match($param);
+
+/* testNoMatchClassConstantAccess */
+$a = MyClass::MATCH;
+
+/* testNoMatchClassConstantArrayAccessMixedCase */
+$a = MyClass::Match[$a];
+
+/* testNoMatchMethodCall */
+$a = $obj->match($param);
+
+/* testNoMatchMethodCallUpper */
+$a = $obj??->MATCH()->chain($param);
+
+/* testNoMatchPropertyAccess */
+$a = $obj->match;
+
+/* testNoMatchNamespacedFunctionCall */
+// Intentional fatal error.
+$a = MyNS\Sub\match($param);
+
+/* testNoMatchNamespaceOperatorFunctionCall */
+// Intentional fatal error.
+$a = namespace\match($param);
+
+interface MatchInterface {
+ /* testNoMatchInterfaceMethodDeclaration */
+ public static function match($param);
+}
+
+class MatchClass {
+ /* testNoMatchClassConstantDeclarationLower */
+ const match = 'a';
+
+ /* testNoMatchClassMethodDeclaration */
+ public static function match($param) {
+ /* testNoMatchPropertyAssignment */
+ $this->match = 'a';
+ }
+}
+
+/* testNoMatchClassInstantiation */
+$obj = new Match();
+
+$anon = new class() {
+ /* testNoMatchAnonClassMethodDeclaration */
+ protected function maTCH($param) {
+ }
+};
+
+/* testNoMatchClassDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+class Match {}
+
+/* testNoMatchInterfaceDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+interface Match {}
+
+/* testNoMatchTraitDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+trait Match {}
+
+/* testNoMatchConstantDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+const MATCH = '1';
+
+/* testNoMatchFunctionDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+function match() {}
+
+/* testNoMatchNamespaceDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+namespace Match {}
+
+/* testNoMatchExtendedClassDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+class Foo extends Match {}
+
+/* testNoMatchImplementedClassDeclaration */
+// Intentional fatal error. Match is now a reserved keyword.
+class Bar implements Match {}
+
+/* testNoMatchInUseStatement */
+// Intentional fatal error in PHP < 8. Match is now a reserved keyword.
+use Match\me;
+
+function brokenMatchNoCurlies($x) {
+ /* testNoMatchMissingCurlies */
+ // Intentional fatal error. New control structure is not supported without curly braces.
+ return match ($x)
+ 0 => 'Zero',
+ 1 => 'One',
+ 2 => 'Two',
+ ;
+}
+
+function brokenMatchAlternativeSyntax($x) {
+ /* testNoMatchAlternativeSyntax */
+ // Intentional fatal error. Alternative syntax is not supported.
+ return match ($x) :
+ 0 => 'Zero',
+ 1 => 'One',
+ 2 => 'Two',
+ endmatch;
+}
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+echo match
diff --git a/tests/Core/Tokenizer/BackfillMatchTokenTest.php b/tests/Core/Tokenizer/BackfillMatchTokenTest.php
new file mode 100644
index 0000000000..80f909acdf
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillMatchTokenTest.php
@@ -0,0 +1,529 @@
+
+ * @copyright 2020-2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class BackfillMatchTokenTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test tokenization of match expressions.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
+ * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataMatchExpression
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testMatchExpression($testMarker, $openerOffset, $closerOffset, $testContent='match')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_MATCH, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (code)');
+ $this->assertSame('T_MATCH', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (type)');
+
+ $this->scopeTestHelper($token, $openerOffset, $closerOffset);
+ $this->parenthesisTestHelper($token);
+
+ }//end testMatchExpression()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testMatchExpression()
+ *
+ * @return array
+ */
+ public function dataMatchExpression()
+ {
+ return [
+ 'simple_match' => [
+ '/* testMatchSimple */',
+ 6,
+ 33,
+ ],
+ 'no_trailing_comma' => [
+ '/* testMatchNoTrailingComma */',
+ 6,
+ 24,
+ ],
+ 'with_default_case' => [
+ '/* testMatchWithDefault */',
+ 6,
+ 33,
+ ],
+ 'expression_in_condition' => [
+ '/* testMatchExpressionInCondition */',
+ 6,
+ 77,
+ ],
+ 'multicase' => [
+ '/* testMatchMultiCase */',
+ 6,
+ 40,
+ ],
+ 'multicase_trailing_comma_in_case' => [
+ '/* testMatchMultiCaseTrailingCommaInCase */',
+ 6,
+ 47,
+ ],
+ 'in_closure_not_lowercase' => [
+ '/* testMatchInClosureNotLowercase */',
+ 6,
+ 36,
+ 'Match',
+ ],
+ 'in_arrow_function' => [
+ '/* testMatchInArrowFunction */',
+ 5,
+ 36,
+ ],
+ 'arrow_function_in_match_no_trailing_comma' => [
+ '/* testArrowFunctionInMatchNoTrailingComma */',
+ 6,
+ 44,
+ ],
+ 'in_function_call_param_not_lowercase' => [
+ '/* testMatchInFunctionCallParamNotLowercase */',
+ 8,
+ 32,
+ 'MATCH',
+ ],
+ 'in_method_call_param' => [
+ '/* testMatchInMethodCallParam */',
+ 5,
+ 13,
+ ],
+ 'discard_result' => [
+ '/* testMatchDiscardResult */',
+ 6,
+ 18,
+ ],
+ 'duplicate_conditions_and_comments' => [
+ '/* testMatchWithDuplicateConditionsWithComments */',
+ 12,
+ 59,
+ ],
+ 'nested_match_outer' => [
+ '/* testNestedMatchOuter */',
+ 6,
+ 33,
+ ],
+ 'nested_match_inner' => [
+ '/* testNestedMatchInner */',
+ 6,
+ 14,
+ ],
+ 'ternary_condition' => [
+ '/* testMatchInTernaryCondition */',
+ 6,
+ 21,
+ ],
+ 'ternary_then' => [
+ '/* testMatchInTernaryThen */',
+ 6,
+ 21,
+ ],
+ 'ternary_else' => [
+ '/* testMatchInTernaryElse */',
+ 6,
+ 21,
+ ],
+ 'array_value' => [
+ '/* testMatchInArrayValue */',
+ 6,
+ 21,
+ ],
+ 'array_key' => [
+ '/* testMatchInArrayKey */',
+ 6,
+ 21,
+ ],
+ 'returning_array' => [
+ '/* testMatchreturningArray */',
+ 6,
+ 125,
+ ],
+ 'nested_in_switch_case_1' => [
+ '/* testMatchWithDefaultNestedInSwitchCase1 */',
+ 6,
+ 25,
+ ],
+ 'nested_in_switch_case_2' => [
+ '/* testMatchWithDefaultNestedInSwitchCase2 */',
+ 6,
+ 25,
+ ],
+ 'nested_in_switch_default' => [
+ '/* testMatchWithDefaultNestedInSwitchDefault */',
+ 6,
+ 25,
+ ],
+ 'match_with_nested_switch' => [
+ '/* testMatchContainingSwitch */',
+ 6,
+ 180,
+ ],
+ 'no_cases' => [
+ '/* testMatchNoCases */',
+ 6,
+ 7,
+ ],
+ 'multi_default' => [
+ '/* testMatchMultiDefault */',
+ 6,
+ 40,
+ ],
+ ];
+
+ }//end dataMatchExpression()
+
+
+ /**
+ * Verify that "match" keywords which are not match control structures get tokenized as T_STRING
+ * and don't have the extra token array indexes.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotAMatchStructure
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testNotAMatchStructure($testMarker, $testContent='match')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set');
+ $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set');
+ $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set');
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
+ $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
+ $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');
+
+ $next = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($token + 1), null, true);
+ if ($next !== false && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set for opener after');
+ }
+
+ }//end testNotAMatchStructure()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotAMatchStructure()
+ *
+ * @return array
+ */
+ public function dataNotAMatchStructure()
+ {
+ return [
+ 'static_method_call' => ['/* testNoMatchStaticMethodCall */'],
+ 'class_constant_access' => [
+ '/* testNoMatchClassConstantAccess */',
+ 'MATCH',
+ ],
+ 'class_constant_array_access' => [
+ '/* testNoMatchClassConstantArrayAccessMixedCase */',
+ 'Match',
+ ],
+ 'method_call' => ['/* testNoMatchMethodCall */'],
+ 'method_call_uppercase' => [
+ '/* testNoMatchMethodCallUpper */',
+ 'MATCH',
+ ],
+ 'property_access' => ['/* testNoMatchPropertyAccess */'],
+ 'namespaced_function_call' => ['/* testNoMatchNamespacedFunctionCall */'],
+ 'namespace_operator_function_call' => ['/* testNoMatchNamespaceOperatorFunctionCall */'],
+ 'interface_method_declaration' => ['/* testNoMatchInterfaceMethodDeclaration */'],
+ 'class_constant_declaration' => ['/* testNoMatchClassConstantDeclarationLower */'],
+ 'class_method_declaration' => ['/* testNoMatchClassMethodDeclaration */'],
+ 'property_assigment' => ['/* testNoMatchPropertyAssignment */'],
+ 'class_instantiation' => [
+ '/* testNoMatchClassInstantiation */',
+ 'Match',
+ ],
+ 'anon_class_method_declaration' => [
+ '/* testNoMatchAnonClassMethodDeclaration */',
+ 'maTCH',
+ ],
+ 'class_declaration' => [
+ '/* testNoMatchClassDeclaration */',
+ 'Match',
+ ],
+ 'interface_declaration' => [
+ '/* testNoMatchInterfaceDeclaration */',
+ 'Match',
+ ],
+ 'trait_declaration' => [
+ '/* testNoMatchTraitDeclaration */',
+ 'Match',
+ ],
+ 'constant_declaration' => [
+ '/* testNoMatchConstantDeclaration */',
+ 'MATCH',
+ ],
+ 'function_declaration' => ['/* testNoMatchFunctionDeclaration */'],
+ 'namespace_declaration' => [
+ '/* testNoMatchNamespaceDeclaration */',
+ 'Match',
+ ],
+ 'class_extends_declaration' => [
+ '/* testNoMatchExtendedClassDeclaration */',
+ 'Match',
+ ],
+ 'class_implements_declaration' => [
+ '/* testNoMatchImplementedClassDeclaration */',
+ 'Match',
+ ],
+ 'use_statement' => [
+ '/* testNoMatchInUseStatement */',
+ 'Match',
+ ],
+ 'unsupported_inline_control_structure' => ['/* testNoMatchMissingCurlies */'],
+ 'unsupported_alternative_syntax' => ['/* testNoMatchAlternativeSyntax */'],
+ 'live_coding' => ['/* testLiveCoding */'],
+ ];
+
+ }//end dataNotAMatchStructure()
+
+
+ /**
+ * Verify that the tokenization of switch structures is not affected by the backfill.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
+ * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
+ *
+ * @dataProvider dataSwitchExpression
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testSwitchExpression($testMarker, $openerOffset, $closerOffset)
+ {
+ $token = $this->getTargetToken($testMarker, T_SWITCH);
+
+ $this->scopeTestHelper($token, $openerOffset, $closerOffset);
+ $this->parenthesisTestHelper($token);
+
+ }//end testSwitchExpression()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testSwitchExpression()
+ *
+ * @return array
+ */
+ public function dataSwitchExpression()
+ {
+ return [
+ 'switch_containing_match' => [
+ '/* testSwitchContainingMatch */',
+ 6,
+ 174,
+ ],
+ 'match_containing_switch_1' => [
+ '/* testSwitchNestedInMatch1 */',
+ 5,
+ 63,
+ ],
+ 'match_containing_switch_2' => [
+ '/* testSwitchNestedInMatch2 */',
+ 5,
+ 63,
+ ],
+ ];
+
+ }//end dataSwitchExpression()
+
+
+ /**
+ * Verify that the tokenization of a switch case/default structure containing a match structure
+ * or contained *in* a match structure is not affected by the backfill.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
+ * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
+ *
+ * @dataProvider dataSwitchCaseVersusMatch
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testSwitchCaseVersusMatch($testMarker, $openerOffset, $closerOffset)
+ {
+ $token = $this->getTargetToken($testMarker, [T_CASE, T_DEFAULT]);
+
+ $this->scopeTestHelper($token, $openerOffset, $closerOffset);
+
+ }//end testSwitchCaseVersusMatch()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testSwitchCaseVersusMatch()
+ *
+ * @return array
+ */
+ public function dataSwitchCaseVersusMatch()
+ {
+ return [
+ 'switch_with_nested_match_case_1' => [
+ '/* testMatchWithDefaultNestedInSwitchCase1 */',
+ 3,
+ 55,
+ ],
+ 'switch_with_nested_match_case_2' => [
+ '/* testMatchWithDefaultNestedInSwitchCase2 */',
+ 4,
+ 21,
+ ],
+ 'switch_with_nested_match_default_case' => [
+ '/* testMatchWithDefaultNestedInSwitchDefault */',
+ 1,
+ 38,
+ ],
+ 'match_with_nested_switch_case' => [
+ '/* testSwitchDefaultNestedInMatchCase */',
+ 1,
+ 18,
+ ],
+ 'match_with_nested_switch_default_case' => [
+ '/* testSwitchDefaultNestedInMatchDefault */',
+ 1,
+ 20,
+ ],
+ ];
+
+ }//end dataSwitchCaseVersusMatch()
+
+
+ /**
+ * Helper function to verify that all scope related array indexes for a control structure
+ * are set correctly.
+ *
+ * @param string $token The control structure token to check.
+ * @param int $openerOffset The expected offset of the scope opener in relation to
+ * the control structure token.
+ * @param int $closerOffset The expected offset of the scope closer in relation to
+ * the control structure token.
+ * @param bool $skipScopeCloserCheck Whether to skip the scope closer check.
+ * This should be set to "true" when testing nested arrow functions,
+ * where the "inner" arrow function shares a scope closer with the
+ * "outer" arrow function, as the 'scope_condition' for the scope closer
+ * of the "inner" arrow function will point to the "outer" arrow function.
+ *
+ * @return void
+ */
+ private function scopeTestHelper($token, $openerOffset, $closerOffset, $skipScopeCloserCheck=false)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $tokenArray = $tokens[$token];
+ $tokenType = $tokenArray['type'];
+ $expectedScopeOpener = ($token + $openerOffset);
+ $expectedScopeCloser = ($token + $closerOffset);
+
+ $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set');
+ $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the '.$tokenType.' token');
+ $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the '.$tokenType.' token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the '.$tokenType.' token incorrect');
+
+ $opener = $tokenArray['scope_opener'];
+ $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set');
+ $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the '.$tokenType.' token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], $tokenType.' opener scope opener token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], $tokenType.' opener scope closer token incorrect');
+
+ $closer = $tokenArray['scope_closer'];
+ $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set');
+ if ($skipScopeCloserCheck === false) {
+ $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the '.$tokenType.' token');
+ }
+
+ $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], $tokenType.' closer scope opener token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], $tokenType.' closer scope closer token incorrect');
+
+ if (($opener + 1) !== $closer) {
+ for ($i = ($opener + 1); $i < $closer; $i++) {
+ $this->assertArrayHasKey(
+ $token,
+ $tokens[$i]['conditions'],
+ $tokenType.' condition not added for token belonging to the '.$tokenType.' structure'
+ );
+ }
+ }
+
+ }//end scopeTestHelper()
+
+
+ /**
+ * Helper function to verify that all parenthesis related array indexes for a control structure
+ * token are set correctly.
+ *
+ * @param int $token The position of the control structure token.
+ *
+ * @return void
+ */
+ private function parenthesisTestHelper($token)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $tokenArray = $tokens[$token];
+ $tokenType = $tokenArray['type'];
+
+ $this->assertArrayHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is not set');
+ $this->assertArrayHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is not set');
+ $this->assertArrayHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is not set');
+ $this->assertSame($token, $tokenArray['parenthesis_owner'], 'Parenthesis owner is not the '.$tokenType.' token');
+
+ $opener = $tokenArray['parenthesis_opener'];
+ $this->assertArrayHasKey('parenthesis_owner', $tokens[$opener], 'Opening parenthesis owner is not set');
+ $this->assertSame($token, $tokens[$opener]['parenthesis_owner'], 'Opening parenthesis owner is not the '.$tokenType.' token');
+
+ $closer = $tokenArray['parenthesis_closer'];
+ $this->assertArrayHasKey('parenthesis_owner', $tokens[$closer], 'Closing parenthesis owner is not set');
+ $this->assertSame($token, $tokens[$closer]['parenthesis_owner'], 'Closing parenthesis owner is not the '.$tokenType.' token');
+
+ }//end parenthesisTestHelper()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc
index eef53f593b..d8559705c3 100644
--- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc
+++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc
@@ -34,6 +34,12 @@ $foo = 0b0101_1111;
/* testOctal */
$foo = 0137_041;
+/* testExplicitOctal */
+$foo = 0o137_041;
+
+/* testExplicitOctalCapitalised */
+$foo = 0O137_041;
+
/* testIntMoreThanMax */
$foo = 10_223_372_036_854_775_807;
@@ -71,6 +77,12 @@ $testValue = 107_925_284 .88;
/* testInvalid10 */
$testValue = 107_925_284/*comment*/.88;
+/* testInvalid11 */
+$foo = 0o_137;
+
+/* testInvalid12 */
+$foo = 0O_41;
+
/*
* Ensure that legitimate calculations are not touched by the backfill.
*/
diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php
index 443cc30ef1..645088fd6c 100644
--- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php
+++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php
@@ -1,6 +1,6 @@
* @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600)
@@ -16,7 +16,7 @@ class BackfillNumericSeparatorTest extends AbstractMethodUnitTest
/**
- * Test that numbers using numeric seperators are tokenized correctly.
+ * Test that numbers using numeric separators are tokenized correctly.
*
* @param array $testData The data required for the specific test case.
*
@@ -132,6 +132,20 @@ public function dataTestBackfill()
'value' => '0137_041',
],
],
+ [
+ [
+ 'marker' => '/* testExplicitOctal */',
+ 'type' => 'T_LNUMBER',
+ 'value' => '0o137_041',
+ ],
+ ],
+ [
+ [
+ 'marker' => '/* testExplicitOctalCapitalised */',
+ 'type' => 'T_LNUMBER',
+ 'value' => '0O137_041',
+ ],
+ ],
[
[
'marker' => '/* testIntMoreThanMax */',
@@ -145,7 +159,7 @@ public function dataTestBackfill()
/**
- * Test that numbers using numeric seperators which are considered parse errors and/or
+ * Test that numbers using numeric separators which are considered parse errors and/or
* which aren't relevant to the backfill, do not incorrectly trigger the backfill anyway.
*
* @param string $testMarker The comment which prefaces the target token in the test file.
@@ -322,6 +336,32 @@ public function dataNoBackfill()
],
],
],
+ [
+ '/* testInvalid11 */',
+ [
+ [
+ 'code' => T_LNUMBER,
+ 'content' => '0',
+ ],
+ [
+ 'code' => T_STRING,
+ 'content' => 'o_137',
+ ],
+ ],
+ ],
+ [
+ '/* testInvalid12 */',
+ [
+ [
+ 'code' => T_LNUMBER,
+ 'content' => '0',
+ ],
+ [
+ 'code' => T_STRING,
+ 'content' => 'O_41',
+ ],
+ ],
+ ],
[
'/* testCalc1 */',
[
diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.inc b/tests/Core/Tokenizer/BackfillReadonlyTest.inc
new file mode 100644
index 0000000000..eaf0b4b3cc
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillReadonlyTest.inc
@@ -0,0 +1,100 @@
+readonly = 'foo';
+
+ /* testReadonlyPropertyInTernaryOperator */
+ $isReadonly = $this->readonly ? true : false;
+ }
+}
+
+/* testReadonlyUsedAsFunctionName */
+function readonly()
+{
+}
+
+/* testReadonlyUsedAsNamespaceName */
+namespace Readonly;
+/* testReadonlyUsedAsPartOfNamespaceName */
+namespace My\Readonly\Collection;
+/* testReadonlyAsFunctionCall */
+$var = readonly($a, $b);
+/* testClassConstantFetchWithReadonlyAsConstantName */
+echo ClassName::READONLY;
+
+/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */
+$var = readonly /* comment */ ();
+
+/* testParseErrorLiveCoding */
+// This must be the last test in the file.
+readonly
diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.php b/tests/Core/Tokenizer/BackfillReadonlyTest.php
new file mode 100644
index 0000000000..dddc18ebc2
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillReadonlyTest.php
@@ -0,0 +1,236 @@
+
+ * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class BackfillReadonlyTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the "readonly" keyword is tokenized as such.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataReadonly
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testReadonly($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent);
+ $this->assertSame(T_READONLY, $tokens[$target]['code']);
+ $this->assertSame('T_READONLY', $tokens[$target]['type']);
+
+ }//end testReadonly()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testReadonly()
+ *
+ * @return array
+ */
+ public function dataReadonly()
+ {
+ return [
+ [
+ '/* testReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testVarReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyVarProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testStaticReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyStaticProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testConstReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyWithoutType */',
+ 'readonly',
+ ],
+ [
+ '/* testPublicReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testProtectedReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testPrivateReadonlyProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testPublicReadonlyPropertyWithReadonlyFirst */',
+ 'readonly',
+ ],
+ [
+ '/* testProtectedReadonlyPropertyWithReadonlyFirst */',
+ 'readonly',
+ ],
+ [
+ '/* testPrivateReadonlyPropertyWithReadonlyFirst */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyWithCommentsInDeclaration */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyWithNullableProperty */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullFirst */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullLast */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyWithArrayTypeHint */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyWithSelfTypeHint */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyWithParentTypeHint */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyWithFullyQualifiedTypeHint */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyIsCaseInsensitive */',
+ 'ReAdOnLy',
+ ],
+ [
+ '/* testReadonlyConstructorPropertyPromotion */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyConstructorPropertyPromotionWithReference */',
+ 'ReadOnly',
+ ],
+ [
+ '/* testReadonlyPropertyInAnonymousClass */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */',
+ 'readonly',
+ ],
+ [
+ '/* testParseErrorLiveCoding */',
+ 'readonly',
+ ],
+ ];
+
+ }//end dataReadonly()
+
+
+ /**
+ * Test that "readonly" when not used as the keyword is still tokenized as `T_STRING`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotReadonly
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testNotReadonly($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent);
+ $this->assertSame(T_STRING, $tokens[$target]['code']);
+ $this->assertSame('T_STRING', $tokens[$target]['type']);
+
+ }//end testNotReadonly()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotReadonly()
+ *
+ * @return array
+ */
+ public function dataNotReadonly()
+ {
+ return [
+ [
+ '/* testReadonlyUsedAsClassConstantName */',
+ 'READONLY',
+ ],
+ [
+ '/* testReadonlyUsedAsMethodName */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyUsedAsPropertyName */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyPropertyInTernaryOperator */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyUsedAsFunctionName */',
+ 'readonly',
+ ],
+ [
+ '/* testReadonlyUsedAsNamespaceName */',
+ 'Readonly',
+ ],
+ [
+ '/* testReadonlyUsedAsPartOfNamespaceName */',
+ 'Readonly',
+ ],
+ [
+ '/* testReadonlyAsFunctionCall */',
+ 'readonly',
+ ],
+ [
+ '/* testClassConstantFetchWithReadonlyAsConstantName */',
+ 'READONLY',
+ ],
+ ];
+
+ }//end dataNotReadonly()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc
new file mode 100644
index 0000000000..bfdbdc18c1
--- /dev/null
+++ b/tests/Core/Tokenizer/BitwiseOrTest.inc
@@ -0,0 +1,134 @@
+ $param | $int;
+
+/* testTypeUnionArrowReturnType */
+$arrowWithReturnType = fn ($param) : int|null => $param * 10;
+
+/* testBitwiseOrInArrayKey */
+$array = array(
+ A | B => /* testBitwiseOrInArrayValue */ B | C
+);
+
+/* testBitwiseOrInShortArrayKey */
+$array = [
+ A | B => /* testBitwiseOrInShortArrayValue */ B | C
+];
+
+/* testBitwiseOrTryCatch */
+try {
+} catch ( ExceptionA | ExceptionB $e ) {
+}
+
+/* testBitwiseOrNonArrowFnFunctionCall */
+$obj->fn($something | $else);
+
+/* testTypeUnionNonArrowFunctionDeclaration */
+function &fn(int|false $something) {}
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+return function( type|
diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php
new file mode 100644
index 0000000000..d56e7340aa
--- /dev/null
+++ b/tests/Core/Tokenizer/BitwiseOrTest.php
@@ -0,0 +1,138 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class BitwiseOrTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that non-union type bitwise or tokens are still tokenized as bitwise or.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataBitwiseOr
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testBitwiseOr($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]);
+ $this->assertSame(T_BITWISE_OR, $tokens[$opener]['code']);
+ $this->assertSame('T_BITWISE_OR', $tokens[$opener]['type']);
+
+ }//end testBitwiseOr()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testBitwiseOr()
+ *
+ * @return array
+ */
+ public function dataBitwiseOr()
+ {
+ return [
+ ['/* testBitwiseOr1 */'],
+ ['/* testBitwiseOr2 */'],
+ ['/* testBitwiseOrPropertyDefaultValue */'],
+ ['/* testBitwiseOrParamDefaultValue */'],
+ ['/* testBitwiseOr3 */'],
+ ['/* testBitwiseOrClosureParamDefault */'],
+ ['/* testBitwiseOrArrowParamDefault */'],
+ ['/* testBitwiseOrArrowExpression */'],
+ ['/* testBitwiseOrInArrayKey */'],
+ ['/* testBitwiseOrInArrayValue */'],
+ ['/* testBitwiseOrInShortArrayKey */'],
+ ['/* testBitwiseOrInShortArrayValue */'],
+ ['/* testBitwiseOrTryCatch */'],
+ ['/* testBitwiseOrNonArrowFnFunctionCall */'],
+ ['/* testLiveCoding */'],
+ ];
+
+ }//end dataBitwiseOr()
+
+
+ /**
+ * Test that bitwise or tokens when used as part of a union type are tokenized as `T_TYPE_UNION`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataTypeUnion
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testTypeUnion($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]);
+ $this->assertSame(T_TYPE_UNION, $tokens[$opener]['code']);
+ $this->assertSame('T_TYPE_UNION', $tokens[$opener]['type']);
+
+ }//end testTypeUnion()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testTypeUnion()
+ *
+ * @return array
+ */
+ public function dataTypeUnion()
+ {
+ return [
+ ['/* testTypeUnionPropertySimple */'],
+ ['/* testTypeUnionPropertyReverseModifierOrder */'],
+ ['/* testTypeUnionPropertyMulti1 */'],
+ ['/* testTypeUnionPropertyMulti2 */'],
+ ['/* testTypeUnionPropertyMulti3 */'],
+ ['/* testTypeUnionPropertyNamespaceRelative */'],
+ ['/* testTypeUnionPropertyPartiallyQualified */'],
+ ['/* testTypeUnionPropertyFullyQualified */'],
+ ['/* testTypeUnionPropertyWithReadOnlyKeyword */'],
+ ['/* testTypeUnionPropertyWithReadOnlyKeywordFirst */'],
+ ['/* testTypeUnionPropertyWithStaticAndReadOnlyKeywords */'],
+ ['/* testTypeUnionPropertyWithVarAndReadOnlyKeywords */'],
+ ['/* testTypeUnionPropertyWithOnlyReadOnlyKeyword */'],
+ ['/* testTypeUnionParam1 */'],
+ ['/* testTypeUnionParam2 */'],
+ ['/* testTypeUnionParam3 */'],
+ ['/* testTypeUnionParamNamespaceRelative */'],
+ ['/* testTypeUnionParamPartiallyQualified */'],
+ ['/* testTypeUnionParamFullyQualified */'],
+ ['/* testTypeUnionReturnType */'],
+ ['/* testTypeUnionConstructorPropertyPromotion */'],
+ ['/* testTypeUnionAbstractMethodReturnType1 */'],
+ ['/* testTypeUnionAbstractMethodReturnType2 */'],
+ ['/* testTypeUnionReturnTypeNamespaceRelative */'],
+ ['/* testTypeUnionReturnPartiallyQualified */'],
+ ['/* testTypeUnionReturnFullyQualified */'],
+ ['/* testTypeUnionClosureParamIllegalNullable */'],
+ ['/* testTypeUnionWithReference */'],
+ ['/* testTypeUnionWithSpreadOperator */'],
+ ['/* testTypeUnionClosureReturn */'],
+ ['/* testTypeUnionArrowParam */'],
+ ['/* testTypeUnionArrowReturnType */'],
+ ['/* testTypeUnionNonArrowFunctionDeclaration */'],
+ ];
+
+ }//end dataTypeUnion()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc
new file mode 100644
index 0000000000..bc98c49f3d
--- /dev/null
+++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc
@@ -0,0 +1,264 @@
+ 'a',
+ 2 => 'b',
+ /* testMatchDefaultIsKeyword */ default => 'default',
+};
+
+$closure = /* testFnIsKeyword */ fn () => 'string';
+
+function () {
+ /* testYieldIsKeyword */ yield $f;
+ /* testYieldFromIsKeyword */ yield from someFunction();
+};
+
+/* testDeclareIsKeyword */ declare(ticks=1):
+/* testEndDeclareIsKeyword */ enddeclare;
+
+if (true /* testAndIsKeyword */ and false /* testOrIsKeyword */ or null /* testXorIsKeyword */ xor 0) {
+
+}
+
+$anonymousClass = new /* testAnonymousClassIsKeyword */ class {};
+$anonymousClass2 = new class /* testExtendsInAnonymousClassIsKeyword */ extends SomeParent {};
+$anonymousClass3 = new class /* testImplementsInAnonymousClassIsKeyword */ implements SomeInterface {};
+
+$instantiated1 = new /* testClassInstantiationParentIsKeyword */ parent();
+$instantiated2 = new /* testClassInstantiationSelfIsKeyword */ SELF;
+$instantiated3 = new /* testClassInstantiationStaticIsKeyword */ static($param);
+
+class Foo extends /* testNamespaceInNameIsKeyword */ namespace\Exception
+{}
+
+function /* testKeywordAfterFunctionShouldBeString */ eval() {}
+function /* testKeywordAfterFunctionByRefShouldBeString */ &switch() {}
+
+function /* testKeywordSelfAfterFunctionByRefShouldBeString */ &self() {}
+function /* testKeywordStaticAfterFunctionByRefShouldBeString */ &static() {}
+function /* testKeywordParentAfterFunctionByRefShouldBeString */ &parent() {}
+function /* testKeywordFalseAfterFunctionByRefShouldBeString */ &false() {}
+function /* testKeywordTrueAfterFunctionByRefShouldBeString */ & true () {}
+function /* testKeywordNullAfterFunctionByRefShouldBeString */ &NULL() {}
+
+/* testKeywordAsFunctionCallNameShouldBeStringSelf */ self();
+/* testKeywordAsFunctionCallNameShouldBeStringStatic */ static();
+$obj-> /* testKeywordAsMethodCallNameShouldBeStringStatic */ static();
+/* testKeywordAsFunctionCallNameShouldBeStringParent */ parent();
+/* testKeywordAsFunctionCallNameShouldBeStringFalse */ false();
+/* testKeywordAsFunctionCallNameShouldBeStringTrue */ True ();
+/* testKeywordAsFunctionCallNameShouldBeStringNull */ null /*comment*/ ();
+
+$instantiated4 = new /* testClassInstantiationFalseIsString */ False();
+$instantiated5 = new /* testClassInstantiationTrueIsString */ true ();
+$instantiated6 = new /* testClassInstantiationNullIsString */ null();
+
+$function = /* testStaticIsKeywordBeforeClosure */ static function(/* testStaticIsKeywordWhenParamType */ static $param) {};
+$arrow = /* testStaticIsKeywordBeforeArrow */ static fn(): /* testStaticIsKeywordWhenReturnType */ static => 10;
+
+function standAloneFalseTrueNullTypesAndMore(
+ /* testFalseIsKeywordAsParamType */ false $paramA,
+ /* testTrueIsKeywordAsParamType */ true $paramB,
+ /* testNullIsKeywordAsParamType */ null $paramC,
+) /* testFalseIsKeywordAsReturnType */ false | /* testTrueIsKeywordAsReturnType */ true | /* testNullIsKeywordAsReturnType */ null {
+ if ($a === /* testFalseIsKeywordInComparison */ false
+ || $a === /* testTrueIsKeywordInComparison */ true
+ || $a === /* testNullIsKeywordInComparison */ null
+ ) {}
+}
diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php
new file mode 100644
index 0000000000..3f077ca639
--- /dev/null
+++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php
@@ -0,0 +1,587 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class ContextSensitiveKeywordsTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that context sensitive keyword is tokenized as string when it should be string.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataStrings
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testStrings($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING, T_NULL, T_FALSE, T_TRUE, T_PARENT, T_SELF]));
+
+ $this->assertSame(T_STRING, $tokens[$token]['code']);
+ $this->assertSame('T_STRING', $tokens[$token]['type']);
+
+ }//end testStrings()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testStrings()
+ *
+ * @return array
+ */
+ public function dataStrings()
+ {
+ return [
+ ['/* testAbstract */'],
+ ['/* testArray */'],
+ ['/* testAs */'],
+ ['/* testBreak */'],
+ ['/* testCallable */'],
+ ['/* testCase */'],
+ ['/* testCatch */'],
+ ['/* testClass */'],
+ ['/* testClone */'],
+ ['/* testConst */'],
+ ['/* testContinue */'],
+ ['/* testDeclare */'],
+ ['/* testDefault */'],
+ ['/* testDo */'],
+ ['/* testEcho */'],
+ ['/* testElse */'],
+ ['/* testElseIf */'],
+ ['/* testEmpty */'],
+ ['/* testEndDeclare */'],
+ ['/* testEndFor */'],
+ ['/* testEndForeach */'],
+ ['/* testEndIf */'],
+ ['/* testEndSwitch */'],
+ ['/* testEndWhile */'],
+ ['/* testEnum */'],
+ ['/* testEval */'],
+ ['/* testExit */'],
+ ['/* testExtends */'],
+ ['/* testFinal */'],
+ ['/* testFinally */'],
+ ['/* testFn */'],
+ ['/* testFor */'],
+ ['/* testForeach */'],
+ ['/* testFunction */'],
+ ['/* testGlobal */'],
+ ['/* testGoto */'],
+ ['/* testIf */'],
+ ['/* testImplements */'],
+ ['/* testInclude */'],
+ ['/* testIncludeOnce */'],
+ ['/* testInstanceOf */'],
+ ['/* testInsteadOf */'],
+ ['/* testInterface */'],
+ ['/* testIsset */'],
+ ['/* testList */'],
+ ['/* testMatch */'],
+ ['/* testNamespace */'],
+ ['/* testNew */'],
+ ['/* testParent */'],
+ ['/* testPrint */'],
+ ['/* testPrivate */'],
+ ['/* testProtected */'],
+ ['/* testPublic */'],
+ ['/* testReadonly */'],
+ ['/* testRequire */'],
+ ['/* testRequireOnce */'],
+ ['/* testReturn */'],
+ ['/* testSelf */'],
+ ['/* testStatic */'],
+ ['/* testSwitch */'],
+ ['/* testThrows */'],
+ ['/* testTrait */'],
+ ['/* testTry */'],
+ ['/* testUnset */'],
+ ['/* testUse */'],
+ ['/* testVar */'],
+ ['/* testWhile */'],
+ ['/* testYield */'],
+ ['/* testYieldFrom */'],
+ ['/* testAnd */'],
+ ['/* testOr */'],
+ ['/* testXor */'],
+ ['/* testFalse */'],
+ ['/* testTrue */'],
+ ['/* testNull */'],
+
+ ['/* testKeywordAfterNamespaceShouldBeString */'],
+ ['/* testNamespaceNameIsString1 */'],
+ ['/* testNamespaceNameIsString2 */'],
+ ['/* testNamespaceNameIsString3 */'],
+
+ ['/* testKeywordAfterFunctionShouldBeString */'],
+ ['/* testKeywordAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordSelfAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordStaticAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordParentAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordFalseAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordTrueAfterFunctionByRefShouldBeString */'],
+ ['/* testKeywordNullAfterFunctionByRefShouldBeString */'],
+
+ ['/* testKeywordAsFunctionCallNameShouldBeStringSelf */'],
+ ['/* testKeywordAsFunctionCallNameShouldBeStringStatic */'],
+ ['/* testKeywordAsMethodCallNameShouldBeStringStatic */'],
+ ['/* testKeywordAsFunctionCallNameShouldBeStringParent */'],
+ ['/* testKeywordAsFunctionCallNameShouldBeStringFalse */'],
+ ['/* testKeywordAsFunctionCallNameShouldBeStringTrue */'],
+ ['/* testKeywordAsFunctionCallNameShouldBeStringNull */'],
+
+ ['/* testClassInstantiationFalseIsString */'],
+ ['/* testClassInstantiationTrueIsString */'],
+ ['/* testClassInstantiationNullIsString */'],
+ ];
+
+ }//end dataStrings()
+
+
+ /**
+ * Test that context sensitive keyword is tokenized as keyword when it should be keyword.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $expectedTokenType The expected token type.
+ *
+ * @dataProvider dataKeywords
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testKeywords($testMarker, $expectedTokenType)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken(
+ $testMarker,
+ (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING, T_NULL, T_FALSE, T_TRUE])
+ );
+
+ $this->assertSame(constant($expectedTokenType), $tokens[$token]['code']);
+ $this->assertSame($expectedTokenType, $tokens[$token]['type']);
+
+ }//end testKeywords()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testKeywords()
+ *
+ * @return array
+ */
+ public function dataKeywords()
+ {
+ return [
+ [
+ '/* testNamespaceIsKeyword */',
+ 'T_NAMESPACE',
+ ],
+ [
+ '/* testAbstractIsKeyword */',
+ 'T_ABSTRACT',
+ ],
+ [
+ '/* testClassIsKeyword */',
+ 'T_CLASS',
+ ],
+ [
+ '/* testExtendsIsKeyword */',
+ 'T_EXTENDS',
+ ],
+ [
+ '/* testImplementsIsKeyword */',
+ 'T_IMPLEMENTS',
+ ],
+ [
+ '/* testUseIsKeyword */',
+ 'T_USE',
+ ],
+ [
+ '/* testInsteadOfIsKeyword */',
+ 'T_INSTEADOF',
+ ],
+ [
+ '/* testAsIsKeyword */',
+ 'T_AS',
+ ],
+ [
+ '/* testConstIsKeyword */',
+ 'T_CONST',
+ ],
+ [
+ '/* testPrivateIsKeyword */',
+ 'T_PRIVATE',
+ ],
+ [
+ '/* testProtectedIsKeyword */',
+ 'T_PROTECTED',
+ ],
+ [
+ '/* testPublicIsKeyword */',
+ 'T_PUBLIC',
+ ],
+ [
+ '/* testVarIsKeyword */',
+ 'T_VAR',
+ ],
+ [
+ '/* testStaticIsKeyword */',
+ 'T_STATIC',
+ ],
+ [
+ '/* testReadonlyIsKeyword */',
+ 'T_READONLY',
+ ],
+ [
+ '/* testFinalIsKeyword */',
+ 'T_FINAL',
+ ],
+ [
+ '/* testFunctionIsKeyword */',
+ 'T_FUNCTION',
+ ],
+ [
+ '/* testCallableIsKeyword */',
+ 'T_CALLABLE',
+ ],
+ [
+ '/* testSelfIsKeyword */',
+ 'T_SELF',
+ ],
+ [
+ '/* testParentIsKeyword */',
+ 'T_PARENT',
+ ],
+ [
+ '/* testReturnIsKeyword */',
+ 'T_RETURN',
+ ],
+
+ [
+ '/* testInterfaceIsKeyword */',
+ 'T_INTERFACE',
+ ],
+ [
+ '/* testTraitIsKeyword */',
+ 'T_TRAIT',
+ ],
+ [
+ '/* testEnumIsKeyword */',
+ 'T_ENUM',
+ ],
+
+ [
+ '/* testNewIsKeyword */',
+ 'T_NEW',
+ ],
+ [
+ '/* testInstanceOfIsKeyword */',
+ 'T_INSTANCEOF',
+ ],
+ [
+ '/* testCloneIsKeyword */',
+ 'T_CLONE',
+ ],
+
+ [
+ '/* testIfIsKeyword */',
+ 'T_IF',
+ ],
+ [
+ '/* testEmptyIsKeyword */',
+ 'T_EMPTY',
+ ],
+ [
+ '/* testElseIfIsKeyword */',
+ 'T_ELSEIF',
+ ],
+ [
+ '/* testElseIsKeyword */',
+ 'T_ELSE',
+ ],
+ [
+ '/* testEndIfIsKeyword */',
+ 'T_ENDIF',
+ ],
+
+ [
+ '/* testForIsKeyword */',
+ 'T_FOR',
+ ],
+ [
+ '/* testEndForIsKeyword */',
+ 'T_ENDFOR',
+ ],
+
+ [
+ '/* testForeachIsKeyword */',
+ 'T_FOREACH',
+ ],
+ [
+ '/* testEndForeachIsKeyword */',
+ 'T_ENDFOREACH',
+ ],
+
+ [
+ '/* testSwitchIsKeyword */',
+ 'T_SWITCH',
+ ],
+ [
+ '/* testCaseIsKeyword */',
+ 'T_CASE',
+ ],
+ [
+ '/* testDefaultIsKeyword */',
+ 'T_DEFAULT',
+ ],
+ [
+ '/* testEndSwitchIsKeyword */',
+ 'T_ENDSWITCH',
+ ],
+ [
+ '/* testBreakIsKeyword */',
+ 'T_BREAK',
+ ],
+ [
+ '/* testContinueIsKeyword */',
+ 'T_CONTINUE',
+ ],
+
+ [
+ '/* testDoIsKeyword */',
+ 'T_DO',
+ ],
+ [
+ '/* testWhileIsKeyword */',
+ 'T_WHILE',
+ ],
+ [
+ '/* testEndWhileIsKeyword */',
+ 'T_ENDWHILE',
+ ],
+
+ [
+ '/* testTryIsKeyword */',
+ 'T_TRY',
+ ],
+ [
+ '/* testThrowIsKeyword */',
+ 'T_THROW',
+ ],
+ [
+ '/* testCatchIsKeyword */',
+ 'T_CATCH',
+ ],
+ [
+ '/* testFinallyIsKeyword */',
+ 'T_FINALLY',
+ ],
+
+ [
+ '/* testGlobalIsKeyword */',
+ 'T_GLOBAL',
+ ],
+ [
+ '/* testEchoIsKeyword */',
+ 'T_ECHO',
+ ],
+ [
+ '/* testPrintIsKeyword */',
+ 'T_PRINT',
+ ],
+ [
+ '/* testDieIsKeyword */',
+ 'T_EXIT',
+ ],
+ [
+ '/* testEvalIsKeyword */',
+ 'T_EVAL',
+ ],
+ [
+ '/* testExitIsKeyword */',
+ 'T_EXIT',
+ ],
+ [
+ '/* testIssetIsKeyword */',
+ 'T_ISSET',
+ ],
+ [
+ '/* testUnsetIsKeyword */',
+ 'T_UNSET',
+ ],
+
+ [
+ '/* testIncludeIsKeyword */',
+ 'T_INCLUDE',
+ ],
+ [
+ '/* testIncludeOnceIsKeyword */',
+ 'T_INCLUDE_ONCE',
+ ],
+ [
+ '/* testRequireIsKeyword */',
+ 'T_REQUIRE',
+ ],
+ [
+ '/* testRequireOnceIsKeyword */',
+ 'T_REQUIRE_ONCE',
+ ],
+
+ [
+ '/* testListIsKeyword */',
+ 'T_LIST',
+ ],
+ [
+ '/* testGotoIsKeyword */',
+ 'T_GOTO',
+ ],
+ [
+ '/* testMatchIsKeyword */',
+ 'T_MATCH',
+ ],
+ [
+ '/* testMatchDefaultIsKeyword */',
+ 'T_MATCH_DEFAULT',
+ ],
+ [
+ '/* testFnIsKeyword */',
+ 'T_FN',
+ ],
+
+ [
+ '/* testYieldIsKeyword */',
+ 'T_YIELD',
+ ],
+ [
+ '/* testYieldFromIsKeyword */',
+ 'T_YIELD_FROM',
+ ],
+
+ [
+ '/* testDeclareIsKeyword */',
+ 'T_DECLARE',
+ ],
+ [
+ '/* testEndDeclareIsKeyword */',
+ 'T_ENDDECLARE',
+ ],
+
+ [
+ '/* testAndIsKeyword */',
+ 'T_LOGICAL_AND',
+ ],
+ [
+ '/* testOrIsKeyword */',
+ 'T_LOGICAL_OR',
+ ],
+ [
+ '/* testXorIsKeyword */',
+ 'T_LOGICAL_XOR',
+ ],
+
+ [
+ '/* testAnonymousClassIsKeyword */',
+ 'T_ANON_CLASS',
+ ],
+ [
+ '/* testExtendsInAnonymousClassIsKeyword */',
+ 'T_EXTENDS',
+ ],
+ [
+ '/* testImplementsInAnonymousClassIsKeyword */',
+ 'T_IMPLEMENTS',
+ ],
+ [
+ '/* testClassInstantiationParentIsKeyword */',
+ 'T_PARENT',
+ ],
+ [
+ '/* testClassInstantiationSelfIsKeyword */',
+ 'T_SELF',
+ ],
+ [
+ '/* testClassInstantiationStaticIsKeyword */',
+ 'T_STATIC',
+ ],
+ [
+ '/* testNamespaceInNameIsKeyword */',
+ 'T_NAMESPACE',
+ ],
+
+ [
+ '/* testStaticIsKeywordBeforeClosure */',
+ 'T_STATIC',
+ ],
+ [
+ '/* testStaticIsKeywordWhenParamType */',
+ 'T_STATIC',
+ ],
+ [
+ '/* testStaticIsKeywordBeforeArrow */',
+ 'T_STATIC',
+ ],
+ [
+ '/* testStaticIsKeywordWhenReturnType */',
+ 'T_STATIC',
+ ],
+
+ [
+ '/* testFalseIsKeywordAsParamType */',
+ 'T_FALSE',
+ ],
+ [
+ '/* testTrueIsKeywordAsParamType */',
+ 'T_TRUE',
+ ],
+ [
+ '/* testNullIsKeywordAsParamType */',
+ 'T_NULL',
+ ],
+ [
+ '/* testFalseIsKeywordAsReturnType */',
+ 'T_FALSE',
+ ],
+ [
+ '/* testTrueIsKeywordAsReturnType */',
+ 'T_TRUE',
+ ],
+ [
+ '/* testNullIsKeywordAsReturnType */',
+ 'T_NULL',
+ ],
+ [
+ '/* testFalseIsKeywordInComparison */',
+ 'T_FALSE',
+ ],
+ [
+ '/* testTrueIsKeywordInComparison */',
+ 'T_TRUE',
+ ],
+ [
+ '/* testNullIsKeywordInComparison */',
+ 'T_NULL',
+ ],
+ ];
+
+ }//end dataKeywords()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/DefaultKeywordTest.inc b/tests/Core/Tokenizer/DefaultKeywordTest.inc
new file mode 100644
index 0000000000..648149d2ff
--- /dev/null
+++ b/tests/Core/Tokenizer/DefaultKeywordTest.inc
@@ -0,0 +1,203 @@
+ 1,
+ 2 => 2,
+ /* testSimpleMatchDefault */
+ default => 'default',
+ };
+}
+
+function switchWithDefault($i) {
+ switch ($i) {
+ case 1:
+ return 1;
+ case 2:
+ return 2;
+ /* testSimpleSwitchDefault */
+ default:
+ return 'default';
+ }
+}
+
+function switchWithDefaultAndCurlies($i) {
+ switch ($i) {
+ case 1:
+ return 1;
+ case 2:
+ return 2;
+ /* testSimpleSwitchDefaultWithCurlies */
+ default: {
+ return 'default';
+ }
+ }
+}
+
+function matchWithDefaultInSwitch() {
+ switch ($something) {
+ case 'foo':
+ $var = [1, 2, 3];
+ $var = match ($i) {
+ 1 => 1,
+ /* testMatchDefaultNestedInSwitchCase1 */
+ default => 'default',
+ };
+ continue;
+
+ case 'bar' :
+ $i = callMe($a, $b);
+ return match ($i) {
+ 1 => 1,
+ /* testMatchDefaultNestedInSwitchCase2 */
+ default => 'default',
+ };
+
+ /* testSwitchDefault */
+ default;
+ echo 'something', match ($i) {
+ 1, => 1,
+ /* testMatchDefaultNestedInSwitchDefault */
+ default, => 'default',
+ };
+ break;
+ }
+}
+
+function switchWithDefaultInMatch() {
+ $x = match ($y) {
+ 5, 8 => function($z) {
+ switch($z) {
+ case 'a';
+ $var = [1, 2, 3];
+ return 'a';
+ /* testSwitchDefaultNestedInMatchCase */
+ default:
+ $var = [1, 2, 3];
+ return 'default1';
+ }
+ },
+ /* testMatchDefault */
+ default => function($z) {
+ switch($z) {
+ case 'a':
+ $i = callMe($a, $b);
+ return 'b';
+ /* testSwitchDefaultNestedInMatchDefault */
+ default:
+ $i = callMe($a, $b);
+ return 'default2';
+ }
+ }
+ };
+}
+
+function shortArrayWithConstantKey() {
+ $arr = [
+ /* testClassConstantAsShortArrayKey */
+ SomeClass::DEFAULT => 1,
+ /* testClassPropertyAsShortArrayKey */
+ SomeClass->DEFAULT => 1,
+ /* testNamespacedConstantAsShortArrayKey */
+ // Intentional parse error PHP < 8.0. Reserved keyword used as namespaced constant.
+ SomeNamespace\DEFAULT => 1,
+ /* testFQNGlobalConstantAsShortArrayKey */
+ // Intentional parse error in PHP < 8.0. Reserved keyword used as global constant.
+ \DEFAULT => 1,
+ ];
+}
+
+function longArrayWithConstantKey() {
+ $arr = array(
+ /* testClassConstantAsLongArrayKey */
+ SomeClass::DEFAULT => 1,
+ );
+}
+
+function yieldWithConstantKey() {
+ /* testClassConstantAsYieldKey */
+ yield SomeClass::DEFAULT => 1;
+}
+
+function longArrayWithConstantKeyNestedInMatch() {
+ return match($x) {
+ /* testMatchDefaultWithNestedLongArrayWithClassConstantKey */
+ DEFAULT => array(
+ /* testClassConstantAsLongArrayKeyNestedInMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchDefaultWithNestedLongArrayWithClassConstantKeyLevelDown */
+ DEFAULT => array(
+ /* testClassConstantAsLongArrayKeyNestedInMatchLevelDown */
+ SomeClass::DEFAULT => 1,
+ ),
+ },
+ ),
+ };
+}
+
+function shortArrayWithConstantKeyNestedInMatch() {
+ return match($x) {
+ /* testMatchDefaultWithNestedShortArrayWithClassConstantKey */
+ DEFAULT => [
+ /* testClassConstantAsShortArrayKeyNestedInMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchDefaultWithNestedShortArrayWithClassConstantKeyLevelDown */
+ DEFAULT => [
+ /* testClassConstantAsShortArrayKeyNestedInMatchLevelDown */
+ SomeClass::DEFAULT => 1,
+ ],
+ },
+ ],
+ };
+}
+
+
+function longArrayWithConstantKeyWithNestedMatch() {
+ return array(
+ /* testClassConstantAsLongArrayKeyWithNestedMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchDefaultNestedInLongArray */
+ DEFAULT => 'foo'
+ },
+ );
+}
+
+function shortArrayWithConstantKeyWithNestedMatch() {
+ return [
+ /* testClassConstantAsShortArrayKeyWithNestedMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchDefaultNestedInShortArray */
+ DEFAULT => 'foo'
+ },
+ ];
+}
+
+function switchWithConstantNonDefault($i) {
+ switch ($i) {
+ /* testClassConstantInSwitchCase */
+ case SomeClass::DEFAULT:
+ return 1;
+
+ /* testClassPropertyInSwitchCase */
+ case SomeClass->DEFAULT:
+ return 2;
+
+ /* testNamespacedConstantInSwitchCase */
+ // Intentional parse error PHP < 8.0. Reserved keyword used as constant.
+ case SomeNamespace\DEFAULT:
+ return 2;
+
+ /* testNamespaceRelativeConstantInSwitchCase */
+ // Intentional parse error PHP < 8.0. Reserved keyword used as global constant.
+ case namespace\DEFAULT:
+ return 2;
+ }
+}
+
+class Foo {
+ /* testClassConstant */
+ const DEFAULT = 'foo';
+
+ /* testMethodDeclaration */
+ public function default() {}
+}
diff --git a/tests/Core/Tokenizer/DefaultKeywordTest.php b/tests/Core/Tokenizer/DefaultKeywordTest.php
new file mode 100644
index 0000000000..9a5b061a05
--- /dev/null
+++ b/tests/Core/Tokenizer/DefaultKeywordTest.php
@@ -0,0 +1,302 @@
+
+ * @copyright 2020-2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class DefaultKeywordTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test the retokenization of the `default` keyword for match structure to `T_MATCH_DEFAULT`.
+ *
+ * Note: Cases and default structures within a match structure do *NOT* get case/default scope
+ * conditions, in contrast to case and default structures in switch control structures.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataMatchDefault
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
+ *
+ * @return void
+ */
+ public function testMatchDefault($testMarker, $testContent='default')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_MATCH_DEFAULT, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_DEFAULT (code)');
+ $this->assertSame('T_MATCH_DEFAULT', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_DEFAULT (type)');
+
+ $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set');
+ $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set');
+ $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set');
+
+ }//end testMatchDefault()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testMatchDefault()
+ *
+ * @return array
+ */
+ public function dataMatchDefault()
+ {
+ return [
+ 'simple_match_default' => ['/* testSimpleMatchDefault */'],
+ 'match_default_in_switch_case_1' => ['/* testMatchDefaultNestedInSwitchCase1 */'],
+ 'match_default_in_switch_case_2' => ['/* testMatchDefaultNestedInSwitchCase2 */'],
+ 'match_default_in_switch_default' => ['/* testMatchDefaultNestedInSwitchDefault */'],
+ 'match_default_containing_switch' => ['/* testMatchDefault */'],
+
+ 'match_default_with_nested_long_array_and_default_key' => [
+ '/* testMatchDefaultWithNestedLongArrayWithClassConstantKey */',
+ 'DEFAULT',
+ ],
+ 'match_default_with_nested_long_array_and_default_key_2' => [
+ '/* testMatchDefaultWithNestedLongArrayWithClassConstantKeyLevelDown */',
+ 'DEFAULT',
+ ],
+ 'match_default_with_nested_short_array_and_default_key' => [
+ '/* testMatchDefaultWithNestedShortArrayWithClassConstantKey */',
+ 'DEFAULT',
+ ],
+ 'match_default_with_nested_short_array_and_default_key_2' => [
+ '/* testMatchDefaultWithNestedShortArrayWithClassConstantKeyLevelDown */',
+ 'DEFAULT',
+ ],
+ 'match_default_in_long_array' => [
+ '/* testMatchDefaultNestedInLongArray */',
+ 'DEFAULT',
+ ],
+ 'match_default_in_short_array' => [
+ '/* testMatchDefaultNestedInShortArray */',
+ 'DEFAULT',
+ ],
+ ];
+
+ }//end dataMatchDefault()
+
+
+ /**
+ * Verify that the retokenization of `T_DEFAULT` tokens in match constructs, doesn't negatively
+ * impact the tokenization of `T_DEFAULT` tokens in switch control structures.
+ *
+ * Note: Cases and default structures within a switch control structure *do* get case/default scope
+ * conditions.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker.
+ * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker.
+ * @param int|null $conditionStop The expected offset at which tokens stop having T_DEFAULT as a scope condition.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataSwitchDefault
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
+ *
+ * @return void
+ */
+ public function testSwitchDefault($testMarker, $openerOffset, $closerOffset, $conditionStop=null, $testContent='default')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+ $expectedScopeOpener = ($token + $openerOffset);
+ $expectedScopeCloser = ($token + $closerOffset);
+
+ $this->assertSame(T_DEFAULT, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_DEFAULT (code)');
+ $this->assertSame('T_DEFAULT', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_DEFAULT (type)');
+
+ $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set');
+ $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the T_DEFAULT token');
+ $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the T_DEFAULT token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the T_DEFAULT token incorrect');
+
+ $opener = $tokenArray['scope_opener'];
+ $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set');
+ $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the T_DEFAULT token');
+ $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'T_DEFAULT opener scope opener token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'T_DEFAULT opener scope closer token incorrect');
+
+ $closer = $tokenArray['scope_closer'];
+ $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set');
+ $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the T_DEFAULT token');
+ $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'T_DEFAULT closer scope opener token incorrect');
+ $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'T_DEFAULT closer scope closer token incorrect');
+
+ if (($opener + 1) !== $closer) {
+ $end = $closer;
+ if (isset($conditionStop) === true) {
+ $end = $conditionStop;
+ }
+
+ for ($i = ($opener + 1); $i < $end; $i++) {
+ $this->assertArrayHasKey(
+ $token,
+ $tokens[$i]['conditions'],
+ 'T_DEFAULT condition not added for token belonging to the T_DEFAULT structure'
+ );
+ }
+ }
+
+ }//end testSwitchDefault()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testSwitchDefault()
+ *
+ * @return array
+ */
+ public function dataSwitchDefault()
+ {
+ return [
+ 'simple_switch_default' => [
+ '/* testSimpleSwitchDefault */',
+ 1,
+ 4,
+ ],
+ 'simple_switch_default_with_curlies' => [
+ // For a default structure with curly braces, the scope opener
+ // will be the open curly and the closer the close curly.
+ // However, scope conditions will not be set for open to close,
+ // but only for the open token up to the "break/return/continue" etc.
+ '/* testSimpleSwitchDefaultWithCurlies */',
+ 3,
+ 12,
+ 6,
+ ],
+ 'switch_default_toplevel' => [
+ '/* testSwitchDefault */',
+ 1,
+ 43,
+ ],
+ 'switch_default_nested_in_match_case' => [
+ '/* testSwitchDefaultNestedInMatchCase */',
+ 1,
+ 20,
+ ],
+ 'switch_default_nested_in_match_default' => [
+ '/* testSwitchDefaultNestedInMatchDefault */',
+ 1,
+ 18,
+ ],
+ ];
+
+ }//end dataSwitchDefault()
+
+
+ /**
+ * Verify that the retokenization of `T_DEFAULT` tokens in match constructs, doesn't negatively
+ * impact the tokenization of `T_STRING` tokens with the contents 'default' which aren't in
+ * actual fact the default keyword.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotDefaultKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testNotDefaultKeyword($testMarker, $testContent='DEFAULT')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set');
+ $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set');
+ $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set');
+
+ }//end testNotDefaultKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotDefaultKeyword()
+ *
+ * @return array
+ */
+ public function dataNotDefaultKeyword()
+ {
+ return [
+ 'class-constant-as-short-array-key' => ['/* testClassConstantAsShortArrayKey */'],
+ 'class-property-as-short-array-key' => ['/* testClassPropertyAsShortArrayKey */'],
+ 'namespaced-constant-as-short-array-key' => ['/* testNamespacedConstantAsShortArrayKey */'],
+ 'fqn-global-constant-as-short-array-key' => ['/* testFQNGlobalConstantAsShortArrayKey */'],
+ 'class-constant-as-long-array-key' => ['/* testClassConstantAsLongArrayKey */'],
+ 'class-constant-as-yield-key' => ['/* testClassConstantAsYieldKey */'],
+
+ 'class-constant-as-long-array-key-nested-in-match' => ['/* testClassConstantAsLongArrayKeyNestedInMatch */'],
+ 'class-constant-as-long-array-key-nested-in-match-2' => ['/* testClassConstantAsLongArrayKeyNestedInMatchLevelDown */'],
+ 'class-constant-as-short-array-key-nested-in-match' => ['/* testClassConstantAsShortArrayKeyNestedInMatch */'],
+ 'class-constant-as-short-array-key-nested-in-match-2' => ['/* testClassConstantAsShortArrayKeyNestedInMatchLevelDown */'],
+ 'class-constant-as-long-array-key-with-nested-match' => ['/* testClassConstantAsLongArrayKeyWithNestedMatch */'],
+ 'class-constant-as-short-array-key-with-nested-match' => ['/* testClassConstantAsShortArrayKeyWithNestedMatch */'],
+
+ 'class-constant-in-switch-case' => ['/* testClassConstantInSwitchCase */'],
+ 'class-property-in-switch-case' => ['/* testClassPropertyInSwitchCase */'],
+ 'namespaced-constant-in-switch-case' => ['/* testNamespacedConstantInSwitchCase */'],
+ 'namespace-relative-constant-in-switch-case' => ['/* testNamespaceRelativeConstantInSwitchCase */'],
+
+ 'class-constant-declaration' => ['/* testClassConstant */'],
+ 'class-method-declaration' => [
+ '/* testMethodDeclaration */',
+ 'default',
+ ],
+ ];
+
+ }//end dataNotDefaultKeyword()
+
+
+ /**
+ * Test a specific edge case where a scope opener would be incorrectly set.
+ *
+ * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3326
+ *
+ * @return void
+ */
+ public function testIssue3326()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken('/* testClassConstant */', [T_SEMICOLON]);
+ $tokenArray = $tokens[$token];
+
+ $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set');
+ $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set');
+ $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set');
+
+ }//end testIssue3326()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/DoubleArrowTest.inc b/tests/Core/Tokenizer/DoubleArrowTest.inc
new file mode 100644
index 0000000000..b67b066015
--- /dev/null
+++ b/tests/Core/Tokenizer/DoubleArrowTest.inc
@@ -0,0 +1,281 @@
+ 'Zero',
+ );
+}
+
+function simpleShortArray($x) {
+ return [
+ /* testShortArrayArrowSimple */
+ 0 => 'Zero',
+ ];
+}
+
+function simpleLongList($x) {
+ list(
+ /* testLongListArrowSimple */
+ 0 => $a,
+ ) = $x;
+}
+
+function simpleShortList($x) {
+ [
+ /* testShortListArrowSimple */
+ 0 => $a,
+ ] = $x;
+}
+
+function simpleYield($x) {
+ $i = 0;
+ foreach (explode("\n", $x) as $line) {
+ /* testYieldArrowSimple */
+ yield ++$i => $line;
+ }
+}
+
+function simpleForeach($x) {
+ /* testForeachArrowSimple */
+ foreach ($x as $k => $value) {}
+}
+
+function simpleMatch($x) {
+ return match ($x) {
+ /* testMatchArrowSimpleSingleCase */
+ 0 => 'Zero',
+ /* testMatchArrowSimpleMultiCase */
+ 2, 4, 6 => 'Zero',
+ /* testMatchArrowSimpleSingleCaseWithTrailingComma */
+ 1, => 'Zero',
+ /* testMatchArrowSimpleMultiCaseWithTrailingComma */
+ 3, 5, => 'Zero',
+ };
+}
+
+function simpleArrowFunction($y) {
+ /* testFnArrowSimple */
+ return fn ($y) => callMe($y);
+}
+
+function matchNestedInMatch() {
+ $x = match ($y) {
+ /* testMatchArrowNestedMatchOuter */
+ default, => match ($z) {
+ /* testMatchArrowNestedMatchInner */
+ 1 => 1
+ },
+ };
+}
+
+function matchNestedInLongArrayValue() {
+ $array = array(
+ /* testLongArrayArrowWithNestedMatchValue1 */
+ 'a' => match ($test) {
+ /* testMatchArrowInLongArrayValue1 */
+ 1 => 'a',
+ /* testMatchArrowInLongArrayValue2 */
+ 2 => 'b'
+ },
+ /* testLongArrayArrowWithNestedMatchValue2 */
+ $i => match ($test) {
+ /* testMatchArrowInLongArrayValue3 */
+ 1 => 'a',
+ },
+ );
+}
+
+function matchNestedInShortArrayValue() {
+ $array = [
+ /* testShortArrayArrowWithNestedMatchValue1 */
+ 'a' => match ($test) {
+ /* testMatchArrowInShortArrayValue1 */
+ 1 => 'a',
+ /* testMatchArrowInShortArrayValue2 */
+ 2 => 'b'
+ },
+ /* testShortArrayArrowWithNestedMatchValue2 */
+ $i => match ($test) {
+ /* testMatchArrowInShortArrayValue3 */
+ 1 => 'a',
+ },
+ ];
+}
+
+function matchNestedInLongArrayKey() {
+ $array = array(
+ match ($test) { /* testMatchArrowInLongArrayKey1 */ 1 => 'a', /* testMatchArrowInLongArrayKey2 */ 2 => 'b' }
+ /* testLongArrayArrowWithMatchKey */
+ => 'dynamic keys, woho!',
+ );
+}
+
+function matchNestedInShortArrayKey() {
+ $array = [
+ match ($test) { /* testMatchArrowInShortArrayKey1 */ 1 => 'a', /* testMatchArrowInShortArrayKey2 */ 2 => 'b' }
+ /* testShortArrayArrowWithMatchKey */
+ => 'dynamic keys, woho!',
+ ];
+}
+
+function arraysNestedInMatch() {
+ $matcher = match ($x) {
+ /* testMatchArrowWithLongArrayBodyWithKeys */
+ 0 => array(
+ /* testLongArrayArrowInMatchBody1 */
+ 0 => 1,
+ /* testLongArrayArrowInMatchBody2 */
+ 'a' => 2,
+ /* testLongArrayArrowInMatchBody3 */
+ 'b' => 3
+ ),
+ /* testMatchArrowWithShortArrayBodyWithoutKeys */
+ 1 => [1, 2, 3],
+ /* testMatchArrowWithLongArrayBodyWithoutKeys */
+ 2 => array( 1, [1, 2, 3], 2, 3),
+ /* testMatchArrowWithShortArrayBodyWithKeys */
+ 3 => [
+ /* testShortArrayArrowInMatchBody1 */
+ 0 => 1,
+ /* testShortArrayArrowInMatchBody2 */
+ 'a' => array(1, 2, 3),
+ /* testShortArrayArrowInMatchBody3 */
+ 'b' => 2,
+ 3
+ ],
+ /* testShortArrayArrowinMatchCase1 */
+ [4 => 'a', /* testShortArrayArrowinMatchCase2 */ 5 => 6]
+ /* testMatchArrowWithShortArrayWithKeysAsCase */
+ => 'match with array as case value',
+ /* testShortArrayArrowinMatchCase3 */
+ [4 => 'a'], /* testLongArrayArrowinMatchCase4 */ array(5 => 6),
+ /* testMatchArrowWithMultipleArraysWithKeysAsCase */
+ => 'match with multiple arrays as case value',
+ };
+}
+
+function matchNestedInArrowFunction($x) {
+ /* testFnArrowWithMatchInValue */
+ $fn = fn($x) => match(true) {
+ /* testMatchArrowInFnBody1 */
+ 1, 2, 3, 4, 5 => 'foo',
+ /* testMatchArrowInFnBody2 */
+ default => 'bar',
+ };
+}
+
+function arrowFunctionsNestedInMatch($x) {
+ return match ($x) {
+ /* testMatchArrowWithFnBody1 */
+ 1 => /* testFnArrowInMatchBody1 */ fn($y) => callMe($y),
+ /* testMatchArrowWithFnBody2 */
+ default => /* testFnArrowInMatchBody2 */ fn($y) => callThem($y)
+ };
+}
+
+function matchShortArrayMismash() {
+ $array = [
+ match ($test) {
+ /* testMatchArrowInComplexShortArrayKey1 */
+ 1 => [ /* testShortArrayArrowInComplexMatchValueinShortArrayKey */ 1 => 'a'],
+ /* testMatchArrowInComplexShortArrayKey2 */
+ 2 => 'b'
+ /* testShortArrayArrowInComplexMatchArrayMismash */
+ } => match ($test) {
+ /* testMatchArrowInComplexShortArrayValue1 */
+ 1 => [ /* testShortArrayArrowInComplexMatchValueinShortArrayValue */ 1 => 'a'],
+ /* testMatchArrowInComplexShortArrayValue2 */
+ 2 => /* testFnArrowInComplexMatchValueInShortArrayValue */ fn($y) => callMe($y)
+ },
+ ];
+}
+
+
+function longListInMatch($x, $y) {
+ return match($x) {
+ /* testMatchArrowWithLongListBody */
+ 1 => list('a' => $a, /* testLongListArrowInMatchBody */ 'b' => $b, 'c' => list('d' => $c)) = $y,
+ /* testLongListArrowInMatchCase */
+ list('a' => $a, 'b' => $b) = $y /* testMatchArrowWithLongListInCase */ => 'something'
+ };
+}
+
+function shortListInMatch($x, $y) {
+ return match($x) {
+ /* testMatchArrowWithShortListBody */
+ 1 => ['a' => $a, 'b' => $b, 'c' => /* testShortListArrowInMatchBody */ ['d' => $c]] = $y,
+ /* testShortListArrowInMatchCase */
+ ['a' => $a, 'b' => $b] = $y /* testMatchArrowWithShortListInCase */ => 'something'
+ };
+}
+
+function matchInLongList() {
+ /* testMatchArrowInLongListKey */
+ list(match($x) {1 => 1, 2 => 2} /* testLongListArrowWithMatchInKey */ => $a) = $array;
+}
+
+function matchInShortList() {
+ /* testMatchArrowInShortListKey */
+ [match($x) {1 => 1, 2 => 2} /* testShortListArrowWithMatchInKey */ => $a] = $array;
+}
+
+function longArrayWithConstantKey() {
+ $arr = array(
+ /* testLongArrayArrowWithClassConstantKey */
+ SomeClass::DEFAULT => 1,
+ );
+}
+
+function shortArrayWithConstantKey() {
+ $arr = [
+ /* testShortArrayArrowWithClassConstantKey */
+ SomeClass::DEFAULT => 1,
+ ];
+}
+
+function yieldWithConstantKey() {
+ /* testYieldArrowWithClassConstantKey */
+ yield SomeClass::DEFAULT => 1;
+}
+
+function longArrayWithConstantKeyNestedInMatch() {
+ return match($x) {
+ /* testMatchArrowWithNestedLongArrayWithClassConstantKey */
+ default => array(
+ /* testLongArrayArrowWithClassConstantKeyNestedInMatch */
+ SomeClass::DEFAULT => 1,
+ ),
+ };
+}
+
+function shortArrayWithConstantKeyNestedInMatch() {
+ return match($x) {
+ /* testMatchArrowWithNestedShortArrayWithClassConstantKey */
+ default => [
+ /* testShortArrayArrowWithClassConstantKeyNestedInMatch */
+ SomeClass::DEFAULT => 1,
+ ],
+ };
+}
+
+
+function longArrayWithConstantKeyWithNestedMatch() {
+ return array(
+ /* testLongArrayArrowWithClassConstantKeyWithNestedMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchArrowNestedInLongArrayWithClassConstantKey */
+ default => 'foo'
+ },
+ );
+}
+
+function shortArrayWithConstantKeyWithNestedMatch() {
+ return [
+ /* testShortArrayArrowWithClassConstantKeyWithNestedMatch */
+ SomeClass::DEFAULT => match($x) {
+ /* testMatchArrowNestedInShortArrayWithClassConstantKey */
+ default => 'foo'
+ },
+ ];
+}
diff --git a/tests/Core/Tokenizer/DoubleArrowTest.php b/tests/Core/Tokenizer/DoubleArrowTest.php
new file mode 100644
index 0000000000..ad1a411ff3
--- /dev/null
+++ b/tests/Core/Tokenizer/DoubleArrowTest.php
@@ -0,0 +1,237 @@
+
+ * @copyright 2020-2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class DoubleArrowTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that "normal" double arrows are correctly tokenized as `T_DOUBLE_ARROW`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ *
+ * @dataProvider dataDoubleArrow
+ * @coversNothing
+ *
+ * @return void
+ */
+ public function testDoubleArrow($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_DOUBLE_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_DOUBLE_ARROW (code)');
+ $this->assertSame('T_DOUBLE_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_DOUBLE_ARROW (type)');
+
+ }//end testDoubleArrow()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testDoubleArrow()
+ *
+ * @return array
+ */
+ public function dataDoubleArrow()
+ {
+ return [
+ 'simple_long_array' => ['/* testLongArrayArrowSimple */'],
+ 'simple_short_array' => ['/* testShortArrayArrowSimple */'],
+ 'simple_long_list' => ['/* testLongListArrowSimple */'],
+ 'simple_short_list' => ['/* testShortListArrowSimple */'],
+ 'simple_yield' => ['/* testYieldArrowSimple */'],
+ 'simple_foreach' => ['/* testForeachArrowSimple */'],
+
+ 'long_array_with_match_value_1' => ['/* testLongArrayArrowWithNestedMatchValue1 */'],
+ 'long_array_with_match_value_2' => ['/* testLongArrayArrowWithNestedMatchValue2 */'],
+ 'short_array_with_match_value_1' => ['/* testShortArrayArrowWithNestedMatchValue1 */'],
+ 'short_array_with_match_value_2' => ['/* testShortArrayArrowWithNestedMatchValue2 */'],
+
+ 'long_array_with_match_key' => ['/* testLongArrayArrowWithMatchKey */'],
+ 'short_array_with_match_key' => ['/* testShortArrayArrowWithMatchKey */'],
+
+ 'long_array_in_match_body_1' => ['/* testLongArrayArrowInMatchBody1 */'],
+ 'long_array_in_match_body_2' => ['/* testLongArrayArrowInMatchBody2 */'],
+ 'long_array_in_match_body_3' => ['/* testLongArrayArrowInMatchBody3 */'],
+ 'short_array_in_match_body_1' => ['/* testShortArrayArrowInMatchBody1 */'],
+ 'short_array_in_match_body_2' => ['/* testShortArrayArrowInMatchBody2 */'],
+ 'short_array_in_match_body_3' => ['/* testShortArrayArrowInMatchBody3 */'],
+
+ 'short_array_in_match_case_1' => ['/* testShortArrayArrowinMatchCase1 */'],
+ 'short_array_in_match_case_2' => ['/* testShortArrayArrowinMatchCase2 */'],
+ 'short_array_in_match_case_3' => ['/* testShortArrayArrowinMatchCase3 */'],
+ 'long_array_in_match_case_4' => ['/* testLongArrayArrowinMatchCase4 */'],
+
+ 'in_complex_short_array_key_match_value' => ['/* testShortArrayArrowInComplexMatchValueinShortArrayKey */'],
+ 'in_complex_short_array_toplevel' => ['/* testShortArrayArrowInComplexMatchArrayMismash */'],
+ 'in_complex_short_array_value_match_value' => ['/* testShortArrayArrowInComplexMatchValueinShortArrayValue */'],
+
+ 'long_list_in_match_body' => ['/* testLongListArrowInMatchBody */'],
+ 'long_list_in_match_case' => ['/* testLongListArrowInMatchCase */'],
+ 'short_list_in_match_body' => ['/* testShortListArrowInMatchBody */'],
+ 'short_list_in_match_case' => ['/* testShortListArrowInMatchCase */'],
+ 'long_list_with_match_in_key' => ['/* testLongListArrowWithMatchInKey */'],
+ 'short_list_with_match_in_key' => ['/* testShortListArrowWithMatchInKey */'],
+
+ 'long_array_with_constant_default_in_key' => ['/* testLongArrayArrowWithClassConstantKey */'],
+ 'short_array_with_constant_default_in_key' => ['/* testShortArrayArrowWithClassConstantKey */'],
+ 'yield_with_constant_default_in_key' => ['/* testYieldArrowWithClassConstantKey */'],
+
+ 'long_array_with_default_in_key_in_match' => ['/* testLongArrayArrowWithClassConstantKeyNestedInMatch */'],
+ 'short_array_with_default_in_key_in_match' => ['/* testShortArrayArrowWithClassConstantKeyNestedInMatch */'],
+ 'long_array_with_default_in_key_with_match' => ['/* testLongArrayArrowWithClassConstantKeyWithNestedMatch */'],
+ 'short_array_with_default_in_key_with_match' => ['/* testShortArrayArrowWithClassConstantKeyWithNestedMatch */'],
+ ];
+
+ }//end dataDoubleArrow()
+
+
+ /**
+ * Test that double arrows in match expressions which are the demarkation between a case and the return value
+ * are correctly tokenized as `T_MATCH_ARROW`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ *
+ * @dataProvider dataMatchArrow
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testMatchArrow($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_MATCH_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_ARROW (code)');
+ $this->assertSame('T_MATCH_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_ARROW (type)');
+
+ }//end testMatchArrow()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testMatchArrow()
+ *
+ * @return array
+ */
+ public function dataMatchArrow()
+ {
+ return [
+ 'single_case' => ['/* testMatchArrowSimpleSingleCase */'],
+ 'multi_case' => ['/* testMatchArrowSimpleMultiCase */'],
+ 'single_case_with_trailing_comma' => ['/* testMatchArrowSimpleSingleCaseWithTrailingComma */'],
+ 'multi_case_with_trailing_comma' => ['/* testMatchArrowSimpleMultiCaseWithTrailingComma */'],
+ 'match_nested_outer' => ['/* testMatchArrowNestedMatchOuter */'],
+ 'match_nested_inner' => ['/* testMatchArrowNestedMatchInner */'],
+
+ 'in_long_array_value_1' => ['/* testMatchArrowInLongArrayValue1 */'],
+ 'in_long_array_value_2' => ['/* testMatchArrowInLongArrayValue2 */'],
+ 'in_long_array_value_3' => ['/* testMatchArrowInLongArrayValue3 */'],
+ 'in_short_array_value_1' => ['/* testMatchArrowInShortArrayValue1 */'],
+ 'in_short_array_value_2' => ['/* testMatchArrowInShortArrayValue2 */'],
+ 'in_short_array_value_3' => ['/* testMatchArrowInShortArrayValue3 */'],
+
+ 'in_long_array_key_1' => ['/* testMatchArrowInLongArrayKey1 */'],
+ 'in_long_array_key_2' => ['/* testMatchArrowInLongArrayKey2 */'],
+ 'in_short_array_key_1' => ['/* testMatchArrowInShortArrayKey1 */'],
+ 'in_short_array_key_2' => ['/* testMatchArrowInShortArrayKey2 */'],
+
+ 'with_long_array_value_with_keys' => ['/* testMatchArrowWithLongArrayBodyWithKeys */'],
+ 'with_short_array_value_without_keys' => ['/* testMatchArrowWithShortArrayBodyWithoutKeys */'],
+ 'with_long_array_value_without_keys' => ['/* testMatchArrowWithLongArrayBodyWithoutKeys */'],
+ 'with_short_array_value_with_keys' => ['/* testMatchArrowWithShortArrayBodyWithKeys */'],
+
+ 'with_short_array_with_keys_as_case' => ['/* testMatchArrowWithShortArrayWithKeysAsCase */'],
+ 'with_multiple_arrays_with_keys_as_case' => ['/* testMatchArrowWithMultipleArraysWithKeysAsCase */'],
+
+ 'in_fn_body_case' => ['/* testMatchArrowInFnBody1 */'],
+ 'in_fn_body_default' => ['/* testMatchArrowInFnBody2 */'],
+ 'with_fn_body_case' => ['/* testMatchArrowWithFnBody1 */'],
+ 'with_fn_body_default' => ['/* testMatchArrowWithFnBody2 */'],
+
+ 'in_complex_short_array_key_1' => ['/* testMatchArrowInComplexShortArrayKey1 */'],
+ 'in_complex_short_array_key_2' => ['/* testMatchArrowInComplexShortArrayKey2 */'],
+ 'in_complex_short_array_value_1' => ['/* testMatchArrowInComplexShortArrayValue1 */'],
+ 'in_complex_short_array_value_2' => ['/* testMatchArrowInComplexShortArrayValue2 */'],
+
+ 'with_long_list_in_body' => ['/* testMatchArrowWithLongListBody */'],
+ 'with_long_list_in_case' => ['/* testMatchArrowWithLongListInCase */'],
+ 'with_short_list_in_body' => ['/* testMatchArrowWithShortListBody */'],
+ 'with_short_list_in_case' => ['/* testMatchArrowWithShortListInCase */'],
+ 'in_long_list_key' => ['/* testMatchArrowInLongListKey */'],
+ 'in_short_list_key' => ['/* testMatchArrowInShortListKey */'],
+
+ 'with_long_array_value_with_default_key' => ['/* testMatchArrowWithNestedLongArrayWithClassConstantKey */'],
+ 'with_short_array_value_with_default_key' => ['/* testMatchArrowWithNestedShortArrayWithClassConstantKey */'],
+ 'in_long_array_value_with_default_key' => ['/* testMatchArrowNestedInLongArrayWithClassConstantKey */'],
+ 'in_short_array_value_with_default_key' => ['/* testMatchArrowNestedInShortArrayWithClassConstantKey */'],
+ ];
+
+ }//end dataMatchArrow()
+
+
+ /**
+ * Test that double arrows used as the scope opener for an arrow function
+ * are correctly tokenized as `T_FN_ARROW`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ *
+ * @dataProvider dataFnArrow
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testFnArrow($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_FN_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_FN_ARROW (code)');
+ $this->assertSame('T_FN_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_FN_ARROW (type)');
+
+ }//end testFnArrow()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testFnArrow()
+ *
+ * @return array
+ */
+ public function dataFnArrow()
+ {
+ return [
+ 'simple_fn' => ['/* testFnArrowSimple */'],
+
+ 'with_match_as_value' => ['/* testFnArrowWithMatchInValue */'],
+ 'in_match_value_case' => ['/* testFnArrowInMatchBody1 */'],
+ 'in_match_value_default' => ['/* testFnArrowInMatchBody2 */'],
+
+ 'in_complex_match_value_in_short_array' => ['/* testFnArrowInComplexMatchValueInShortArrayValue */'],
+ ];
+
+ }//end dataFnArrow()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.inc b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc
new file mode 100644
index 0000000000..62535b1e41
--- /dev/null
+++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc
@@ -0,0 +1,52 @@
+bar";
+/* testProperty2 */
+"{$foo->bar}";
+
+/* testMethod1 */
+"{$foo->bar()}";
+
+/* testClosure1 */
+"{$foo()}";
+
+/* testChain1 */
+"{$foo['bar']->baz()()}";
+
+/* testVariableVar1 */
+"${$bar}";
+/* testVariableVar2 */
+"${(foo)}";
+/* testVariableVar3 */
+"${foo->bar}";
+
+/* testNested1 */
+"${foo["${bar}"]}";
+/* testNested2 */
+"${foo["${bar['baz']}"]}";
+/* testNested3 */
+"${foo->{$baz}}";
+/* testNested4 */
+"${foo->{${'a'}}}";
+/* testNested5 */
+"${foo->{"${'a'}"}}";
+
+/* testParseError */
+"${foo["${bar
diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.php b/tests/Core/Tokenizer/DoubleQuotedStringTest.php
new file mode 100644
index 0000000000..cc9fe49ec4
--- /dev/null
+++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.php
@@ -0,0 +1,136 @@
+
+ * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class DoubleQuotedStringTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that double quoted strings contain the complete string.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $expectedContent The expected content of the double quoted string.
+ *
+ * @dataProvider dataDoubleQuotedString
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testDoubleQuotedString($testMarker, $expectedContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, T_DOUBLE_QUOTED_STRING);
+ $this->assertSame($expectedContent, $tokens[$target]['content']);
+
+ }//end testDoubleQuotedString()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testDoubleQuotedString()
+ *
+ * @return array
+ */
+ public function dataDoubleQuotedString()
+ {
+ return [
+ [
+ 'testMarker' => '/* testSimple1 */',
+ 'expectedContent' => '"$foo"',
+ ],
+ [
+ 'testMarker' => '/* testSimple2 */',
+ 'expectedContent' => '"{$foo}"',
+ ],
+ [
+ 'testMarker' => '/* testSimple3 */',
+ 'expectedContent' => '"${foo}"',
+ ],
+ [
+ 'testMarker' => '/* testDIM1 */',
+ 'expectedContent' => '"$foo[bar]"',
+ ],
+ [
+ 'testMarker' => '/* testDIM2 */',
+ 'expectedContent' => '"{$foo[\'bar\']}"',
+ ],
+ [
+ 'testMarker' => '/* testDIM3 */',
+ 'expectedContent' => '"${foo[\'bar\']}"',
+ ],
+ [
+ 'testMarker' => '/* testProperty1 */',
+ 'expectedContent' => '"$foo->bar"',
+ ],
+ [
+ 'testMarker' => '/* testProperty2 */',
+ 'expectedContent' => '"{$foo->bar}"',
+ ],
+ [
+ 'testMarker' => '/* testMethod1 */',
+ 'expectedContent' => '"{$foo->bar()}"',
+ ],
+ [
+ 'testMarker' => '/* testClosure1 */',
+ 'expectedContent' => '"{$foo()}"',
+ ],
+ [
+ 'testMarker' => '/* testChain1 */',
+ 'expectedContent' => '"{$foo[\'bar\']->baz()()}"',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar1 */',
+ 'expectedContent' => '"${$bar}"',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar2 */',
+ 'expectedContent' => '"${(foo)}"',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar3 */',
+ 'expectedContent' => '"${foo->bar}"',
+ ],
+ [
+ 'testMarker' => '/* testNested1 */',
+ 'expectedContent' => '"${foo["${bar}"]}"',
+ ],
+ [
+ 'testMarker' => '/* testNested2 */',
+ 'expectedContent' => '"${foo["${bar[\'baz\']}"]}"',
+ ],
+ [
+ 'testMarker' => '/* testNested3 */',
+ 'expectedContent' => '"${foo->{$baz}}"',
+ ],
+ [
+ 'testMarker' => '/* testNested4 */',
+ 'expectedContent' => '"${foo->{${\'a\'}}}"',
+ ],
+ [
+ 'testMarker' => '/* testNested5 */',
+ 'expectedContent' => '"${foo->{"${\'a\'}"}}"',
+ ],
+ [
+ 'testMarker' => '/* testParseError */',
+ 'expectedContent' => '"${foo["${bar
+',
+ ],
+ ];
+
+ }//end dataDoubleQuotedString()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/EnumCaseTest.inc b/tests/Core/Tokenizer/EnumCaseTest.inc
new file mode 100644
index 0000000000..13b87242e1
--- /dev/null
+++ b/tests/Core/Tokenizer/EnumCaseTest.inc
@@ -0,0 +1,95 @@
+
+ * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class EnumCaseTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the enum "case" is converted to T_ENUM_CASE.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataEnumCases
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
+ *
+ * @return void
+ */
+ public function testEnumCases($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $enumCase = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]);
+
+ $this->assertSame(T_ENUM_CASE, $tokens[$enumCase]['code']);
+ $this->assertSame('T_ENUM_CASE', $tokens[$enumCase]['type']);
+
+ $this->assertArrayNotHasKey('scope_condition', $tokens[$enumCase], 'Scope condition is set');
+ $this->assertArrayNotHasKey('scope_opener', $tokens[$enumCase], 'Scope opener is set');
+ $this->assertArrayNotHasKey('scope_closer', $tokens[$enumCase], 'Scope closer is set');
+
+ }//end testEnumCases()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testEnumCases()
+ *
+ * @return array
+ */
+ public function dataEnumCases()
+ {
+ return [
+ ['/* testPureEnumCase */'],
+ ['/* testBackingIntegerEnumCase */'],
+ ['/* testBackingStringEnumCase */'],
+ ['/* testEnumCaseInComplexEnum */'],
+ ['/* testEnumCaseIsCaseInsensitive */'],
+ ['/* testEnumCaseAfterSwitch */'],
+ ['/* testEnumCaseAfterSwitchWithEndSwitch */'],
+ ];
+
+ }//end dataEnumCases()
+
+
+ /**
+ * Test that "case" that is not enum case is still tokenized as `T_CASE`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataNotEnumCases
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
+ *
+ * @return void
+ */
+ public function testNotEnumCases($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $case = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]);
+
+ $this->assertSame(T_CASE, $tokens[$case]['code']);
+ $this->assertSame('T_CASE', $tokens[$case]['type']);
+
+ $this->assertArrayHasKey('scope_condition', $tokens[$case], 'Scope condition is not set');
+ $this->assertArrayHasKey('scope_opener', $tokens[$case], 'Scope opener is not set');
+ $this->assertArrayHasKey('scope_closer', $tokens[$case], 'Scope closer is not set');
+
+ }//end testNotEnumCases()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotEnumCases()
+ *
+ * @return array
+ */
+ public function dataNotEnumCases()
+ {
+ return [
+ ['/* testCaseWithSemicolonIsNotEnumCase */'],
+ ['/* testCaseWithConstantIsNotEnumCase */'],
+ ['/* testCaseWithConstantAndIdenticalIsNotEnumCase */'],
+ ['/* testCaseWithAssigmentToConstantIsNotEnumCase */'],
+ ['/* testIsNotEnumCaseIsCaseInsensitive */'],
+ ['/* testCaseInSwitchWhenCreatingEnumInSwitch1 */'],
+ ['/* testCaseInSwitchWhenCreatingEnumInSwitch2 */'],
+ ];
+
+ }//end dataNotEnumCases()
+
+
+ /**
+ * Test that "case" that is not enum case is still tokenized as `T_CASE`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataKeywordAsEnumCaseNameShouldBeString
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testKeywordAsEnumCaseNameShouldBeString($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $enumCaseName = $this->getTargetToken($testMarker, [T_STRING, T_INTERFACE, T_TRAIT, T_ENUM, T_FUNCTION, T_FALSE, T_DEFAULT, T_ARRAY]);
+
+ $this->assertSame(T_STRING, $tokens[$enumCaseName]['code']);
+ $this->assertSame('T_STRING', $tokens[$enumCaseName]['type']);
+
+ }//end testKeywordAsEnumCaseNameShouldBeString()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testKeywordAsEnumCaseNameShouldBeString()
+ *
+ * @return array
+ */
+ public function dataKeywordAsEnumCaseNameShouldBeString()
+ {
+ return [
+ ['/* testKeywordAsEnumCaseNameShouldBeString1 */'],
+ ['/* testKeywordAsEnumCaseNameShouldBeString2 */'],
+ ['/* testKeywordAsEnumCaseNameShouldBeString3 */'],
+ ['/* testKeywordAsEnumCaseNameShouldBeString4 */'],
+ ];
+
+ }//end dataKeywordAsEnumCaseNameShouldBeString()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/FinallyTest.inc b/tests/Core/Tokenizer/FinallyTest.inc
new file mode 100644
index 0000000000..e65600b6b4
--- /dev/null
+++ b/tests/Core/Tokenizer/FinallyTest.inc
@@ -0,0 +1,40 @@
+finally = 'foo';
+ }
+}
diff --git a/tests/Core/Tokenizer/FinallyTest.php b/tests/Core/Tokenizer/FinallyTest.php
new file mode 100644
index 0000000000..2b28bc5d30
--- /dev/null
+++ b/tests/Core/Tokenizer/FinallyTest.php
@@ -0,0 +1,96 @@
+
+ * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class FinallyTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the finally keyword is tokenized as such.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataFinallyKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testFinallyKeyword($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, [T_FINALLY, T_STRING]);
+ $this->assertSame(T_FINALLY, $tokens[$target]['code']);
+ $this->assertSame('T_FINALLY', $tokens[$target]['type']);
+
+ }//end testFinallyKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testFinallyKeyword()
+ *
+ * @return array
+ */
+ public function dataFinallyKeyword()
+ {
+ return [
+ ['/* testTryCatchFinally */'],
+ ['/* testTryFinallyCatch */'],
+ ['/* testTryFinally */'],
+ ];
+
+ }//end dataFinallyKeyword()
+
+
+ /**
+ * Test that 'finally' when not used as the reserved keyword is tokenized as `T_STRING`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataFinallyNonKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testFinallyNonKeyword($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, [T_FINALLY, T_STRING]);
+ $this->assertSame(T_STRING, $tokens[$target]['code']);
+ $this->assertSame('T_STRING', $tokens[$target]['type']);
+
+ }//end testFinallyNonKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testFinallyNonKeyword()
+ *
+ * @return array
+ */
+ public function dataFinallyNonKeyword()
+ {
+ return [
+ ['/* testFinallyUsedAsClassConstantName */'],
+ ['/* testFinallyUsedAsMethodName */'],
+ ['/* testFinallyUsedAsPropertyName */'],
+ ];
+
+ }//end dataFinallyNonKeyword()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/GotoLabelTest.inc b/tests/Core/Tokenizer/GotoLabelTest.inc
new file mode 100644
index 0000000000..12df5d296b
--- /dev/null
+++ b/tests/Core/Tokenizer/GotoLabelTest.inc
@@ -0,0 +1,56 @@
+
+
+
+property:
+ // Do something.
+ break;
+}
+
+switch (true) {
+ /* testNotGotoDeclarationGlobalConstantInTernary */
+ case $x === ($cond) ? CONST_A : CONST_B:
+ // Do something.
+ break;
+}
+
+/* testNotGotoDeclarationEnumWithType */
+enum Suit: string implements Colorful, CardGame {}
diff --git a/tests/Core/Tokenizer/GotoLabelTest.php b/tests/Core/Tokenizer/GotoLabelTest.php
new file mode 100644
index 0000000000..0f937cc8b0
--- /dev/null
+++ b/tests/Core/Tokenizer/GotoLabelTest.php
@@ -0,0 +1,175 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class GotoLabelTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Verify that the label in a goto statement is tokenized as T_STRING.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to expect.
+ *
+ * @dataProvider dataGotoStatement
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testGotoStatement($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $label = $this->getTargetToken($testMarker, T_STRING);
+
+ $this->assertInternalType('int', $label);
+ $this->assertSame($testContent, $tokens[$label]['content']);
+
+ }//end testGotoStatement()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testGotoStatement()
+ *
+ * @return array
+ */
+ public function dataGotoStatement()
+ {
+ return [
+ [
+ '/* testGotoStatement */',
+ 'marker',
+ ],
+ [
+ '/* testGotoStatementInLoop */',
+ 'end',
+ ],
+ ];
+
+ }//end dataGotoStatement()
+
+
+ /**
+ * Verify that the label in a goto declaration is tokenized as T_GOTO_LABEL.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to expect.
+ *
+ * @dataProvider dataGotoDeclaration
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testGotoDeclaration($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $label = $this->getTargetToken($testMarker, T_GOTO_LABEL);
+
+ $this->assertInternalType('int', $label);
+ $this->assertSame($testContent, $tokens[$label]['content']);
+
+ }//end testGotoDeclaration()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testGotoDeclaration()
+ *
+ * @return array
+ */
+ public function dataGotoDeclaration()
+ {
+ return [
+ [
+ '/* testGotoDeclaration */',
+ 'marker:',
+ ],
+ [
+ '/* testGotoDeclarationOutsideLoop */',
+ 'end:',
+ ],
+ ];
+
+ }//end dataGotoDeclaration()
+
+
+ /**
+ * Verify that the constant used in a switch - case statement is not confused with a goto label.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to expect.
+ *
+ * @dataProvider dataNotAGotoDeclaration
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testNotAGotoDeclaration($testMarker, $testContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $target = $this->getTargetToken($testMarker, [T_GOTO_LABEL, T_STRING], $testContent);
+
+ $this->assertSame(T_STRING, $tokens[$target]['code']);
+ $this->assertSame('T_STRING', $tokens[$target]['type']);
+
+ }//end testNotAGotoDeclaration()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotAGotoDeclaration()
+ *
+ * @return array
+ */
+ public function dataNotAGotoDeclaration()
+ {
+ return [
+ [
+ '/* testNotGotoDeclarationGlobalConstant */',
+ 'CONSTANT',
+ ],
+ [
+ '/* testNotGotoDeclarationNamespacedConstant */',
+ 'CONSTANT',
+ ],
+ [
+ '/* testNotGotoDeclarationClassConstant */',
+ 'CONSTANT',
+ ],
+ [
+ '/* testNotGotoDeclarationClassProperty */',
+ 'property',
+ ],
+ [
+ '/* testNotGotoDeclarationGlobalConstantInTernary */',
+ 'CONST_A',
+ ],
+ [
+ '/* testNotGotoDeclarationGlobalConstantInTernary */',
+ 'CONST_B',
+ ],
+ [
+ '/* testNotGotoDeclarationEnumWithType */',
+ 'Suit',
+ ],
+ ];
+
+ }//end dataNotAGotoDeclaration()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc
new file mode 100644
index 0000000000..a800980b8e
--- /dev/null
+++ b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc
@@ -0,0 +1,43 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Config;
+use PHP_CodeSniffer\Ruleset;
+use PHP_CodeSniffer\Files\DummyFile;
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+/**
+ * Heredoc/nowdoc closer token test.
+ *
+ * @requires PHP 7.3
+ */
+class HeredocNowdocCloserTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Initialize & tokenize \PHP_CodeSniffer\Files\File with code from the test case file.
+ *
+ * {@internal This is a near duplicate of the original method. Only difference is that
+ * tab replacement is enabled for this test.}
+ *
+ * @return void
+ */
+ public static function setUpBeforeClass()
+ {
+ $config = new Config();
+ $config->standards = ['PSR1'];
+ $config->tabWidth = 4;
+
+ $ruleset = new Ruleset($config);
+
+ // Default to a file with the same name as the test class. Extension is property based.
+ $relativeCN = str_replace(__NAMESPACE__, '', get_called_class());
+ $relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativeCN);
+ $pathToTestFile = realpath(__DIR__).$relativePath.'.'.static::$fileExtension;
+
+ // Make sure the file gets parsed correctly based on the file type.
+ $contents = 'phpcs_input_file: '.$pathToTestFile.PHP_EOL;
+ $contents .= file_get_contents($pathToTestFile);
+
+ self::$phpcsFile = new DummyFile($contents, $ruleset, $config);
+ self::$phpcsFile->process();
+
+ }//end setUpBeforeClass()
+
+
+ /**
+ * Verify that leading (indent) whitespace in a heredoc/nowdoc closer token get the tab replacement treatment.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param array $expected Expectations for the token array.
+ *
+ * @dataProvider dataHeredocNowdocCloserTabReplacement
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap
+ *
+ * @return void
+ */
+ public function testHeredocNowdocCloserTabReplacement($testMarker, $expected)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $closer = $this->getTargetToken($testMarker, [T_END_HEREDOC, T_END_NOWDOC]);
+
+ foreach ($expected as $key => $value) {
+ if ($key === 'orig_content' && $value === null) {
+ $this->assertArrayNotHasKey($key, $tokens[$closer], "Unexpected 'orig_content' key found in the token array.");
+ continue;
+ }
+
+ $this->assertArrayHasKey($key, $tokens[$closer], "Key $key not found in the token array.");
+ $this->assertSame($value, $tokens[$closer][$key], "Value for key $key does not match expectation.");
+ }
+
+ }//end testHeredocNowdocCloserTabReplacement()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testHeredocNowdocCloserTabReplacement()
+ *
+ * @return array
+ */
+ public function dataHeredocNowdocCloserTabReplacement()
+ {
+ return [
+ [
+ 'testMarker' => '/* testHeredocCloserNoIndent */',
+ 'expected' => [
+ 'length' => 3,
+ 'content' => 'EOD',
+ 'orig_content' => null,
+ ],
+ ],
+ [
+ 'testMarker' => '/* testNowdocCloserNoIndent */',
+ 'expected' => [
+ 'length' => 3,
+ 'content' => 'EOD',
+ 'orig_content' => null,
+ ],
+ ],
+ [
+ 'testMarker' => '/* testHeredocCloserSpaceIndent */',
+ 'expected' => [
+ 'length' => 7,
+ 'content' => ' END',
+ 'orig_content' => null,
+ ],
+ ],
+ [
+ 'testMarker' => '/* testNowdocCloserSpaceIndent */',
+ 'expected' => [
+ 'length' => 8,
+ 'content' => ' END',
+ 'orig_content' => null,
+ ],
+ ],
+ [
+ 'testMarker' => '/* testHeredocCloserTabIndent */',
+ 'expected' => [
+ 'length' => 8,
+ 'content' => ' END',
+ 'orig_content' => ' END',
+ ],
+ ],
+ [
+ 'testMarker' => '/* testNowdocCloserTabIndent */',
+ 'expected' => [
+ 'length' => 7,
+ 'content' => ' END',
+ 'orig_content' => ' END',
+ ],
+ ],
+ ];
+
+ }//end dataHeredocNowdocCloserTabReplacement()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/HeredocStringTest.inc b/tests/Core/Tokenizer/HeredocStringTest.inc
new file mode 100644
index 0000000000..ae43e24a59
--- /dev/null
+++ b/tests/Core/Tokenizer/HeredocStringTest.inc
@@ -0,0 +1,193 @@
+bar
+EOD;
+
+/* testProperty2 */
+$heredoc = <<<"EOD"
+{$foo->bar}
+EOD;
+
+/* testMethod1 */
+$heredoc = <<bar()}
+EOD;
+
+/* testClosure1 */
+$heredoc = <<<"EOD"
+{$foo()}
+EOD;
+
+/* testChain1 */
+$heredoc = <<baz()()}
+EOD;
+
+/* testVariableVar1 */
+$heredoc = <<<"EOD"
+${$bar}
+EOD;
+
+/* testVariableVar2 */
+$heredoc = <<bar}
+EOD;
+
+/* testNested1 */
+$heredoc = <<{$baz}}
+EOD;
+
+/* testNested4 */
+$heredoc = <<<"EOD"
+${foo->{${'a'}}}
+EOD;
+
+/* testNested5 */
+$heredoc = <<{"${'a'}"}}
+EOD;
+
+/* testSimple1Wrapped */
+$heredoc = <<bar Something
+EOD;
+
+/* testProperty2Wrapped */
+$heredoc = <<<"EOD"
+Do {$foo->bar} Something
+EOD;
+
+/* testMethod1Wrapped */
+$heredoc = <<bar()} Something
+EOD;
+
+/* testClosure1Wrapped */
+$heredoc = <<<"EOD"
+Do {$foo()} Something
+EOD;
+
+/* testChain1Wrapped */
+$heredoc = <<baz()()} Something
+EOD;
+
+/* testVariableVar1Wrapped */
+$heredoc = <<<"EOD"
+Do ${$bar} Something
+EOD;
+
+/* testVariableVar2Wrapped */
+$heredoc = <<bar} Something
+EOD;
+
+/* testNested1Wrapped */
+$heredoc = <<{$baz}} Something
+EOD;
+
+/* testNested4Wrapped */
+$heredoc = <<<"EOD"
+Do ${foo->{${'a'}}} Something
+EOD;
+
+/* testNested5Wrapped */
+$heredoc = <<{"${'a'}"}} Something
+EOD;
diff --git a/tests/Core/Tokenizer/HeredocStringTest.php b/tests/Core/Tokenizer/HeredocStringTest.php
new file mode 100644
index 0000000000..2c808be906
--- /dev/null
+++ b/tests/Core/Tokenizer/HeredocStringTest.php
@@ -0,0 +1,153 @@
+
+ * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class HeredocStringTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that heredoc strings contain the complete interpolated string.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $expectedContent The expected content of the heredoc string.
+ *
+ * @dataProvider dataHeredocString
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testHeredocString($testMarker, $expectedContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, T_HEREDOC);
+ $this->assertSame($expectedContent."\n", $tokens[$target]['content']);
+
+ }//end testHeredocString()
+
+
+ /**
+ * Test that heredoc strings contain the complete interpolated string when combined with other texts.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param string $expectedContent The expected content of the heredoc string.
+ *
+ * @dataProvider dataHeredocString
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testHeredocStringWrapped($testMarker, $expectedContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $testMarker = substr($testMarker, 0, -3).'Wrapped */';
+ $target = $this->getTargetToken($testMarker, T_HEREDOC);
+ $this->assertSame('Do '.$expectedContent." Something\n", $tokens[$target]['content']);
+
+ }//end testHeredocStringWrapped()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testHeredocString()
+ *
+ * @return array
+ */
+ public function dataHeredocString()
+ {
+ return [
+ [
+ 'testMarker' => '/* testSimple1 */',
+ 'expectedContent' => '$foo',
+ ],
+ [
+ 'testMarker' => '/* testSimple2 */',
+ 'expectedContent' => '{$foo}',
+ ],
+ [
+ 'testMarker' => '/* testSimple3 */',
+ 'expectedContent' => '${foo}',
+ ],
+ [
+ 'testMarker' => '/* testDIM1 */',
+ 'expectedContent' => '$foo[bar]',
+ ],
+ [
+ 'testMarker' => '/* testDIM2 */',
+ 'expectedContent' => '{$foo[\'bar\']}',
+ ],
+ [
+ 'testMarker' => '/* testDIM3 */',
+ 'expectedContent' => '${foo[\'bar\']}',
+ ],
+ [
+ 'testMarker' => '/* testProperty1 */',
+ 'expectedContent' => '$foo->bar',
+ ],
+ [
+ 'testMarker' => '/* testProperty2 */',
+ 'expectedContent' => '{$foo->bar}',
+ ],
+ [
+ 'testMarker' => '/* testMethod1 */',
+ 'expectedContent' => '{$foo->bar()}',
+ ],
+ [
+ 'testMarker' => '/* testClosure1 */',
+ 'expectedContent' => '{$foo()}',
+ ],
+ [
+ 'testMarker' => '/* testChain1 */',
+ 'expectedContent' => '{$foo[\'bar\']->baz()()}',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar1 */',
+ 'expectedContent' => '${$bar}',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar2 */',
+ 'expectedContent' => '${(foo)}',
+ ],
+ [
+ 'testMarker' => '/* testVariableVar3 */',
+ 'expectedContent' => '${foo->bar}',
+ ],
+ [
+ 'testMarker' => '/* testNested1 */',
+ 'expectedContent' => '${foo["${bar}"]}',
+ ],
+ [
+ 'testMarker' => '/* testNested2 */',
+ 'expectedContent' => '${foo["${bar[\'baz\']}"]}',
+ ],
+ [
+ 'testMarker' => '/* testNested3 */',
+ 'expectedContent' => '${foo->{$baz}}',
+ ],
+ [
+ 'testMarker' => '/* testNested4 */',
+ 'expectedContent' => '${foo->{${\'a\'}}}',
+ ],
+ [
+ 'testMarker' => '/* testNested5 */',
+ 'expectedContent' => '${foo->{"${\'a\'}"}}',
+ ],
+ ];
+
+ }//end dataHeredocString()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc
new file mode 100644
index 0000000000..b9c0df24d4
--- /dev/null
+++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc
@@ -0,0 +1,407 @@
+getPos(skip: false),
+ count: count(array_or_countable: $array),
+ value: 50
+);
+
+array_fill(
+ start_index: /* testNestedFunctionCallInner1 */ $obj->getPos(skip: false),
+ count: /* testNestedFunctionCallInner2 */ count(array_or_countable: $array),
+ value: 50
+);
+
+/* testNamespaceOperatorFunction */
+namespace\function_name(label:$string, more: false);
+
+/* testNamespaceRelativeFunction */
+Partially\Qualified\function_name(label:$string, more: false);
+
+/* testNamespacedFQNFunction */
+\Fully\Qualified\function_name(label: $string, more:false);
+
+/* testVariableFunction */
+$fn(label: $string, more:false);
+
+/* testVariableVariableFunction */
+${$fn}(label: $string, more:false);
+
+/* testMethodCall */
+$obj->methodName(label: $foo, more: $bar);
+
+/* testVariableMethodCall */
+$obj->{$var}(label: $foo, more: $bar);
+
+/* testClassInstantiation */
+$obj = new MyClass(label: $string, more:false);
+
+/* testClassInstantiationSelf */
+$obj = new self(label: $string, more:true);
+
+/* testClassInstantiationStatic */
+$obj = new static(label: $string, more:false);
+
+/* testAnonClass */
+$anon = new class(label: $string, more: false) {
+ public function __construct($label, $more) {}
+};
+
+function myfoo( $💩💩💩, $Пасха, $_valid) {}
+/* testNonAsciiNames */
+foo(💩💩💩: [], Пасха: 'text', _valid: 123);
+
+/* testMixedPositionalAndNamedArgsWithTernary */
+foo( $cond ? true : false, name: $value2 );
+
+/* testNamedArgWithTernary */
+foo( label: $cond ? true : false, more: $cond ? CONSTANT_A : CONSTANT_B );
+
+/* testTernaryWithFunctionCallsInThenElse */
+echo $cond ? foo( label: $something ) : foo( more: $something_else );
+
+/* testTernaryWithConstantsInThenElse */
+echo $cond ? CONSTANT_NAME : OTHER_CONSTANT;
+
+switch ($s) {
+ /* testSwitchCaseWithConstant */
+ case MY_CONSTANT:
+ // Do something.
+ break;
+
+ /* testSwitchCaseWithClassProperty */
+ case $obj->property:
+ // Do something.
+ break;
+
+ /* testSwitchDefault */
+ default:
+ // Do something.
+ break;
+}
+
+/* testTernaryWithClosuresAndReturnTypes */
+$closure = $cond ? function() : bool {return true;} : function() : int {return 123;};
+
+/* testTernaryWithArrowFunctionsAndReturnTypes */
+$fn = $cond ? fn() : bool => true : fn() : int => 123;
+
+
+/* testCompileErrorNamedBeforePositional */
+// Not the concern of PHPCS. Should still be handled.
+test(param: $bar, $foo);
+
+/* testDuplicateName1 */
+// Error Exception, but not the concern of PHPCS. Should still be handled.
+test(param: 1, /* testDuplicateName2 */ param: 2);
+
+/* testIncorrectOrderWithVariadic */
+// Error Exception, but not the concern of PHPCS. Should still be handled.
+array_fill(start_index: 0, ...[100, 50]);
+
+/* testCompileErrorIncorrectOrderWithVariadic */
+// Not the concern of PHPCS. Should still be handled.
+test(...$values, param: $value); // Compile-time error
+
+/* testParseErrorNoValue */
+// Not the concern of PHPCS. Should still be handled.
+test(param1:, param2:);
+
+/* testParseErrorDynamicName */
+// Parse error. Ignore.
+function_name($variableStoringParamName: $value);
+
+/* testParseErrorExit */
+// Exit is a language construct, not a function. Named params not supported, handle it anyway.
+exit(status: $value);
+
+/* testParseErrorEmpty */
+// Empty is a language construct, not a function. Named params not supported, handle it anyway.
+empty(variable: $value);
+
+/* testParseErrorEval */
+// Eval is a language construct, not a function. Named params not supported, handle it anyway.
+eval(code: $value);
+
+/* testParseErrorArbitraryParentheses */
+// Parse error. Not named param, handle it anyway.
+$calc = (something: $value / $other);
+
+
+/* testReservedKeywordAbstract1 */
+foobar(abstract: $value, /* testReservedKeywordAbstract2 */ abstract: $value);
+
+/* testReservedKeywordAnd1 */
+foobar(and: $value, /* testReservedKeywordAnd2 */ and: $value);
+
+/* testReservedKeywordArray1 */
+foobar(array: $value, /* testReservedKeywordArray2 */ array: $value);
+
+/* testReservedKeywordAs1 */
+foobar(as: $value, /* testReservedKeywordAs2 */ as: $value);
+
+/* testReservedKeywordBreak1 */
+foobar(break: $value, /* testReservedKeywordBreak2 */ break: $value);
+
+/* testReservedKeywordCallable1 */
+foobar(callable: $value, /* testReservedKeywordCallable2 */ callable: $value);
+
+/* testReservedKeywordCase1 */
+foobar(case: $value, /* testReservedKeywordCase2 */ case: $value);
+
+/* testReservedKeywordCatch1 */
+foobar(catch: $value, /* testReservedKeywordCatch2 */ catch: $value);
+
+/* testReservedKeywordClass1 */
+foobar(class: $value, /* testReservedKeywordClass2 */ class: $value);
+
+/* testReservedKeywordClone1 */
+foobar(clone: $value, /* testReservedKeywordClone2 */ clone: $value);
+
+/* testReservedKeywordConst1 */
+foobar(const: $value, /* testReservedKeywordConst2 */ const: $value);
+
+/* testReservedKeywordContinue1 */
+foobar(continue: $value, /* testReservedKeywordContinue2 */ continue: $value);
+
+/* testReservedKeywordDeclare1 */
+foobar(declare: $value, /* testReservedKeywordDeclare2 */ declare: $value);
+
+/* testReservedKeywordDefault1 */
+foobar(default: $value, /* testReservedKeywordDefault2 */ default: $value);
+
+/* testReservedKeywordDie1 */
+foobar(die: $value, /* testReservedKeywordDie2 */ die: $value);
+
+/* testReservedKeywordDo1 */
+foobar(do: $value, /* testReservedKeywordDo2 */ do: $value);
+
+/* testReservedKeywordEcho1 */
+foobar(echo: $value, /* testReservedKeywordEcho2 */ echo: $value);
+
+/* testReservedKeywordElse1 */
+foobar(else: $value, /* testReservedKeywordElse2 */ else: $value);
+
+/* testReservedKeywordElseif1 */
+foobar(elseif: $value, /* testReservedKeywordElseif2 */ elseif: $value);
+
+/* testReservedKeywordEmpty1 */
+foobar(empty: $value, /* testReservedKeywordEmpty2 */ empty: $value);
+
+/* testReservedKeywordEnum1 */
+foobar(enum: $value, /* testReservedKeywordEnum2 */ enum: $value);
+
+/* testReservedKeywordEnddeclare1 */
+foobar(enddeclare: $value, /* testReservedKeywordEnddeclare2 */ enddeclare: $value);
+
+/* testReservedKeywordEndfor1 */
+foobar(endfor: $value, /* testReservedKeywordEndfor2 */ endfor: $value);
+
+/* testReservedKeywordEndforeach1 */
+foobar(endforeach: $value, /* testReservedKeywordEndforeach2 */ endforeach: $value);
+
+/* testReservedKeywordEndif1 */
+foobar(endif: $value, /* testReservedKeywordEndif2 */ endif: $value);
+
+/* testReservedKeywordEndswitch1 */
+foobar(endswitch: $value, /* testReservedKeywordEndswitch2 */ endswitch: $value);
+
+/* testReservedKeywordEndwhile1 */
+foobar(endwhile: $value, /* testReservedKeywordEndwhile2 */ endwhile: $value);
+
+/* testReservedKeywordEval1 */
+foobar(eval: $value, /* testReservedKeywordEval2 */ eval: $value);
+
+/* testReservedKeywordExit1 */
+foobar(exit: $value, /* testReservedKeywordExit2 */ exit: $value);
+
+/* testReservedKeywordExtends1 */
+foobar(extends: $value, /* testReservedKeywordExtends2 */ extends: $value);
+
+/* testReservedKeywordFinal1 */
+foobar(final: $value, /* testReservedKeywordFinal2 */ final: $value);
+
+/* testReservedKeywordFinally1 */
+foobar(finally: $value, /* testReservedKeywordFinally2 */ finally: $value);
+
+/* testReservedKeywordFn1 */
+foobar(fn: $value, /* testReservedKeywordFn2 */ fn: $value);
+
+/* testReservedKeywordFor1 */
+foobar(for: $value, /* testReservedKeywordFor2 */ for: $value);
+
+/* testReservedKeywordForeach1 */
+foobar(foreach: $value, /* testReservedKeywordForeach2 */ foreach: $value);
+
+/* testReservedKeywordFunction1 */
+foobar(function: $value, /* testReservedKeywordFunction2 */ function: $value);
+
+/* testReservedKeywordGlobal1 */
+foobar(global: $value, /* testReservedKeywordGlobal2 */ global: $value);
+
+/* testReservedKeywordGoto1 */
+foobar(goto: $value, /* testReservedKeywordGoto2 */ goto: $value);
+
+/* testReservedKeywordIf1 */
+foobar(if: $value, /* testReservedKeywordIf2 */ if: $value);
+
+/* testReservedKeywordImplements1 */
+foobar(implements: $value, /* testReservedKeywordImplements2 */ implements: $value);
+
+/* testReservedKeywordInclude1 */
+foobar(include: $value, /* testReservedKeywordInclude2 */ include: $value);
+
+/* testReservedKeywordInclude_once1 */
+foobar(include_once: $value, /* testReservedKeywordInclude_once2 */ include_once: $value);
+
+/* testReservedKeywordInstanceof1 */
+foobar(instanceof: $value, /* testReservedKeywordInstanceof2 */ instanceof: $value);
+
+/* testReservedKeywordInsteadof1 */
+foobar(insteadof: $value, /* testReservedKeywordInsteadof2 */ insteadof: $value);
+
+/* testReservedKeywordInterface1 */
+foobar(interface: $value, /* testReservedKeywordInterface2 */ interface: $value);
+
+/* testReservedKeywordIsset1 */
+foobar(isset: $value, /* testReservedKeywordIsset2 */ isset: $value);
+
+/* testReservedKeywordList1 */
+foobar(list: $value, /* testReservedKeywordList2 */ list: $value);
+
+/* testReservedKeywordMatch1 */
+foobar(match: $value, /* testReservedKeywordMatch2 */ match: $value);
+
+/* testReservedKeywordNamespace1 */
+foobar(namespace: $value, /* testReservedKeywordNamespace2 */ namespace: $value);
+
+/* testReservedKeywordNew1 */
+foobar(new: $value, /* testReservedKeywordNew2 */ new: $value);
+
+/* testReservedKeywordOr1 */
+foobar(or: $value, /* testReservedKeywordOr2 */ or: $value);
+
+/* testReservedKeywordPrint1 */
+foobar(print: $value, /* testReservedKeywordPrint2 */ print: $value);
+
+/* testReservedKeywordPrivate1 */
+foobar(private: $value, /* testReservedKeywordPrivate2 */ private: $value);
+
+/* testReservedKeywordProtected1 */
+foobar(protected: $value, /* testReservedKeywordProtected2 */ protected: $value);
+
+/* testReservedKeywordPublic1 */
+foobar(public: $value, /* testReservedKeywordPublic2 */ public: $value);
+
+/* testReservedKeywordReadonly1 */
+foobar(readonly: $value, /* testReservedKeywordReadonly2 */ readonly: $value);
+
+/* testReservedKeywordRequire1 */
+foobar(require: $value, /* testReservedKeywordRequire2 */ require: $value);
+
+/* testReservedKeywordRequire_once1 */
+foobar(require_once: $value, /* testReservedKeywordRequire_once2 */ require_once: $value);
+
+/* testReservedKeywordReturn1 */
+foobar(return: $value, /* testReservedKeywordReturn2 */ return: $value);
+
+/* testReservedKeywordStatic1 */
+foobar(static: $value, /* testReservedKeywordStatic2 */ static: $value);
+
+/* testReservedKeywordSwitch1 */
+foobar(switch: $value, /* testReservedKeywordSwitch2 */ switch: $value);
+
+/* testReservedKeywordThrow1 */
+foobar(throw: $value, /* testReservedKeywordThrow2 */ throw: $value);
+
+/* testReservedKeywordTrait1 */
+foobar(trait: $value, /* testReservedKeywordTrait2 */ trait: $value);
+
+/* testReservedKeywordTry1 */
+foobar(try: $value, /* testReservedKeywordTry2 */ try: $value);
+
+/* testReservedKeywordUnset1 */
+foobar(unset: $value, /* testReservedKeywordUnset2 */ unset: $value);
+
+/* testReservedKeywordUse1 */
+foobar(use: $value, /* testReservedKeywordUse2 */ use: $value);
+
+/* testReservedKeywordVar1 */
+foobar(var: $value, /* testReservedKeywordVar2 */ var: $value);
+
+/* testReservedKeywordWhile1 */
+foobar(while: $value, /* testReservedKeywordWhile2 */ while: $value);
+
+/* testReservedKeywordXor1 */
+foobar(xor: $value, /* testReservedKeywordXor2 */ xor: $value);
+
+/* testReservedKeywordYield1 */
+foobar(yield: $value, /* testReservedKeywordYield2 */ yield: $value);
+
+/* testReservedKeywordInt1 */
+foobar(int: $value, /* testReservedKeywordInt2 */ int: $value);
+
+/* testReservedKeywordFloat1 */
+foobar(float: $value, /* testReservedKeywordFloat2 */ float: $value);
+
+/* testReservedKeywordBool1 */
+foobar(bool: $value, /* testReservedKeywordBool2 */ bool: $value);
+
+/* testReservedKeywordString1 */
+foobar(string: $value, /* testReservedKeywordString2 */ string: $value);
+
+/* testReservedKeywordTrue1 */
+foobar(true: $value, /* testReservedKeywordTrue2 */ true: $value);
+
+/* testReservedKeywordFalse1 */
+foobar(false: $value, /* testReservedKeywordFalse2 */ false: $value);
+
+/* testReservedKeywordNull1 */
+foobar(null: $value, /* testReservedKeywordNull2 */ null: $value);
+
+/* testReservedKeywordVoid1 */
+foobar(void: $value, /* testReservedKeywordVoid2 */ void: $value);
+
+/* testReservedKeywordIterable1 */
+foobar(iterable: $value, /* testReservedKeywordIterable2 */ iterable: $value);
+
+/* testReservedKeywordObject1 */
+foobar(object: $value, /* testReservedKeywordObject2 */ object: $value);
+
+/* testReservedKeywordResource1 */
+foobar(resource: $value, /* testReservedKeywordResource2 */ resource: $value);
+
+/* testReservedKeywordMixed1 */
+foobar(mixed: $value, /* testReservedKeywordMixed2 */ mixed: $value);
+
+/* testReservedKeywordNumeric1 */
+foobar(numeric: $value, /* testReservedKeywordNumeric2 */ numeric: $value);
+
+/* testReservedKeywordParent1 */
+foobar(parent: $value, /* testReservedKeywordParent2 */ parent: $value);
+
+/* testReservedKeywordSelf1 */
+foobar(self: $value, /* testReservedKeywordSelf2 */ self: $value);
+
+/* testReservedKeywordNever1 */
+foobar(never: $value, /* testReservedKeywordNever2 */ never: $value);
diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php
new file mode 100644
index 0000000000..cc57637c6f
--- /dev/null
+++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php
@@ -0,0 +1,885 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class NamedFunctionCallArgumentsTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Verify that parameter labels are tokenized as T_PARAM_NAME and that
+ * the colon after it is tokenized as a T_COLON.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param array $parameters The token content for each parameter label to look for.
+ *
+ * @dataProvider dataNamedFunctionCallArguments
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testNamedFunctionCallArguments($testMarker, $parameters)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ foreach ($parameters as $content) {
+ $label = $this->getTargetToken($testMarker, [T_STRING, T_PARAM_NAME], $content);
+
+ $this->assertSame(
+ T_PARAM_NAME,
+ $tokens[$label]['code'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (code)'
+ );
+ $this->assertSame(
+ 'T_PARAM_NAME',
+ $tokens[$label]['type'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (type)'
+ );
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+ }//end foreach
+
+ }//end testNamedFunctionCallArguments()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNamedFunctionCallArguments()
+ *
+ * @return array
+ */
+ public function dataNamedFunctionCallArguments()
+ {
+ return [
+ [
+ '/* testNamedArgs */',
+ [
+ 'start_index',
+ 'count',
+ 'value',
+ ],
+ ],
+ [
+ '/* testNamedArgsMultiline */',
+ [
+ 'start_index',
+ 'count',
+ 'value',
+ ],
+ ],
+ [
+ '/* testNamedArgsWithWhitespaceAndComments */',
+ [
+ 'start_index',
+ 'count',
+ 'value',
+ ],
+ ],
+ [
+ '/* testMixedPositionalAndNamedArgs */',
+ ['double_encode'],
+ ],
+ [
+ '/* testNestedFunctionCallOuter */',
+ [
+ 'start_index',
+ 'count',
+ 'value',
+ ],
+ ],
+ [
+ '/* testNestedFunctionCallInner1 */',
+ ['skip'],
+ ],
+ [
+ '/* testNestedFunctionCallInner2 */',
+ ['array_or_countable'],
+ ],
+ [
+ '/* testNamespaceOperatorFunction */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testNamespaceRelativeFunction */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testNamespacedFQNFunction */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testVariableFunction */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testVariableVariableFunction */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testMethodCall */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testVariableMethodCall */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testClassInstantiation */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testClassInstantiationSelf */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testClassInstantiationStatic */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testAnonClass */',
+ [
+ 'label',
+ 'more',
+ ],
+ ],
+ [
+ '/* testNonAsciiNames */',
+ [
+ '💩💩💩',
+ 'Пасха',
+ '_valid',
+ ],
+ ],
+
+ // Coding errors which should still be handled.
+ [
+ '/* testCompileErrorNamedBeforePositional */',
+ ['param'],
+ ],
+ [
+ '/* testDuplicateName1 */',
+ ['param'],
+ ],
+ [
+ '/* testDuplicateName2 */',
+ ['param'],
+ ],
+ [
+ '/* testIncorrectOrderWithVariadic */',
+ ['start_index'],
+ ],
+ [
+ '/* testCompileErrorIncorrectOrderWithVariadic */',
+ ['param'],
+ ],
+ [
+ '/* testParseErrorNoValue */',
+ [
+ 'param1',
+ 'param2',
+ ],
+ ],
+ [
+ '/* testParseErrorExit */',
+ ['status'],
+ ],
+ [
+ '/* testParseErrorEmpty */',
+ ['variable'],
+ ],
+ [
+ '/* testParseErrorEval */',
+ ['code'],
+ ],
+ [
+ '/* testParseErrorArbitraryParentheses */',
+ ['something'],
+ ],
+ ];
+
+ }//end dataNamedFunctionCallArguments()
+
+
+ /**
+ * Verify that other T_STRING tokens within a function call are still tokenized as T_STRING.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $content The token content to look for.
+ *
+ * @dataProvider dataOtherTstringInFunctionCall
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testOtherTstringInFunctionCall($testMarker, $content)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $label = $this->getTargetToken($testMarker, [T_STRING, T_PARAM_NAME], $content);
+
+ $this->assertSame(
+ T_STRING,
+ $tokens[$label]['code'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_STRING (code)'
+ );
+ $this->assertSame(
+ 'T_STRING',
+ $tokens[$label]['type'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_STRING (type)'
+ );
+
+ }//end testOtherTstringInFunctionCall()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testOtherTstringInFunctionCall()
+ *
+ * @return array
+ */
+ public function dataOtherTstringInFunctionCall()
+ {
+ return [
+ [
+ '/* testPositionalArgs */',
+ 'START_INDEX',
+ ],
+ [
+ '/* testPositionalArgs */',
+ 'COUNT',
+ ],
+ [
+ '/* testPositionalArgs */',
+ 'VALUE',
+ ],
+ [
+ '/* testNestedFunctionCallInner2 */',
+ 'count',
+ ],
+ ];
+
+ }//end dataOtherTstringInFunctionCall()
+
+
+ /**
+ * Verify whether the colons are tokenized correctly when a ternary is used in a mixed
+ * positional and named arguments function call.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testMixedPositionalAndNamedArgsWithTernary()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $true = $this->getTargetToken('/* testMixedPositionalAndNamedArgsWithTernary */', T_TRUE);
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true);
+
+ $this->assertSame(
+ T_INLINE_ELSE,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)'
+ );
+ $this->assertSame(
+ 'T_INLINE_ELSE',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)'
+ );
+
+ $label = $this->getTargetToken('/* testMixedPositionalAndNamedArgsWithTernary */', T_PARAM_NAME, 'name');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ }//end testMixedPositionalAndNamedArgsWithTernary()
+
+
+ /**
+ * Verify whether the colons are tokenized correctly when a ternary is used
+ * in a named arguments function call.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testNamedArgWithTernary()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ /*
+ * First argument.
+ */
+
+ $label = $this->getTargetToken('/* testNamedArgWithTernary */', T_PARAM_NAME, 'label');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'First arg: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'First arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'First arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ $true = $this->getTargetToken('/* testNamedArgWithTernary */', T_TRUE);
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true);
+
+ $this->assertSame(
+ T_INLINE_ELSE,
+ $tokens[$colon]['code'],
+ 'First arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)'
+ );
+ $this->assertSame(
+ 'T_INLINE_ELSE',
+ $tokens[$colon]['type'],
+ 'First arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)'
+ );
+
+ /*
+ * Second argument.
+ */
+
+ $label = $this->getTargetToken('/* testNamedArgWithTernary */', T_PARAM_NAME, 'more');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Second arg: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Second arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Second arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ $true = $this->getTargetToken('/* testNamedArgWithTernary */', T_STRING, 'CONSTANT_A');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true);
+
+ $this->assertSame(
+ T_INLINE_ELSE,
+ $tokens[$colon]['code'],
+ 'Second arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)'
+ );
+ $this->assertSame(
+ 'T_INLINE_ELSE',
+ $tokens[$colon]['type'],
+ 'Second arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)'
+ );
+
+ }//end testNamedArgWithTernary()
+
+
+ /**
+ * Verify whether the colons are tokenized correctly when named arguments
+ * function calls are used in a ternary.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testTernaryWithFunctionCallsInThenElse()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ /*
+ * Then.
+ */
+
+ $label = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_PARAM_NAME, 'label');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Function in then: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Function in then: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Function in then: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ $closeParens = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_CLOSE_PARENTHESIS);
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($closeParens + 1), null, true);
+
+ $this->assertSame(
+ T_INLINE_ELSE,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)'
+ );
+ $this->assertSame(
+ 'T_INLINE_ELSE',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)'
+ );
+
+ /*
+ * Else.
+ */
+
+ $label = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_PARAM_NAME, 'more');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Function in else: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Function in else: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Function in else: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ }//end testTernaryWithFunctionCallsInThenElse()
+
+
+ /**
+ * Verify whether the colons are tokenized correctly when constants are used in a ternary.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testTernaryWithConstantsInThenElse()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $constant = $this->getTargetToken('/* testTernaryWithConstantsInThenElse */', T_STRING, 'CONSTANT_NAME');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($constant + 1), null, true);
+
+ $this->assertSame(
+ T_INLINE_ELSE,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)'
+ );
+ $this->assertSame(
+ 'T_INLINE_ELSE',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)'
+ );
+
+ }//end testTernaryWithConstantsInThenElse()
+
+
+ /**
+ * Verify whether the colons are tokenized correctly in a switch statement.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testSwitchStatement()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $label = $this->getTargetToken('/* testSwitchCaseWithConstant */', T_STRING, 'MY_CONSTANT');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'First case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'First case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ $label = $this->getTargetToken('/* testSwitchCaseWithClassProperty */', T_STRING, 'property');
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Second case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Second case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ $default = $this->getTargetToken('/* testSwitchDefault */', T_DEFAULT);
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($default + 1), null, true);
+
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Default case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Default case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ }//end testSwitchStatement()
+
+
+ /**
+ * Verify that a variable parameter label (parse error) is still tokenized as T_VARIABLE.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testParseErrorVariableLabel()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $label = $this->getTargetToken('/* testParseErrorDynamicName */', [T_VARIABLE, T_PARAM_NAME], '$variableStoringParamName');
+
+ $this->assertSame(
+ T_VARIABLE,
+ $tokens[$label]['code'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_VARIABLE (code)'
+ );
+ $this->assertSame(
+ 'T_VARIABLE',
+ $tokens[$label]['type'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_VARIABLE (type)'
+ );
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ }//end testParseErrorVariableLabel()
+
+
+ /**
+ * Verify that reserved keywords used as a parameter label are tokenized as T_PARAM_NAME
+ * and that the colon after it is tokenized as a T_COLON.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param array $tokenTypes The token codes to look for.
+ * @param string $tokenContent The token content to look for.
+ *
+ * @dataProvider dataReservedKeywordsAsName
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testReservedKeywordsAsName($testMarker, $tokenTypes, $tokenContent)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $label = $this->getTargetToken($testMarker, $tokenTypes, $tokenContent);
+
+ $this->assertSame(
+ T_PARAM_NAME,
+ $tokens[$label]['code'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (code)'
+ );
+ $this->assertSame(
+ 'T_PARAM_NAME',
+ $tokens[$label]['type'],
+ 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (type)'
+ );
+
+ // Get the next non-empty token.
+ $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true);
+
+ $this->assertSame(
+ ':',
+ $tokens[$colon]['content'],
+ 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content']
+ );
+ $this->assertSame(
+ T_COLON,
+ $tokens[$colon]['code'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)'
+ );
+ $this->assertSame(
+ 'T_COLON',
+ $tokens[$colon]['type'],
+ 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)'
+ );
+
+ }//end testReservedKeywordsAsName()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testReservedKeywordsAsName()
+ *
+ * @return array
+ */
+ public function dataReservedKeywordsAsName()
+ {
+ $reservedKeywords = [
+ // '__halt_compiler', NOT TESTABLE
+ 'abstract',
+ 'and',
+ 'array',
+ 'as',
+ 'break',
+ 'callable',
+ 'case',
+ 'catch',
+ 'class',
+ 'clone',
+ 'const',
+ 'continue',
+ 'declare',
+ 'default',
+ 'die',
+ 'do',
+ 'echo',
+ 'else',
+ 'elseif',
+ 'empty',
+ 'enddeclare',
+ 'endfor',
+ 'endforeach',
+ 'endif',
+ 'endswitch',
+ 'endwhile',
+ 'enum',
+ 'eval',
+ 'exit',
+ 'extends',
+ 'final',
+ 'finally',
+ 'fn',
+ 'for',
+ 'foreach',
+ 'function',
+ 'global',
+ 'goto',
+ 'if',
+ 'implements',
+ 'include',
+ 'include_once',
+ 'instanceof',
+ 'insteadof',
+ 'interface',
+ 'isset',
+ 'list',
+ 'match',
+ 'namespace',
+ 'new',
+ 'or',
+ 'print',
+ 'private',
+ 'protected',
+ 'public',
+ 'readonly',
+ 'require',
+ 'require_once',
+ 'return',
+ 'static',
+ 'switch',
+ 'throw',
+ 'trait',
+ 'try',
+ 'unset',
+ 'use',
+ 'var',
+ 'while',
+ 'xor',
+ 'yield',
+ 'int',
+ 'float',
+ 'bool',
+ 'string',
+ 'true',
+ 'false',
+ 'null',
+ 'void',
+ 'iterable',
+ 'object',
+ 'resource',
+ 'mixed',
+ 'numeric',
+ 'never',
+
+ // Not reserved keyword, but do have their own token in PHPCS.
+ 'parent',
+ 'self',
+ ];
+
+ $data = [];
+
+ foreach ($reservedKeywords as $keyword) {
+ $tokensTypes = [
+ T_PARAM_NAME,
+ T_STRING,
+ T_GOTO_LABEL,
+ ];
+ $tokenName = 'T_'.strtoupper($keyword);
+
+ if ($keyword === 'and') {
+ $tokensTypes[] = T_LOGICAL_AND;
+ } else if ($keyword === 'die') {
+ $tokensTypes[] = T_EXIT;
+ } else if ($keyword === 'or') {
+ $tokensTypes[] = T_LOGICAL_OR;
+ } else if ($keyword === 'xor') {
+ $tokensTypes[] = T_LOGICAL_XOR;
+ } else if ($keyword === '__halt_compiler') {
+ $tokensTypes[] = T_HALT_COMPILER;
+ } else if (defined($tokenName) === true) {
+ $tokensTypes[] = constant($tokenName);
+ }
+
+ $data[$keyword.'FirstParam'] = [
+ '/* testReservedKeyword'.ucfirst($keyword).'1 */',
+ $tokensTypes,
+ $keyword,
+ ];
+
+ $data[$keyword.'SecondParam'] = [
+ '/* testReservedKeyword'.ucfirst($keyword).'2 */',
+ $tokensTypes,
+ $keyword,
+ ];
+ }//end foreach
+
+ return $data;
+
+ }//end dataReservedKeywordsAsName()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc
new file mode 100644
index 0000000000..982841eac4
--- /dev/null
+++ b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc
@@ -0,0 +1,29 @@
+foo;
+
+/* testNullsafeObjectOperator */
+echo $obj?->foo;
+
+/* testNullsafeObjectOperatorWriteContext */
+// Intentional parse error, but not the concern of the tokenizer.
+$foo?->bar->baz = 'baz';
+
+/* testTernaryThen */
+echo $obj ? $obj->prop : $other->prop;
+
+/* testParseErrorWhitespaceNotAllowed */
+echo $obj ?
+ -> foo;
+
+/* testParseErrorCommentNotAllowed */
+echo $obj ?/*comment*/-> foo;
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+echo $obj?
diff --git a/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php
new file mode 100644
index 0000000000..8e465a3be1
--- /dev/null
+++ b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php
@@ -0,0 +1,140 @@
+= 8.0 nullsafe object operator.
+ *
+ * @author Juliette Reinders Folmer
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class NullsafeObjectOperatorTest extends AbstractMethodUnitTest
+{
+
+ /**
+ * Tokens to search for.
+ *
+ * @var array
+ */
+ protected $find = [
+ T_NULLSAFE_OBJECT_OPERATOR,
+ T_OBJECT_OPERATOR,
+ T_INLINE_THEN,
+ ];
+
+
+ /**
+ * Test that a normal object operator is still tokenized as such.
+ *
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testObjectOperator()
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $operator = $this->getTargetToken('/* testObjectOperator */', $this->find);
+ $this->assertSame(T_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is object operator');
+ $this->assertSame('T_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is object operator');
+
+ }//end testObjectOperator()
+
+
+ /**
+ * Test that a nullsafe object operator is tokenized as such.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataNullsafeObjectOperator
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testNullsafeObjectOperator($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $operator = $this->getTargetToken($testMarker, $this->find);
+ $this->assertSame(T_NULLSAFE_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is nullsafe object operator');
+ $this->assertSame('T_NULLSAFE_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is nullsafe object operator');
+
+ }//end testNullsafeObjectOperator()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNullsafeObjectOperator()
+ *
+ * @return array
+ */
+ public function dataNullsafeObjectOperator()
+ {
+ return [
+ ['/* testNullsafeObjectOperator */'],
+ ['/* testNullsafeObjectOperatorWriteContext */'],
+ ];
+
+ }//end dataNullsafeObjectOperator()
+
+
+ /**
+ * Test that a question mark not followed by an object operator is tokenized as T_TERNARY_THEN.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ * @param bool $testObjectOperator Whether to test for the next non-empty token being tokenized
+ * as an object operator.
+ *
+ * @dataProvider dataTernaryThen
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testTernaryThen($testMarker, $testObjectOperator=false)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $operator = $this->getTargetToken($testMarker, $this->find);
+ $this->assertSame(T_INLINE_THEN, $tokens[$operator]['code'], 'Failed asserting code is inline then');
+ $this->assertSame('T_INLINE_THEN', $tokens[$operator]['type'], 'Failed asserting type is inline then');
+
+ if ($testObjectOperator === true) {
+ $next = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($operator + 1), null, true);
+ $this->assertSame(T_OBJECT_OPERATOR, $tokens[$next]['code'], 'Failed asserting code is object operator');
+ $this->assertSame('T_OBJECT_OPERATOR', $tokens[$next]['type'], 'Failed asserting type is object operator');
+ }
+
+ }//end testTernaryThen()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testTernaryThen()
+ *
+ * @return array
+ */
+ public function dataTernaryThen()
+ {
+ return [
+ ['/* testTernaryThen */'],
+ [
+ '/* testParseErrorWhitespaceNotAllowed */',
+ true,
+ ],
+ [
+ '/* testParseErrorCommentNotAllowed */',
+ true,
+ ],
+ ['/* testLiveCoding */'],
+ ];
+
+ }//end dataTernaryThen()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc
new file mode 100644
index 0000000000..38e5a47d53
--- /dev/null
+++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc
@@ -0,0 +1,19 @@
+ new namespace\Baz;
diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php
new file mode 100644
index 0000000000..23cbd9877c
--- /dev/null
+++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php
@@ -0,0 +1,98 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class ScopeSettingWithNamespaceOperatorTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the scope opener/closers are set correctly when the namespace keyword is encountered as an operator.
+ *
+ * @param string $testMarker The comment which prefaces the target tokens in the test file.
+ * @param int|string[] $tokenTypes The token type to search for.
+ * @param int|string[] $open Optional. The token type for the scope opener.
+ * @param int|string[] $close Optional. The token type for the scope closer.
+ *
+ * @dataProvider dataScopeSetting
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap
+ *
+ * @return void
+ */
+ public function testScopeSetting($testMarker, $tokenTypes, $open=T_OPEN_CURLY_BRACKET, $close=T_CLOSE_CURLY_BRACKET)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $target = $this->getTargetToken($testMarker, $tokenTypes);
+ $opener = $this->getTargetToken($testMarker, $open);
+ $closer = $this->getTargetToken($testMarker, $close);
+
+ $this->assertArrayHasKey('scope_opener', $tokens[$target], 'Scope opener missing');
+ $this->assertArrayHasKey('scope_closer', $tokens[$target], 'Scope closer missing');
+ $this->assertSame($opener, $tokens[$target]['scope_opener'], 'Scope opener not same');
+ $this->assertSame($closer, $tokens[$target]['scope_closer'], 'Scope closer not same');
+
+ $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Scope opener missing for open curly');
+ $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Scope closer missing for open curly');
+ $this->assertSame($opener, $tokens[$opener]['scope_opener'], 'Scope opener not same for open curly');
+ $this->assertSame($closer, $tokens[$opener]['scope_closer'], 'Scope closer not same for open curly');
+
+ $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Scope opener missing for close curly');
+ $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Scope closer missing for close curly');
+ $this->assertSame($opener, $tokens[$closer]['scope_opener'], 'Scope opener not same for close curly');
+ $this->assertSame($closer, $tokens[$closer]['scope_closer'], 'Scope closer not same for close curly');
+
+ }//end testScopeSetting()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testScopeSetting()
+ *
+ * @return array
+ */
+ public function dataScopeSetting()
+ {
+ return [
+ [
+ '/* testClassExtends */',
+ [T_CLASS],
+ ],
+ [
+ '/* testClassImplements */',
+ [T_ANON_CLASS],
+ ],
+ [
+ '/* testInterfaceExtends */',
+ [T_INTERFACE],
+ ],
+ [
+ '/* testFunctionReturnType */',
+ [T_FUNCTION],
+ ],
+ [
+ '/* testClosureReturnType */',
+ [T_CLOSURE],
+ ],
+ [
+ '/* testArrowFunctionReturnType */',
+ [T_FN],
+ [T_FN_ARROW],
+ [T_SEMICOLON],
+ ],
+ ];
+
+ }//end dataScopeSetting()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/ShortArrayTest.inc b/tests/Core/Tokenizer/ShortArrayTest.inc
new file mode 100644
index 0000000000..60b23a51cc
--- /dev/null
+++ b/tests/Core/Tokenizer/ShortArrayTest.inc
@@ -0,0 +1,111 @@
+function_call()[$x];
+
+/* testStaticMethodCallDereferencing */
+$var = ClassName::function_call()[$x];
+
+/* testPropertyDereferencing */
+$var = $obj->property[2];
+
+/* testPropertyDereferencingWithInaccessibleName */
+$var = $ref->{'ref-type'}[1];
+
+/* testStaticPropertyDereferencing */
+$var ClassName::$property[2];
+
+/* testStringDereferencing */
+$var = 'PHP'[1];
+
+/* testStringDereferencingDoubleQuoted */
+$var = "PHP"[$y];
+
+/* testConstantDereferencing */
+$var = MY_CONSTANT[1];
+
+/* testClassConstantDereferencing */
+$var ClassName::CONSTANT_NAME[2];
+
+/* testMagicConstantDereferencing */
+$var = __FILE__[0];
+
+/* testArrayAccessCurlyBraces */
+$var = $array{'key'}['key'];
+
+/* testArrayLiteralDereferencing */
+echo array(1, 2, 3)[0];
+
+echo [1, 2, 3]/* testShortArrayLiteralDereferencing */[0];
+
+/* testClassMemberDereferencingOnInstantiation1 */
+(new foo)[0];
+
+/* testClassMemberDereferencingOnInstantiation2 */
+$a = (new Foo( array(1, array(4, 5), 3) ))[1][0];
+
+/* testClassMemberDereferencingOnClone */
+echo (clone $iterable)[20];
+
+/* testNullsafeMethodCallDereferencing */
+$var = $obj?->function_call()[$x];
+
+/* testInterpolatedStringDereferencing */
+$var = "PHP{$rocks}"[1];
+
+/*
+ * Short array brackets.
+ */
+
+/* testShortArrayDeclarationEmpty */
+$array = [];
+
+/* testShortArrayDeclarationWithOneValue */
+$array = [1];
+
+/* testShortArrayDeclarationWithMultipleValues */
+$array = [1, 2, 3];
+
+/* testShortArrayDeclarationWithDereferencing */
+echo [1, 2, 3][0];
+
+/* testShortListDeclaration */
+[ $a, $b ] = $array;
+
+[ $a, $b, /* testNestedListDeclaration */, [$c, $d]] = $array;
+
+/* testArrayWithinFunctionCall */
+$var = functionCall([$x, $y]);
+
+if ( true ) {
+ /* testShortListDeclarationAfterBracedControlStructure */
+ [ $a ] = [ 'hi' ];
+}
+
+if ( true )
+ /* testShortListDeclarationAfterNonBracedControlStructure */
+ [ $a ] = [ 'hi' ];
+
+if ( true ) :
+ /* testShortListDeclarationAfterAlternativeControlStructure */
+ [ $a ] = [ 'hi' ];
+endif;
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+$array = [
diff --git a/tests/Core/Tokenizer/ShortArrayTest.php b/tests/Core/Tokenizer/ShortArrayTest.php
new file mode 100644
index 0000000000..1d97894f59
--- /dev/null
+++ b/tests/Core/Tokenizer/ShortArrayTest.php
@@ -0,0 +1,135 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class ShortArrayTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that real square brackets are still tokenized as square brackets.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataSquareBrackets
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testSquareBrackets($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY]);
+ $this->assertSame(T_OPEN_SQUARE_BRACKET, $tokens[$opener]['code']);
+ $this->assertSame('T_OPEN_SQUARE_BRACKET', $tokens[$opener]['type']);
+
+ if (isset($tokens[$opener]['bracket_closer']) === true) {
+ $closer = $tokens[$opener]['bracket_closer'];
+ $this->assertSame(T_CLOSE_SQUARE_BRACKET, $tokens[$closer]['code']);
+ $this->assertSame('T_CLOSE_SQUARE_BRACKET', $tokens[$closer]['type']);
+ }
+
+ }//end testSquareBrackets()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testSquareBrackets()
+ *
+ * @return array
+ */
+ public function dataSquareBrackets()
+ {
+ return [
+ 'array access 1' => ['/* testArrayAccess1 */'],
+ 'array access 2' => ['/* testArrayAccess2 */'],
+ 'array assignment' => ['/* testArrayAssignment */'],
+ 'function call dereferencing' => ['/* testFunctionCallDereferencing */'],
+ 'method call dereferencing' => ['/* testMethodCallDereferencing */'],
+ 'static method call dereferencing' => ['/* testStaticMethodCallDereferencing */'],
+ 'property dereferencing' => ['/* testPropertyDereferencing */'],
+ 'property dereferencing with inaccessable name' => ['/* testPropertyDereferencingWithInaccessibleName */'],
+ 'static property dereferencing' => ['/* testStaticPropertyDereferencing */'],
+ 'string dereferencing single quotes' => ['/* testStringDereferencing */'],
+ 'string dereferencing double quotes' => ['/* testStringDereferencingDoubleQuoted */'],
+ 'global constant dereferencing' => ['/* testConstantDereferencing */'],
+ 'class constant dereferencing' => ['/* testClassConstantDereferencing */'],
+ 'magic constant dereferencing' => ['/* testMagicConstantDereferencing */'],
+ 'array access with curly braces' => ['/* testArrayAccessCurlyBraces */'],
+ 'array literal dereferencing' => ['/* testArrayLiteralDereferencing */'],
+ 'short array literal dereferencing' => ['/* testShortArrayLiteralDereferencing */'],
+ 'class member dereferencing on instantiation 1' => ['/* testClassMemberDereferencingOnInstantiation1 */'],
+ 'class member dereferencing on instantiation 2' => ['/* testClassMemberDereferencingOnInstantiation2 */'],
+ 'class member dereferencing on clone' => ['/* testClassMemberDereferencingOnClone */'],
+ 'nullsafe method call dereferencing' => ['/* testNullsafeMethodCallDereferencing */'],
+ 'interpolated string dereferencing' => ['/* testInterpolatedStringDereferencing */'],
+ 'live coding' => ['/* testLiveCoding */'],
+ ];
+
+ }//end dataSquareBrackets()
+
+
+ /**
+ * Test that short arrays and short lists are still tokenized as short arrays.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataShortArrays
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testShortArrays($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY]);
+ $this->assertSame(T_OPEN_SHORT_ARRAY, $tokens[$opener]['code']);
+ $this->assertSame('T_OPEN_SHORT_ARRAY', $tokens[$opener]['type']);
+
+ if (isset($tokens[$opener]['bracket_closer']) === true) {
+ $closer = $tokens[$opener]['bracket_closer'];
+ $this->assertSame(T_CLOSE_SHORT_ARRAY, $tokens[$closer]['code']);
+ $this->assertSame('T_CLOSE_SHORT_ARRAY', $tokens[$closer]['type']);
+ }
+
+ }//end testShortArrays()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testShortArrays()
+ *
+ * @return array
+ */
+ public function dataShortArrays()
+ {
+ return [
+ 'short array empty' => ['/* testShortArrayDeclarationEmpty */'],
+ 'short array with value' => ['/* testShortArrayDeclarationWithOneValue */'],
+ 'short array with values' => ['/* testShortArrayDeclarationWithMultipleValues */'],
+ 'short array with dereferencing' => ['/* testShortArrayDeclarationWithDereferencing */'],
+ 'short list' => ['/* testShortListDeclaration */'],
+ 'short list nested' => ['/* testNestedListDeclaration */'],
+ 'short array within function call' => ['/* testArrayWithinFunctionCall */'],
+ 'short list after braced control structure' => ['/* testShortListDeclarationAfterBracedControlStructure */'],
+ 'short list after non-braced control structure' => ['/* testShortListDeclarationAfterNonBracedControlStructure */'],
+ 'short list after alternative control structure' => ['/* testShortListDeclarationAfterAlternativeControlStructure */'],
+ ];
+
+ }//end dataShortArrays()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc b/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc
new file mode 100644
index 0000000000..3bf013c66b
--- /dev/null
+++ b/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc
@@ -0,0 +1,139 @@
+
+
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class StableCommentWhitespaceTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that comment tokenization with new lines at the end of the comment is stable.
+ *
+ * @param string $testMarker The comment prefacing the test.
+ * @param array $expectedTokens The tokenization expected.
+ *
+ * @dataProvider dataCommentTokenization
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testCommentTokenization($testMarker, $expectedTokens)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $comment = $this->getTargetToken($testMarker, Tokens::$commentTokens);
+
+ foreach ($expectedTokens as $key => $tokenInfo) {
+ $this->assertSame(constant($tokenInfo['type']), $tokens[$comment]['code']);
+ $this->assertSame($tokenInfo['type'], $tokens[$comment]['type']);
+ $this->assertSame($tokenInfo['content'], $tokens[$comment]['content']);
+
+ ++$comment;
+ }
+
+ }//end testCommentTokenization()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testCommentTokenization()
+ *
+ * @return array
+ */
+ public function dataCommentTokenization()
+ {
+ return [
+ [
+ '/* testSingleLineSlashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashCommentTrailing */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashAnnotation */',
+ [
+ [
+ 'type' => 'T_PHPCS_DISABLE',
+ 'content' => '// phpcs:disable Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationStart */',
+ [
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationMiddle */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// @phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineStarComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Single line star comment */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineStarCommentTrailing */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineStarAnnotation */',
+ [
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '/* phpcs:ignore Stnd.Cat */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineStarComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment3 */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineStarCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment3 */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineStarCommentWithAnnotationStart */',
+ [
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '/* @phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment3 */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineStarCommentWithAnnotationMiddle */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment1
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => ' * phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment3 */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineStarCommentWithAnnotationEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => ' * Comment2
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => ' * phpcs:ignore Stnd.Cat */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+
+ [
+ '/* testSingleLineDocblockComment */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineDocblockCommentTrailing */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineDocblockAnnotation */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => 'phpcs:ignore Stnd.Cat.Sniff ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+
+ [
+ '/* testMultiLineDocblockComment */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment1',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment2',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_TAG',
+ 'content' => '@tag',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineDocblockCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment1',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment2',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_TAG',
+ 'content' => '@tag',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineDocblockCommentWithAnnotation */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => 'phpcs:ignore Stnd.Cat',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_TAG',
+ 'content' => '@tag',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineDocblockCommentWithTagAnnotation */',
+ [
+ [
+ 'type' => 'T_DOC_COMMENT_OPEN_TAG',
+ 'content' => '/**',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '@phpcs:ignore Stnd.Cat',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STAR',
+ 'content' => '*',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_TAG',
+ 'content' => '@tag',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_STRING',
+ 'content' => 'Comment',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_DOC_COMMENT_CLOSE_TAG',
+ 'content' => '*/',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashCommentTrailing */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineHashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineHashCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment1
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment2
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashCommentNoNewLineAtEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Slash ',
+ ],
+ [
+ 'type' => 'T_CLOSE_TAG',
+ 'content' => '?>
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashCommentNoNewLineAtEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Hash ',
+ ],
+ [
+ 'type' => 'T_CLOSE_TAG',
+ 'content' => '?>
+',
+ ],
+ ],
+ ],
+ [
+ '/* testCommentAtEndOfFile */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment',
+ ],
+ ],
+ ],
+ ];
+
+ }//end dataCommentTokenization()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc b/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc
new file mode 100644
index 0000000000..6ca7026d4b
--- /dev/null
+++ b/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc
@@ -0,0 +1,63 @@
+
+
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+use PHP_CodeSniffer\Util\Tokens;
+
+class StableCommentWhitespaceWinTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that comment tokenization with new lines at the end of the comment is stable.
+ *
+ * @param string $testMarker The comment prefacing the test.
+ * @param array $expectedTokens The tokenization expected.
+ *
+ * @dataProvider dataCommentTokenization
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testCommentTokenization($testMarker, $expectedTokens)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $comment = $this->getTargetToken($testMarker, Tokens::$commentTokens);
+
+ foreach ($expectedTokens as $key => $tokenInfo) {
+ $this->assertSame(constant($tokenInfo['type']), $tokens[$comment]['code']);
+ $this->assertSame($tokenInfo['type'], $tokens[$comment]['type']);
+ $this->assertSame($tokenInfo['content'], $tokens[$comment]['content']);
+
+ ++$comment;
+ }
+
+ }//end testCommentTokenization()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testCommentTokenization()
+ *
+ * @return array
+ */
+ public function dataCommentTokenization()
+ {
+ return [
+ [
+ '/* testSingleLineSlashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashCommentTrailing */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashAnnotation */',
+ [
+ [
+ 'type' => 'T_PHPCS_DISABLE',
+ 'content' => '// phpcs:disable Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationStart */',
+ [
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationMiddle */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// @phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineSlashCommentWithAnnotationEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Comment2
+',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// phpcs:ignore Stnd.Cat
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineSlashCommentNoNewLineAtEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '// Slash ',
+ ],
+ [
+ 'type' => 'T_CLOSE_TAG',
+ 'content' => '?>
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashCommentTrailing */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineHashComment */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment1
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment2
+',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiLineHashCommentWithIndent */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment1
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment2
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Comment3
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testSingleLineHashCommentNoNewLineAtEnd */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '# Hash ',
+ ],
+ [
+ 'type' => 'T_CLOSE_TAG',
+ 'content' => '?>
+',
+ ],
+ ],
+ ],
+ [
+ '/* testCommentAtEndOfFile */',
+ [
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* Comment',
+ ],
+ ],
+ ],
+ ];
+
+ }//end dataCommentTokenization()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc
new file mode 100644
index 0000000000..abf9b85ba8
--- /dev/null
+++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc
@@ -0,0 +1,120 @@
+ $param & $int;
+
+/* testTypeIntersectionArrowReturnType */
+$arrowWithReturnType = fn ($param) : Foo&Bar => $param * 10;
+
+/* testBitwiseAndInArrayKey */
+$array = array(
+ A & B => /* testBitwiseAndInArrayValue */ B & C
+);
+
+/* testBitwiseAndInShortArrayKey */
+$array = [
+ A & B => /* testBitwiseAndInShortArrayValue */ B & C
+];
+
+/* testBitwiseAndNonArrowFnFunctionCall */
+$obj->fn($something & $else);
+
+/* testBitwiseAnd6 */
+function &fn(/* testTypeIntersectionNonArrowFunctionDeclaration */ Foo&Bar $something) {}
+
+/* testTypeIntersectionWithInvalidTypes */
+function (int&string $var) {};
+
+/* testLiveCoding */
+// Intentional parse error. This has to be the last test in the file.
+return function( Foo&
diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php
new file mode 100644
index 0000000000..2170021933
--- /dev/null
+++ b/tests/Core/Tokenizer/TypeIntersectionTest.php
@@ -0,0 +1,138 @@
+
+ * @author Jaroslav Hanslík
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class TypeIntersectionTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that non-intersection type bitwise and tokens are still tokenized as bitwise and.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataBitwiseAnd
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testBitwiseAnd($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]);
+ $this->assertSame(T_BITWISE_AND, $tokens[$opener]['code']);
+ $this->assertSame('T_BITWISE_AND', $tokens[$opener]['type']);
+
+ }//end testBitwiseAnd()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testBitwiseAnd()
+ *
+ * @return array
+ */
+ public function dataBitwiseAnd()
+ {
+ return [
+ ['/* testBitwiseAnd1 */'],
+ ['/* testBitwiseAnd2 */'],
+ ['/* testBitwiseAndPropertyDefaultValue */'],
+ ['/* testBitwiseAndParamDefaultValue */'],
+ ['/* testBitwiseAnd3 */'],
+ ['/* testBitwiseAnd4 */'],
+ ['/* testBitwiseAnd5 */'],
+ ['/* testBitwiseAndClosureParamDefault */'],
+ ['/* testBitwiseAndArrowParamDefault */'],
+ ['/* testBitwiseAndArrowExpression */'],
+ ['/* testBitwiseAndInArrayKey */'],
+ ['/* testBitwiseAndInArrayValue */'],
+ ['/* testBitwiseAndInShortArrayKey */'],
+ ['/* testBitwiseAndInShortArrayValue */'],
+ ['/* testBitwiseAndNonArrowFnFunctionCall */'],
+ ['/* testBitwiseAnd6 */'],
+ ['/* testLiveCoding */'],
+ ];
+
+ }//end dataBitwiseAnd()
+
+
+ /**
+ * Test that bitwise and tokens when used as part of a intersection type are tokenized as `T_TYPE_INTERSECTION`.
+ *
+ * @param string $testMarker The comment which prefaces the target token in the test file.
+ *
+ * @dataProvider dataTypeIntersection
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional
+ *
+ * @return void
+ */
+ public function testTypeIntersection($testMarker)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]);
+ $this->assertSame(T_TYPE_INTERSECTION, $tokens[$opener]['code']);
+ $this->assertSame('T_TYPE_INTERSECTION', $tokens[$opener]['type']);
+
+ }//end testTypeIntersection()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testTypeIntersection()
+ *
+ * @return array
+ */
+ public function dataTypeIntersection()
+ {
+ return [
+ ['/* testTypeIntersectionPropertySimple */'],
+ ['/* testTypeIntersectionPropertyReverseModifierOrder */'],
+ ['/* testTypeIntersectionPropertyMulti1 */'],
+ ['/* testTypeIntersectionPropertyMulti2 */'],
+ ['/* testTypeIntersectionPropertyMulti3 */'],
+ ['/* testTypeIntersectionPropertyNamespaceRelative */'],
+ ['/* testTypeIntersectionPropertyPartiallyQualified */'],
+ ['/* testTypeIntersectionPropertyFullyQualified */'],
+ ['/* testTypeIntersectionPropertyWithReadOnlyKeyword */'],
+ ['/* testTypeIntersectionParam1 */'],
+ ['/* testTypeIntersectionParam2 */'],
+ ['/* testTypeIntersectionParam3 */'],
+ ['/* testTypeIntersectionParamNamespaceRelative */'],
+ ['/* testTypeIntersectionParamPartiallyQualified */'],
+ ['/* testTypeIntersectionParamFullyQualified */'],
+ ['/* testTypeIntersectionReturnType */'],
+ ['/* testTypeIntersectionConstructorPropertyPromotion */'],
+ ['/* testTypeIntersectionAbstractMethodReturnType1 */'],
+ ['/* testTypeIntersectionAbstractMethodReturnType2 */'],
+ ['/* testTypeIntersectionReturnTypeNamespaceRelative */'],
+ ['/* testTypeIntersectionReturnPartiallyQualified */'],
+ ['/* testTypeIntersectionReturnFullyQualified */'],
+ ['/* testTypeIntersectionClosureParamIllegalNullable */'],
+ ['/* testTypeIntersectionWithReference */'],
+ ['/* testTypeIntersectionWithSpreadOperator */'],
+ ['/* testTypeIntersectionClosureReturn */'],
+ ['/* testTypeIntersectionArrowParam */'],
+ ['/* testTypeIntersectionArrowReturnType */'],
+ ['/* testTypeIntersectionNonArrowFunctionDeclaration */'],
+ ['/* testTypeIntersectionWithInvalidTypes */'],
+ ];
+
+ }//end dataTypeIntersection()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc
new file mode 100644
index 0000000000..540f72c9dc
--- /dev/null
+++ b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc
@@ -0,0 +1,147 @@
+
+ * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Tests\Core\Tokenizer;
+
+use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest;
+
+class UndoNamespacedNameSingleTokenTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that identifier names are tokenized the same across PHP versions, based on the PHP 5/7 tokenization.
+ *
+ * @param string $testMarker The comment prefacing the test.
+ * @param array $expectedTokens The tokenization expected.
+ *
+ * @dataProvider dataIdentifierTokenization
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testIdentifierTokenization($testMarker, $expectedTokens)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+ $identifier = $this->getTargetToken($testMarker, constant($expectedTokens[0]['type']));
+
+ foreach ($expectedTokens as $key => $tokenInfo) {
+ $this->assertSame(constant($tokenInfo['type']), $tokens[$identifier]['code']);
+ $this->assertSame($tokenInfo['type'], $tokens[$identifier]['type']);
+ $this->assertSame($tokenInfo['content'], $tokens[$identifier]['content']);
+
+ ++$identifier;
+ }
+
+ }//end testIdentifierTokenization()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testIdentifierTokenization()
+ *
+ * @return array
+ */
+ public function dataIdentifierTokenization()
+ {
+ return [
+ [
+ '/* testNamespaceDeclaration */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Package',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testNamespaceDeclarationWithLevels */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'SubLevel',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Domain',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testUseStatement */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testUseStatementWithLevels */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Domain',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionUseStatement */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionUseStatementWithLevels */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_in_ns',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testConstantUseStatement */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'const',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'CONSTANT_NAME',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testConstantUseStatementWithLevels */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'const',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'OTHER_CONSTANT',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiUseUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'UnqualifiedClassName',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ ],
+ ],
+ [
+ '/* testMultiUsePartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Sublevel',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'PartiallyClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testGroupUseStatement */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_OPEN_USE_GROUP',
+ 'content' => '{',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'AnotherDomain',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_grouped',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'const',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'CONSTANT_GROUPED',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Sub',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'YetAnotherDomain',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'SubLevelA',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_grouped_too',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'const',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'SubLevelB',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'CONSTANT_GROUPED_TOO',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_CLOSE_USE_GROUP',
+ 'content' => '}',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testClassName */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'MyClass',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testExtendedFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'FQN',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testImplementsRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ ],
+ ],
+ [
+ '/* testImplementsFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Fully',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Qualified',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ ],
+ ],
+ [
+ '/* testImplementsUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Unqualified',
+ ],
+ [
+ 'type' => 'T_COMMA',
+ 'content' => ',',
+ ],
+ ],
+ ],
+ [
+ '/* testImplementsPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Sub',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionName */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testTypeDeclarationRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_TYPE_UNION',
+ 'content' => '|',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'object',
+ ],
+ ],
+ ],
+ [
+ '/* testTypeDeclarationFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Fully',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Qualified',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testTypeDeclarationUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Unqualified',
+ ],
+ [
+ 'type' => 'T_TYPE_UNION',
+ 'content' => '|',
+ ],
+ [
+ 'type' => 'T_FALSE',
+ 'content' => 'false',
+ ],
+ ],
+ ],
+ [
+ '/* testTypeDeclarationPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_NULLABLE',
+ 'content' => '?',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Sublevel',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testReturnTypeFQN */',
+ [
+ [
+ 'type' => 'T_NULLABLE',
+ 'content' => '?',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionCallRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'NameSpace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionCallFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Package',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionCallUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testFunctionPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testCatchRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'SubLevel',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Exception',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testCatchFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Exception',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testCatchUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Exception',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testCatchPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Exception',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testNewRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testNewFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Vendor',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testNewUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testNewPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testDoubleColonRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_DOUBLE_COLON',
+ 'content' => '::',
+ ],
+ ],
+ ],
+ [
+ '/* testDoubleColonFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_DOUBLE_COLON',
+ 'content' => '::',
+ ],
+ ],
+ ],
+ [
+ '/* testDoubleColonUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_DOUBLE_COLON',
+ 'content' => '::',
+ ],
+ ],
+ ],
+ [
+ '/* testDoubleColonPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Level',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_DOUBLE_COLON',
+ 'content' => '::',
+ ],
+ ],
+ ],
+ [
+ '/* testInstanceOfRelative */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testInstanceOfFQN */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Full',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_CLOSE_PARENTHESIS',
+ 'content' => ')',
+ ],
+ ],
+ ],
+ [
+ '/* testInstanceOfUnqualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ ],
+ ],
+ [
+ '/* testInstanceOfPartiallyQualified */',
+ [
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Partially',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'ClassName',
+ ],
+ [
+ 'type' => 'T_SEMICOLON',
+ 'content' => ';',
+ ],
+ ],
+ ],
+ [
+ '/* testInvalidInPHP8Whitespace */',
+ [
+ [
+ 'type' => 'T_NAMESPACE',
+ 'content' => 'namespace',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Sublevel',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'function_name',
+ ],
+ [
+ 'type' => 'T_OPEN_PARENTHESIS',
+ 'content' => '(',
+ ],
+ ],
+ ],
+ [
+ '/* testInvalidInPHP8Comments */',
+ [
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Fully',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_PHPCS_IGNORE',
+ 'content' => '// phpcs:ignore Stnd.Cat.Sniff -- for reasons
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Qualified',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_COMMENT',
+ 'content' => '/* comment */',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => ' ',
+ ],
+ [
+ 'type' => 'T_NS_SEPARATOR',
+ 'content' => '\\',
+ ],
+ [
+ 'type' => 'T_STRING',
+ 'content' => 'Name',
+ ],
+ [
+ 'type' => 'T_WHITESPACE',
+ 'content' => '
+',
+ ],
+ ],
+ ],
+ ];
+
+ }//end dataIdentifierTokenization()
+
+
+}//end class
diff --git a/tests/Standards/AbstractSniffUnitTest.php b/tests/Standards/AbstractSniffUnitTest.php
index dc7c14cbe8..c050a0c2af 100644
--- a/tests/Standards/AbstractSniffUnitTest.php
+++ b/tests/Standards/AbstractSniffUnitTest.php
@@ -320,6 +320,17 @@ public function generateFailureMessages(LocalFile $file)
$warningsTemp = [];
foreach ($warnings as $warning) {
$warningsTemp[] = $warning['message'].' ('.$warning['source'].')';
+
+ $source = $warning['source'];
+ if (in_array($source, $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'], true) === false) {
+ $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'][] = $source;
+ }
+
+ if ($warning['fixable'] === true
+ && in_array($source, $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'], true) === false
+ ) {
+ $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'][] = $source;
+ }
}
$allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp);
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 560253c6a7..47084d1135 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -44,6 +44,13 @@ class_alias('PHPUnit_TextUI_TestRunner', 'PHPUnit'.'\TextUI\TestRunner');
class_alias('PHPUnit_Framework_TestResult', 'PHPUnit'.'\Framework\TestResult');
}
+// Determine whether this is a PEAR install or not.
+$GLOBALS['PHP_CODESNIFFER_PEAR'] = false;
+
+if (is_file(__DIR__.'/../autoload.php') === false) {
+ $GLOBALS['PHP_CODESNIFFER_PEAR'] = true;
+}
+
/**
* A global util function to help print unit test fixing data.