Skip to content

Commit 3558e06

Browse files
authored
Merge pull request #66 from cdn77/variable-name
Allow to specify pattern in ValidVariableNameSniff
2 parents 4257e30 + 81b865c commit 3558e06

File tree

7 files changed

+420
-1
lines changed

7 files changed

+420
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"doctrine/coding-standard": "^9.0"
1919
},
2020
"require-dev": {
21+
"ext-json": "*",
2122
"phpstan/phpstan": "^0.12.51",
2223
"phpstan/phpstan-phpunit": "^0.12.16",
2324
"phpstan/phpstan-strict-rules": "^0.12.5",

phpstan-baseline.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Constant T_OPEN_PARENTHESIS not found\\.$#"
5+
count: 1
6+
path: src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php
7+
8+
-
9+
message: "#^Used constant T_OPEN_PARENTHESIS not found\\.$#"
10+
count: 1
11+
path: src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php
12+

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ parameters:
88
- %currentWorkingDirectory%
99

1010
includes:
11+
- phpstan-baseline.neon
1112
- vendor/phpstan/phpstan-phpunit/rules.neon
1213
- vendor/phpstan/phpstan-strict-rules/rules.neon
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\Sniffs\NamingConventions;
6+
7+
use PHP_CodeSniffer\Files\File;
8+
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
9+
10+
use function assert;
11+
use function ltrim;
12+
use function preg_match;
13+
use function preg_match_all;
14+
use function sprintf;
15+
16+
use const T_DOUBLE_COLON;
17+
use const T_NULLSAFE_OBJECT_OPERATOR;
18+
use const T_OBJECT_OPERATOR;
19+
use const T_OPEN_PARENTHESIS;
20+
use const T_STRING;
21+
use const T_WHITESPACE;
22+
23+
class ValidVariableNameSniff extends AbstractVariableSniff
24+
{
25+
public const CODE_DOES_NOT_MATCH_PATTERN = 'DoesNotMatchPattern';
26+
public const CODE_MEMBER_DOES_NOT_MATCH_PATTERN = 'MemberDoesNotMatchPattern';
27+
public const CODE_STRING_DOES_NOT_MATCH_PATTERN = 'StringDoesNotMatchPattern';
28+
private const PATTERN_CAMEL_CASE = '\b([a-zA-Z][a-zA-Z0-9]*?([A-Z][a-zA-Z0-9]*?)*?)\b';
29+
private const PATTERN_CAMEL_CASE_OR_UNUSED = '\b(([a-zA-Z][a-zA-Z0-9]*?([A-Z][a-zA-Z0-9]*?)*?)|_+)\b';
30+
31+
public string $pattern = self::PATTERN_CAMEL_CASE_OR_UNUSED;
32+
public string $memberPattern = self::PATTERN_CAMEL_CASE;
33+
public string $stringPattern = self::PATTERN_CAMEL_CASE;
34+
35+
/**
36+
* Processes this test, when one of its tokens is encountered.
37+
*
38+
* @param File $phpcsFile The file being scanned.
39+
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
40+
*
41+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
42+
*/
43+
protected function processVariable(File $phpcsFile, $stackPtr): void
44+
{
45+
$tokens = $phpcsFile->getTokens();
46+
$varName = ltrim($tokens[$stackPtr]['content'], '$');
47+
48+
// If it's a php reserved var, then its ok.
49+
if (isset($this->phpReservedVars[$varName]) === true) {
50+
return;
51+
}
52+
53+
$objOperator = $phpcsFile->findNext([T_WHITESPACE], $stackPtr + 1, null, true);
54+
assert($objOperator !== false);
55+
56+
if (
57+
$tokens[$objOperator]['code'] === T_OBJECT_OPERATOR
58+
|| $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR
59+
) {
60+
// Check to see if we are using a variable from an object.
61+
$var = $phpcsFile->findNext([T_WHITESPACE], $objOperator + 1, null, true);
62+
assert($var !== false);
63+
64+
if ($tokens[$var]['code'] === T_STRING) {
65+
$bracket = $phpcsFile->findNext([T_WHITESPACE], $var + 1, null, true);
66+
if ($tokens[$bracket]['code'] !== T_OPEN_PARENTHESIS) {
67+
$objVarName = $tokens[$var]['content'];
68+
69+
if (! $this->matchesRegex($objVarName, $this->memberPattern)) {
70+
$error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern);
71+
$data = [$objVarName];
72+
$phpcsFile->addError($error, $var, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $data);
73+
}
74+
}
75+
}
76+
}
77+
78+
$objOperator = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true);
79+
if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) {
80+
if (! $this->matchesRegex($varName, $this->memberPattern)) {
81+
$error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern);
82+
$data = [$tokens[$stackPtr]['content']];
83+
$phpcsFile->addError($error, $stackPtr, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $data);
84+
}
85+
86+
return;
87+
}
88+
89+
if ($this->matchesRegex($varName, $this->pattern)) {
90+
return;
91+
}
92+
93+
$error = sprintf('Variable "%%s" does not match pattern "%s"', $this->pattern);
94+
$data = [$varName];
95+
$phpcsFile->addError($error, $stackPtr, self::CODE_DOES_NOT_MATCH_PATTERN, $data);
96+
}
97+
98+
/**
99+
* Processes class member variables.
100+
*
101+
* @param File $phpcsFile The file being scanned.
102+
* @param int $stackPtr The position of the current token in the stack passed in $tokens.
103+
*
104+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
105+
*/
106+
protected function processMemberVar(File $phpcsFile, $stackPtr): void
107+
{
108+
$tokens = $phpcsFile->getTokens();
109+
110+
$varName = ltrim($tokens[$stackPtr]['content'], '$');
111+
$memberProps = $phpcsFile->getMemberProperties($stackPtr);
112+
if ($memberProps === []) {
113+
// Couldn't get any info about this variable, which
114+
// generally means it is invalid or possibly has a parse
115+
// error. Any errors will be reported by the core, so
116+
// we can ignore it.
117+
return;
118+
}
119+
120+
$errorData = [$varName];
121+
122+
if ($this->matchesRegex($varName, $this->memberPattern)) {
123+
return;
124+
}
125+
126+
$error = sprintf('Member variable "%%s" does not match pattern "%s"', $this->memberPattern);
127+
$phpcsFile->addError($error, $stackPtr, self::CODE_MEMBER_DOES_NOT_MATCH_PATTERN, $errorData);
128+
}
129+
130+
/**
131+
* Processes the variable found within a double quoted string.
132+
*
133+
* @param File $phpcsFile The file being scanned.
134+
* @param int $stackPtr The position of the double quoted string.
135+
*
136+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
137+
*/
138+
protected function processVariableInString(File $phpcsFile, $stackPtr): void
139+
{
140+
$tokens = $phpcsFile->getTokens();
141+
142+
if (
143+
preg_match_all(
144+
'|[^\\\]\${?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)|',
145+
$tokens[$stackPtr]['content'],
146+
$matches
147+
) === 0
148+
) {
149+
return;
150+
}
151+
152+
foreach ($matches[1] as $varName) {
153+
// If it's a php reserved var, then its ok.
154+
if (isset($this->phpReservedVars[$varName]) === true) {
155+
continue;
156+
}
157+
158+
if ($this->matchesRegex($varName, $this->stringPattern)) {
159+
continue;
160+
}
161+
162+
$error = sprintf('Variable "%%s" does not match pattern "%s"', $this->stringPattern);
163+
$data = [$varName];
164+
$phpcsFile->addError($error, $stackPtr, self::CODE_STRING_DOES_NOT_MATCH_PATTERN, $data);
165+
}
166+
}
167+
168+
private function matchesRegex(string $varName, string $pattern): bool
169+
{
170+
return preg_match(sprintf('~%s~', $pattern), $varName) === 1;
171+
}
172+
}

src/Cdn77/ruleset.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType"/>
2525

2626
<!-- $_ variables are used to tell e.g. psalm that the variable is intentionally unused. -->
27-
<exclude name="Squiz.NamingConventions.ValidVariableName.NotCamelCaps" />
27+
<!-- replaced by Cdn77.NamingConventions.ValidVariableName -->
28+
<exclude name="Squiz.NamingConventions.ValidVariableName" />
2829

2930
<!-- replaced by SlevomatCodingStandard.Commenting.RequireOneLineDocComment -->
3031
<exclude name="SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment"/>
@@ -35,6 +36,8 @@
3536
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification" />
3637
</rule>
3738

39+
<rule ref="Cdn77.NamingConventions.ValidVariableName"/>
40+
3841
<rule ref="SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement"/>
3942
<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
4043
<properties>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\Sniffs\NamingConventions;
6+
7+
use Cdn77\TestCase;
8+
9+
use function array_keys;
10+
use function is_array;
11+
use function json_encode;
12+
13+
use const JSON_THROW_ON_ERROR;
14+
15+
class ValidVariableNameSniffTest extends TestCase
16+
{
17+
public function testErrors(): void
18+
{
19+
$file = self::checkFile(__DIR__ . '/data/ValidVariableNameSniffTest.inc');
20+
21+
$errorTypesPerLine = [
22+
3 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
23+
5 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
24+
10 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
25+
12 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
26+
15 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
27+
17 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
28+
19 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
29+
20 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
30+
21 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
31+
26 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
32+
28 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
33+
31 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN,
34+
32 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN,
35+
34 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN,
36+
37 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
37+
39 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
38+
48 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
39+
50 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
40+
53 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
41+
55 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
42+
57 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
43+
58 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
44+
59 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
45+
62 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
46+
76 => ValidVariableNameSniff::CODE_STRING_DOES_NOT_MATCH_PATTERN,
47+
100 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
48+
101 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
49+
102 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
50+
117 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
51+
118 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
52+
128 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
53+
132 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
54+
134 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
55+
135 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
56+
140 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
57+
142 => ValidVariableNameSniff::CODE_MEMBER_DOES_NOT_MATCH_PATTERN,
58+
144 => [
59+
ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
60+
ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
61+
],
62+
146 => ValidVariableNameSniff::CODE_DOES_NOT_MATCH_PATTERN,
63+
];
64+
$possibleLines = array_keys($errorTypesPerLine);
65+
66+
$errors = $file->getErrors();
67+
foreach ($errors as $line => $error) {
68+
self::assertContains($line, $possibleLines, json_encode($error, JSON_THROW_ON_ERROR));
69+
70+
$errorTypes = $errorTypesPerLine[$line];
71+
if (! is_array($errorTypes)) {
72+
$errorTypes = [$errorTypes];
73+
}
74+
75+
foreach ($errorTypes as $errorType) {
76+
self::assertSniffError($file, $line, $errorType);
77+
}
78+
}
79+
80+
self::assertSame(41, $file->getErrorCount());
81+
}
82+
}

0 commit comments

Comments
 (0)