diff --git a/composer.json b/composer.json index cfb0d4b..9cd90eb 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "doctrine/coding-standard": "^9.0" }, "require-dev": { + "ext-json": "*", "phpstan/phpstan": "^0.12.51", "phpstan/phpstan-phpunit": "^0.12.16", "phpstan/phpstan-strict-rules": "^0.12.5", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..125e5e3 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,12 @@ +parameters: + ignoreErrors: + - + message: "#^Constant T_OPEN_PARENTHESIS not found\\.$#" + count: 1 + path: src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php + + - + message: "#^Used constant T_OPEN_PARENTHESIS not found\\.$#" + count: 1 + path: src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e1690b3..b9d05b4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,5 +9,6 @@ parameters: - %currentWorkingDirectory% includes: + - phpstan-baseline.neon - vendor/phpstan/phpstan-phpunit/rules.neon - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php new file mode 100644 index 0000000..917b5f6 --- /dev/null +++ b/src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -0,0 +1,172 @@ +getTokens(); + $varName = ltrim($tokens[$stackPtr]['content'], '$'); + + // If it's a php reserved var, then its ok. + if (isset($this->phpReservedVars[$varName]) === true) { + return; + } + + $objOperator = $phpcsFile->findNext([T_WHITESPACE], $stackPtr + 1, null, true); + assert($objOperator !== false); + + 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); + assert($var !== false); + + if ($tokens[$var]['code'] === T_STRING) { + $bracket = $phpcsFile->findNext([T_WHITESPACE], $var + 1, null, true); + if ($tokens[$bracket]['code'] !== T_OPEN_PARENTHESIS) { + $objVarName = $tokens[$var]['content']; + + if (! $this->matchesRegex($objVarName, $this->memberPattern)) { + $error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern); + $data = [$objVarName]; + $phpcsFile->addError($error, $var, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $data); + } + } + } + } + + $objOperator = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) { + if (! $this->matchesRegex($varName, $this->memberPattern)) { + $error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern); + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $stackPtr, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $data); + } + + return; + } + + if ($this->matchesRegex($varName, $this->pattern)) { + return; + } + + $error = sprintf('Variable "%%s" does not match pattern "%s"', $this->pattern); + $data = [$varName]; + $phpcsFile->addError($error, $stackPtr, self::CODE_DOES_NOT_MATCH_PATTERN, $data); + } + + /** + * Processes class member variables. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + protected function processMemberVar(File $phpcsFile, $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + $varName = ltrim($tokens[$stackPtr]['content'], '$'); + $memberProps = $phpcsFile->getMemberProperties($stackPtr); + if ($memberProps === []) { + // Couldn't get any info about this variable, which + // generally means it is invalid or possibly has a parse + // error. Any errors will be reported by the core, so + // we can ignore it. + return; + } + + $errorData = [$varName]; + + if ($this->matchesRegex($varName, $this->memberPattern)) { + return; + } + + $error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern); + $phpcsFile->addError($error, $stackPtr, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $errorData); + } + + /** + * Processes the variable found within a double quoted string. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the double quoted string. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + protected function processVariableInString(File $phpcsFile, $stackPtr): void + { + $tokens = $phpcsFile->getTokens(); + + if ( + preg_match_all( + '|[^\\\]\${?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)|', + $tokens[$stackPtr]['content'], + $matches + ) === 0 + ) { + return; + } + + foreach ($matches[1] as $varName) { + // If it's a php reserved var, then its ok. + if (isset($this->phpReservedVars[$varName]) === true) { + continue; + } + + if ($this->matchesRegex($varName, $this->stringPattern)) { + continue; + } + + $error = sprintf('Variable "%%s" does not match pattern "%s"', $this->stringPattern); + $data = [$varName]; + $phpcsFile->addError($error, $stackPtr, self::CODE_STRING_DOES_NOT_MATCH_PATTERN, $data); + } + } + + private function matchesRegex(string $varName, string $pattern): bool + { + return preg_match(sprintf('~%s~', $pattern), $varName) === 1; + } +} diff --git a/src/Cdn77/ruleset.xml b/src/Cdn77/ruleset.xml index 8539736..8633f36 100644 --- a/src/Cdn77/ruleset.xml +++ b/src/Cdn77/ruleset.xml @@ -24,7 +24,8 @@ - + + @@ -35,6 +36,8 @@ + + diff --git a/tests/Sniffs/NamingConventions/ValidVariableNameSniffTest.php b/tests/Sniffs/NamingConventions/ValidVariableNameSniffTest.php new file mode 100644 index 0000000..440e92c --- /dev/null +++ b/tests/Sniffs/NamingConventions/ValidVariableNameSniffTest.php @@ -0,0 +1,82 @@ + ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 5 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 10 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 12 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 15 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 17 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 19 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 20 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 21 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 26 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 28 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 31 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN, + 32 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN, + 34 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN, + 37 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 39 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 48 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 50 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 53 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 55 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 57 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 58 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 59 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 62 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 76 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN, + 100 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 101 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 102 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 117 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 118 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 128 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 132 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 134 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 135 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + 140 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 142 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, + 144 => [ + ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + ], + 146 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN, + ]; + $possibleLines = array_keys($errorTypesPerLine); + + $errors = $file->getErrors(); + foreach ($errors as $line => $error) { + self::assertContains($line, $possibleLines, json_encode($error, JSON_THROW_ON_ERROR)); + + $errorTypes = $errorTypesPerLine[$line]; + if (! is_array($errorTypes)) { + $errorTypes = [$errorTypes]; + } + + foreach ($errorTypes as $errorType) { + self::assertSniffError($file, $line, $errorType); + } + } + + self::assertSame(41, $file->getErrorCount()); + } +} diff --git a/tests/Sniffs/NamingConventions/data/ValidVariableNameSniffTest.inc b/tests/Sniffs/NamingConventions/data/ValidVariableNameSniffTest.inc new file mode 100644 index 0000000..53c46ba --- /dev/null +++ b/tests/Sniffs/NamingConventions/data/ValidVariableNameSniffTest.inc @@ -0,0 +1,148 @@ +varName2; +echo $this->var_name2; +echo $this->varname2; +echo $this->_varName2; +echo $object->varName2; +echo $object->var_name2; +echo $object_name->varname2; +echo $object_name->_varName2; + +echo $this->myFunction($one, $two); +echo $object->myFunction($one_two); + +$error = "format is \$GLOBALS['$varName']"; + +echo $_SESSION['var_name']; +echo $_FILES['var_name']; +echo $_ENV['var_name']; +echo $_COOKIE['var_name']; + +$XML = 'hello'; +$myXML = 'hello'; +$XMLParser = 'hello'; +$xmlParser = 'hello'; + +echo "{$_SERVER['HOSTNAME']} $var_name"; + +$obj->$classVar = $prefix.'-'.$type; + +class foo +{ + const bar = <<varName; +echo $obj?->var_name; +echo $obj?->varname; +echo $obj?->_varName; + +$var_name . $var_name; + +fn($_, $__, $_x) => true; + +$var = Potato::new();