Skip to content

Allow to specify pattern in ValidVariableNameSniff #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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

1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ parameters:
- %currentWorkingDirectory%

includes:
- phpstan-baseline.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
172 changes: 172 additions & 0 deletions src/Cdn77/Sniffs/NamingConventions/ValidVariableNameSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\NamingConventions;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;

use function assert;
use function ltrim;
use function preg_match;
use function preg_match_all;
use function sprintf;

use const T_DOUBLE_COLON;
use const T_NULLSAFE_OBJECT_OPERATOR;
use const T_OBJECT_OPERATOR;
use const T_OPEN_PARENTHESIS;
use const T_STRING;
use const T_WHITESPACE;

class ValidVariableNameSniff extends AbstractVariableSniff
{
public const CODE_DOES_NOT_MATCH_PATTERN = 'DoesNotMatchPattern';
public const CODE_MEMBER_DOES_NOT_MATCH_PATTERN = 'MemberDoesNotMatchPattern';
public const CODE_STRING_DOES_NOT_MATCH_PATTERN = 'StringDoesNotMatchPattern';
private const PATTERN_CAMEL_CASE = '\b([a-zA-Z][a-zA-Z0-9]*?([A-Z][a-zA-Z0-9]*?)*?)\b';
private const PATTERN_CAMEL_CASE_OR_UNUSED = '\b(([a-zA-Z][a-zA-Z0-9]*?([A-Z][a-zA-Z0-9]*?)*?)|_+)\b';

public string $pattern = self::PATTERN_CAMEL_CASE_OR_UNUSED;
public string $memberPattern = self::PATTERN_CAMEL_CASE;
public string $stringPattern = self::PATTERN_CAMEL_CASE;

/**
* Processes this test, 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.
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*/
protected function processVariable(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->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;
}
}
5 changes: 4 additions & 1 deletion src/Cdn77/ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType"/>

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

<!-- replaced by SlevomatCodingStandard.Commenting.RequireOneLineDocComment -->
<exclude name="SlevomatCodingStandard.Commenting.RequireOneLinePropertyDocComment"/>
Expand All @@ -35,6 +36,8 @@
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification" />
</rule>

<rule ref="Cdn77.NamingConventions.ValidVariableName"/>

<rule ref="SlevomatCodingStandard.Arrays.MultiLineArrayEndBracketPlacement"/>
<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
<properties>
Expand Down
82 changes: 82 additions & 0 deletions tests/Sniffs/NamingConventions/ValidVariableNameSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Cdn77\Sniffs\NamingConventions;

use Cdn77\TestCase;

use function array_keys;
use function is_array;
use function json_encode;

use const JSON_THROW_ON_ERROR;

class ValidVariableNameSniffTest extends TestCase
{
public function testErrors(): void
{
$file = self::checkFile(__DIR__ . '/data/ValidVariableNameSniffTest.inc');

$errorTypesPerLine = [
3 => 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());
}
}
Loading