Skip to content

Sniff improvements #123

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 5 commits into from
Apr 3, 2020
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
composer.lock
composer.phar
coverage/
vendor/
80 changes: 80 additions & 0 deletions SymfonyCustom/Helpers/SniffHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,70 @@ class SniffHelper extends AbstractHelper
'@var',
];

/**
* <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
* <array> is array of <simple>, eg `int[]` or `\Foo[]`
* <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` or more complex`
* <object> is array key => value type, like `array{type: string, name: string, value: mixed}`
* <type> is <simple>, <array>, <object>, <generic> type
* <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
*/
public const REGEX_TYPES = '
(?<types>
(?<type>
(?<array>
(?&notArray)(?:
\s*\[\s*\]
)+
)
|
(?<notArray>
(?<multiple>
\(\s*(?<mutipleContent>
(?&types)
)\s*\)
)
|
(?<generic>
(?<genericName>
(?&simple)
)
\s*<\s*
(?<genericContent>
(?:(?&types)\s*,\s*)*
(?&types)
)
\s*>
)
|
(?<object>
array\s*{\s*
(?<objectContent>
(?:
(?<objectKeyValue>
(?:\w+\s*\??:\s*)?
(?&types)
)
\s*,\s*
)*
(?&objectKeyValue)
)
\s*}
)
|
(?<simple>
\\\\?\w+(?:\\\\\w+)*
|
\$this
)
)
)
(?:
\s*[\|&]\s*(?&type)
)*
)
';

/**
* @param File $phpcsFile
* @param int $stackPtr
Expand Down Expand Up @@ -122,4 +186,20 @@ public static function isGlobalUse(File $phpcsFile, int $stackPtr): bool

return true;
}

/**
* @param string $content
*
* @return array
*/
public static function parseTypeHint(string $content): array
{
preg_match(
'{^'.SniffHelper::REGEX_TYPES.'(?<space>[\s\t]*)?(?<description>.*)?$}six',
$content,
$matches
);

return [$matches['types'] ?? '', $matches['space'] ?? '', $matches['description'] ?? ''];
}
}
85 changes: 76 additions & 9 deletions SymfonyCustom/Sniffs/Classes/PropertyDeclarationSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class PropertyDeclarationSniff implements Sniff
*/
public function register(): array
{
return [T_CLASS];
return [T_CLASS, T_ANON_CLASS];
}

/**
Expand All @@ -27,24 +27,91 @@ public function register(): array
* @return void
*/
public function process(File $phpcsFile, $stackPtr): void
{
$this->processProperty($phpcsFile, $stackPtr);
$this->processFunction($phpcsFile, $stackPtr);
}

/**
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function processFunction(File $phpcsFile, int $stackPtr): void
{
$tokens = $phpcsFile->getTokens();
$end = $tokens[$stackPtr]['scope_closer'] ?? null;

$end = null;
if (isset($tokens[$stackPtr]['scope_closer'])) {
$end = $tokens[$stackPtr]['scope_closer'];
$function = $phpcsFile->findNext(T_FUNCTION, $stackPtr, $end);
if (false === $function) {
return;
}

$scope = $phpcsFile->findNext(T_FUNCTION, $stackPtr, $end);
$wantedTokens = [T_CONST, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_ANON_CLASS];
$scope = $phpcsFile->findNext($wantedTokens, $function + 1, $end);

while (false !== $scope) {
if (T_ANON_CLASS === $tokens[$scope]['code']) {
$scope = $tokens[$scope]['scope_closer'];

$wantedTokens = [T_PUBLIC, T_PROTECTED, T_PRIVATE];
continue;
}

if (T_CONST === $tokens[$scope]['code']) {
$phpcsFile->addError('Declare class constants before methods', $scope, 'ConstBeforeFunction');
} elseif (T_VARIABLE === $tokens[$scope + 2]['code']) {
$phpcsFile->addError('Declare class properties before methods', $scope, 'PropertyBeforeFunction');
}

while ($scope) {
$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
}
}

if ($scope && T_VARIABLE === $tokens[$scope + 2]['code']) {
$phpcsFile->addError('Declare class properties before methods', $scope, 'Invalid');
/**
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function processProperty(File $phpcsFile, int $stackPtr): void
{
$tokens = $phpcsFile->getTokens();
$end = $tokens[$stackPtr]['scope_closer'] ?? null;

$wantedTokens = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_ANON_CLASS];
$scope = $phpcsFile->findNext($wantedTokens, $stackPtr + 1, $end);

while (false !== $scope && T_VARIABLE !== $tokens[$scope + 2]['code']) {
if (T_ANON_CLASS === $tokens[$scope]['code']) {
$scope = $tokens[$scope]['scope_closer'];

continue;
}

$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
}

if (false === $scope) {
return;
}
$property = $scope + 2;

$wantedTokens = [T_CONST, T_ANON_CLASS];
$scope = $phpcsFile->findNext($wantedTokens, $property + 1, $end);

while (false !== $scope) {
if (T_ANON_CLASS === $tokens[$scope]['code']) {
$scope = $tokens[$scope]['scope_closer'];

continue;
}

if (T_CONST === $tokens[$scope]['code']) {
$phpcsFile->addError('Declare class property before const', $scope, 'ConstBeforeProperty');
}

$scope = $phpcsFile->findNext($wantedTokens, $scope + 1, $end);
}
}
}
16 changes: 16 additions & 0 deletions SymfonyCustom/Sniffs/Commenting/FunctionCommentSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff;
use PHP_CodeSniffer\Util\Tokens;
use SymfonyCustom\Helpers\SniffHelper;

/**
* SymfonyCustom standard customization to PEARs FunctionCommentSniff.
Expand Down Expand Up @@ -144,6 +145,21 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart, $has
$error = 'Return type missing for @return tag in function comment';
$phpcsFile->addError($error, $return, 'MissingReturnType');
}

[$type, $space, $description] = SniffHelper::parseTypeHint($content);
if (preg_match('/^\$\S+/', $description)) {
$error = '@return annotations should not contain variable name';
$fix = $phpcsFile->addFixableError($error, $return, 'NamedReturn');

if ($fix) {
$description = preg_replace('/^\$\S+/', '', $description);
if ('' !== $description) {
$phpcsFile->fixer->replaceToken($return + 2, $type.$space.$description);
} else {
$phpcsFile->fixer->replaceToken($return + 2, $type);
}
}
}
} else {
$error = 'Missing @return tag in function comment';
$phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
Expand Down
15 changes: 9 additions & 6 deletions SymfonyCustom/Sniffs/Commenting/VariableCommentSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
use SymfonyCustom\Helpers\SniffHelper;

/**
* Parses and verifies the variable doc comment.
Expand Down Expand Up @@ -97,19 +98,21 @@ public function processMemberVar(File $phpcsFile, $stackPtr): void
return;
}

$content = explode(' ', $tokens[$string]['content']);
$newContent = array_filter($content, function ($value) use ($tokens, $stackPtr) {
return 0 === preg_match('/^\$/', $value);
});
if (count($newContent) < count($content)) {
[$type, $space, $description] = SniffHelper::parseTypeHint($tokens[$string]['content']);
if (preg_match('/^\$\S+/', $description)) {
$fix = $phpcsFile->addFixableError(
'@var annotations should not contain variable name',
$foundVar,
'NamedVar'
);

if ($fix) {
$phpcsFile->fixer->replaceToken($string, implode(' ', $newContent));
$description = preg_replace('/^\$\S+/', '', $description);
if ('' !== $description) {
$phpcsFile->fixer->replaceToken($string, $type.$space.$description);
} else {
$phpcsFile->fixer->replaceToken($string, $type);
}
}
}
}
Expand Down
Loading