-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
264 additions
and
52 deletions.
There are no files selected for viewing
142 changes: 142 additions & 0 deletions
142
src/Cdn77/Sniffs/NamingConventions/ValidConstantNameSniff.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Cdn77\Sniffs\NamingConventions; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
use PHP_CodeSniffer\Util\Tokens; | ||
|
||
use function preg_match; | ||
use function sprintf; | ||
use function strpos; | ||
use function strrpos; | ||
use function strtolower; | ||
use function substr; | ||
|
||
use const T_CONST; | ||
use const T_CONSTANT_ENCAPSED_STRING; | ||
use const T_DOUBLE_COLON; | ||
use const T_NULLSAFE_OBJECT_OPERATOR; | ||
use const T_OBJECT_OPERATOR; | ||
use const T_STRING; | ||
use const T_WHITESPACE; | ||
|
||
class ValidConstantNameSniff implements Sniff | ||
{ | ||
public const CodeConstantNotMatchPattern = 'ConstantNotUpperCase'; | ||
public const CodeClassConstantNotMatchPattern = 'ClassConstantNotUpperCase'; | ||
private const PatternPascalCase = '\b([A-Z][a-zA-Z0-9]*?([A-Z][a-zA-Z0-9]*?)*?)\b'; | ||
|
||
public string $pattern = self::PatternPascalCase; | ||
|
||
/** | ||
* Returns an array of tokens this test wants to listen for. | ||
* | ||
* @return list<int> | ||
*/ | ||
public function register(): array | ||
{ | ||
return [ | ||
T_STRING, | ||
T_CONST, | ||
]; | ||
} | ||
|
||
/** | ||
* 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 | ||
*/ | ||
public function process(File $phpcsFile, $stackPtr): void | ||
{ | ||
$tokens = $phpcsFile->getTokens(); | ||
|
||
if ($tokens[$stackPtr]['code'] === T_CONST) { | ||
// This is a class constant. | ||
$constant = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); | ||
if ($constant === false) { | ||
return; | ||
} | ||
|
||
$constName = $tokens[$constant]['content']; | ||
|
||
if ($this->matchesRegex($constName, $this->pattern)) { | ||
return; | ||
} | ||
|
||
$error = sprintf('Constant "%%s" does not match pattern "%s"', $this->pattern); | ||
$data = [$constName]; | ||
$phpcsFile->addError( | ||
$error, | ||
$constant, | ||
self::CodeClassConstantNotMatchPattern, | ||
$data, | ||
); | ||
} | ||
|
||
// Only interested in define statements now. | ||
if (strtolower($tokens[$stackPtr]['content']) !== 'define') { | ||
return; | ||
} | ||
|
||
// Make sure this is not a method call. | ||
$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; | ||
} | ||
|
||
// If the next non-whitespace token after this token | ||
// is not an opening parenthesis then it is not a function call. | ||
$openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); | ||
if ($openBracket === false) { | ||
return; | ||
} | ||
|
||
// The next non-whitespace token must be the constant name. | ||
$constPtr = $phpcsFile->findNext(T_WHITESPACE, $openBracket + 1, null, true); | ||
if ($tokens[$constPtr]['code'] !== T_CONSTANT_ENCAPSED_STRING) { | ||
return; | ||
} | ||
|
||
$constName = $tokens[$constPtr]['content']; | ||
|
||
// Check for constants like self::CONSTANT. | ||
$prefix = ''; | ||
$splitPos = strpos($constName, '::'); | ||
if ($splitPos !== false) { | ||
$prefix = substr($constName, 0, $splitPos + 2); | ||
$constName = substr($constName, $splitPos + 2); | ||
} | ||
|
||
// Strip namespace from constant like /foo/bar/CONSTANT. | ||
$splitPos = strrpos($constName, '\\'); | ||
if ($splitPos !== false) { | ||
$prefix = substr($constName, 0, $splitPos + 1); | ||
$constName = substr($constName, $splitPos + 1); | ||
} | ||
|
||
if ($this->matchesRegex($constName, $this->pattern)) { | ||
return; | ||
} | ||
|
||
$error = sprintf('Constant "%%s" does not match pattern "%s"', $this->pattern); | ||
$data = [ | ||
$prefix . $constName, | ||
]; | ||
$phpcsFile->addError($error, $stackPtr, self::CodeConstantNotMatchPattern, $data); | ||
} | ||
|
||
private function matchesRegex(string $varName, string $pattern): bool | ||
{ | ||
return preg_match(sprintf('~%s~', $pattern), $varName) === 1; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
tests/Sniffs/NamingConventions/ValidConstantNameSniffTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Cdn77\Sniffs\NamingConventions; | ||
|
||
use Cdn77\TestCase; | ||
|
||
use function array_keys; | ||
use function json_encode; | ||
|
||
use const JSON_THROW_ON_ERROR; | ||
|
||
class ValidConstantNameSniffTest extends TestCase | ||
{ | ||
public function testErrors(): void | ||
{ | ||
$file = self::checkFile(__DIR__ . '/data/ValidConstantNameTest.inc'); | ||
|
||
$errorTypesPerLine = [ | ||
7 => ValidConstantNameSniff::CodeConstantNotMatchPattern, | ||
8 => ValidConstantNameSniff::CodeConstantNotMatchPattern, | ||
10 => ValidConstantNameSniff::CodeConstantNotMatchPattern, | ||
11 => ValidConstantNameSniff::CodeConstantNotMatchPattern, | ||
17 => ValidConstantNameSniff::CodeClassConstantNotMatchPattern, | ||
18 => ValidConstantNameSniff::CodeClassConstantNotMatchPattern, | ||
19 => ValidConstantNameSniff::CodeClassConstantNotMatchPattern, | ||
]; | ||
$possibleLines = array_keys($errorTypesPerLine); | ||
|
||
$errors = $file->getErrors(); | ||
foreach ($errors as $line => $error) { | ||
self::assertContains($line, $possibleLines, json_encode($error, JSON_THROW_ON_ERROR)); | ||
|
||
$errorType = $errorTypesPerLine[$line]; | ||
|
||
self::assertSniffError($file, $line, $errorType); | ||
} | ||
|
||
self::assertSame(6, $file->getErrorCount()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
tests/Sniffs/NamingConventions/data/ValidConstantNameTest.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
use Exception as My_Exception, foo\bar, baz; | ||
namespace foo; | ||
namespace foo\bar; | ||
namespace bar\foo\baz; | ||
|
||
define('SCREAMING_SNAKE_CASE', true); | ||
define('camelCase', true); | ||
define('PascalCase', true); | ||
define('bar\foo\baz\SCREAMING_SNAKE_CASE_WITH_NAMESPACE', true); | ||
define('bar\foo\baz\camelCaseWithNamespace', true); | ||
define("bar\foo\baz\PascalCaseWithNamespace", true); | ||
|
||
class TestClass extends MyClass implements MyInterface, YourInterface | ||
{ | ||
|
||
const SCREAMING_SNAKE_CASE = 'hello'; | ||
const camelCase = 'hello'; | ||
const PascalCase = 'hello'; | ||
} | ||
|
||
$foo->define('bar'); | ||
$foo->getBar()->define('foo'); | ||
Foo::define('bar'); |