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();