Skip to content

Commit

Permalink
Merge pull request #11 from vossik/update
Browse files Browse the repository at this point in the history
  • Loading branch information
peldax committed May 23, 2021
2 parents 109bcf1 + f75a11f commit 6958894
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 11 deletions.
@@ -0,0 +1,268 @@
<?php

declare(strict_types = 1);

namespace InfinityloopCodingStandard\Sniffs\TypeHints;

/**
* https://github.com/slevomat/coding-standard/blob/master/SlevomatCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php
*/
class UnionTypeHintFormatSniff implements \PHP_CodeSniffer\Sniffs\Sniff
{
public const CODE_DISALLOWED_WHITESPACE = 'DisallowedWhitespace';
public const CODE_REQUIRED_SHORT_NULLABLE = 'RequiredShortNullable';
public const CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION = 'NullTypeHintNotOnLastPosition';
public const UNION_SEPARATOR_NOT_ON_LAST_POSITION = 'Union type separator (|) should be placed at end of the line';
public const MULTILINE_UNION_WRONG_INDENTATION = 'Union types should be intended on same level as the one';

/**
* @return array<int, (int|string)>
*/
public function register() : array
{
return \array_merge(
[\T_VARIABLE],
\SlevomatCodingStandard\Helpers\TokenHelper::$functionTokenCodes,
);
}

/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $pointer
*/
//@phpcs:ignore Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
public function process(\PHP_CodeSniffer\Files\File $phpcsFile, $pointer) : void
{
if (!\SlevomatCodingStandard\Helpers\SniffSettingsHelper::isEnabledByPhpVersion(null, 80000)) {
return;
}

$tokens = $phpcsFile->getTokens();

if ($tokens[$pointer]['code'] === \T_VARIABLE) {
if (!\SlevomatCodingStandard\Helpers\PropertyHelper::isProperty($phpcsFile, $pointer)) {
return;
}

$propertyTypeHint = \SlevomatCodingStandard\Helpers\PropertyHelper::findTypeHint($phpcsFile, $pointer);

if ($propertyTypeHint !== null) {
$this->checkTypeHint($phpcsFile, $propertyTypeHint);
}

return;
}

$returnTypeHint = \SlevomatCodingStandard\Helpers\FunctionHelper::findReturnTypeHint($phpcsFile, $pointer);

if ($returnTypeHint !== null) {
$this->checkTypeHint($phpcsFile, $returnTypeHint);
}

foreach (\SlevomatCodingStandard\Helpers\FunctionHelper::getParametersTypeHints($phpcsFile, $pointer) as $parameterTypeHint) {
if ($parameterTypeHint !== null) {
$this->checkTypeHint($phpcsFile, $parameterTypeHint);
}
}
}

private function checkTypeHint(\PHP_CodeSniffer\Files\File $phpcsFile, \SlevomatCodingStandard\Helpers\TypeHint $typeHint) : void
{
$tokens = $phpcsFile->getTokens();

$typeHintsCount = \substr_count($typeHint->getTypeHint(), '|') + 1;

if ($typeHintsCount > 1) {
$firstUnionType = $tokens[$typeHint->getStartPointer()];
$isOneline = true;

foreach (
\SlevomatCodingStandard\Helpers\TokenHelper::findNextAll(
$phpcsFile,
[\T_TYPE_UNION],
$typeHint->getStartPointer(),
$typeHint->getEndPointer(),
) as $unionSeparator
) {
if ($tokens[$unionSeparator]['line'] !== $firstUnionType['line'] && $tokens[$unionSeparator + 1]['content'] !== $phpcsFile->eolChar) {
$phpcsFile->addError(
self::UNION_SEPARATOR_NOT_ON_LAST_POSITION,
$unionSeparator,
self::UNION_SEPARATOR_NOT_ON_LAST_POSITION,
);
}

$nextUnionType = \SlevomatCodingStandard\Helpers\TokenHelper::findNextEffective($phpcsFile, $unionSeparator + 1);

if ($tokens[$nextUnionType]['line'] === $firstUnionType['line']) {
continue;
}

$isOneline = false;

if ($tokens[$typeHint->getStartPointer()]['column'] === $tokens[$nextUnionType]['column']) {
continue;
}

$fix = $phpcsFile->addFixableError(
self::MULTILINE_UNION_WRONG_INDENTATION,
$nextUnionType,
self::MULTILINE_UNION_WRONG_INDENTATION,
);

if (!$fix) {
continue;
}

$difference = $tokens[$typeHint->getStartPointer()]['column'] - $tokens[$nextUnionType]['column'];

if ($difference === 0) {
continue;
}

$phpcsFile->fixer->beginChangeset();

if ($difference > 0) {
$phpcsFile->fixer->addContentBefore($nextUnionType, \str_repeat(' ', \abs($difference)));

$phpcsFile->fixer->endChangeset();

continue;
}

for ($i = 0; $i < \abs($difference); $i++) {
$token = \SlevomatCodingStandard\Helpers\TokenHelper::findPrevious(
$phpcsFile,
[\T_WHITESPACE],
$nextUnionType - $i,
);

if (\strlen($tokens[$token]['content']) > 1) { //Handle multiple spaces
$phpcsFile->fixer->replaceToken(
$token,
\str_repeat(' ', \abs(\strlen($tokens[$token]['content']) - \abs($difference))),
);

break;
}

$phpcsFile->fixer->replaceToken($token, '');
}

$phpcsFile->fixer->endChangeset();
}

if ($isOneline) {
$whitespacePointer = \SlevomatCodingStandard\Helpers\TokenHelper::findNext(
$phpcsFile,
\T_WHITESPACE,
$typeHint->getStartPointer() + 1,
$typeHint->getEndPointer(),
);

if ($whitespacePointer !== null) {
$originalTypeHint = \SlevomatCodingStandard\Helpers\TokenHelper::getContent(
$phpcsFile,
$typeHint->getStartPointer(),
$typeHint->getEndPointer(),
);

$fix = $phpcsFile->addFixableError(
\sprintf('Spaces in type hint "%s" are disallowed.', $originalTypeHint),
$typeHint->getStartPointer(),
self::CODE_DISALLOWED_WHITESPACE,
);

if ($fix) {
$this->fixTypeHint($phpcsFile, $typeHint, $typeHint->getTypeHint());
}
}
}
}

if (!$typeHint->isNullable()) {
return;
}

$hasShortNullable = \strpos($typeHint->getTypeHint(), '?') === 0;

if ($typeHintsCount === 2 && !$hasShortNullable) {
$fix = $phpcsFile->addFixableError(
\sprintf('Short nullable type hint in "%s" is required.', $typeHint->getTypeHint()),
$typeHint->getStartPointer(),
self::CODE_REQUIRED_SHORT_NULLABLE,
);

if ($fix) {
$typeHintWithoutNull = self::getTypeHintContentWithoutNull($phpcsFile, $typeHint);
$this->fixTypeHint($phpcsFile, $typeHint, '?' . $typeHintWithoutNull);
}
}

if ($hasShortNullable || ($typeHintsCount === 2) || \strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') {
return;
}

$fix = $phpcsFile->addFixableError(
\sprintf('Null type hint should be on last position in "%s".', $typeHint->getTypeHint()),
$typeHint->getStartPointer(),
self::CODE_NULL_TYPE_HINT_NOT_ON_LAST_POSITION,
);

if ($fix) {
$this->fixTypeHint($phpcsFile, $typeHint, self::getTypeHintContentWithoutNull($phpcsFile, $typeHint) . '|null');
}
}

private function getTypeHintContentWithoutNull(
\PHP_CodeSniffer\Files\File $phpcsFile,
\SlevomatCodingStandard\Helpers\TypeHint $typeHint,
) : string
{
$tokens = $phpcsFile->getTokens();

if (\strtolower($tokens[$typeHint->getEndPointer()]['content']) === 'null') {
$previousTypeHintPointer = \SlevomatCodingStandard\Helpers\TokenHelper::findPrevious(
$phpcsFile,
\SlevomatCodingStandard\Helpers\TokenHelper::getOnlyTypeHintTokenCodes(),
$typeHint->getEndPointer() - 1,
);

return \SlevomatCodingStandard\Helpers\TokenHelper::getContent($phpcsFile, $typeHint->getStartPointer(), $previousTypeHintPointer);
}

$content = '';

for ($i = $typeHint->getStartPointer(); $i <= $typeHint->getEndPointer(); $i++) {
if (\strtolower($tokens[$i]['content']) === 'null') {
$i = \SlevomatCodingStandard\Helpers\TokenHelper::findNext(
$phpcsFile,
\SlevomatCodingStandard\Helpers\TokenHelper::getOnlyTypeHintTokenCodes(),
$i + 1,
);
}

$content .= $tokens[$i]['content'];
}

return $content;
}

private function fixTypeHint(
\PHP_CodeSniffer\Files\File $phpcsFile,
\SlevomatCodingStandard\Helpers\TypeHint $typeHint,
string $fixedTypeHint,
) : void
{
$phpcsFile->fixer->beginChangeset();

$phpcsFile->fixer->replaceToken($typeHint->getStartPointer(), $fixedTypeHint);

for ($i = $typeHint->getStartPointer() + 1; $i <= $typeHint->getEndPointer(); $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}

$phpcsFile->fixer->endChangeset();
}
}
8 changes: 1 addition & 7 deletions InfinityloopCodingStandard/ruleset.xml
Expand Up @@ -459,13 +459,6 @@
<rule ref="SlevomatCodingStandard.TypeHints.NullTypeHintOnLastPosition"/>
<rule ref="SlevomatCodingStandard.TypeHints.DisallowArrayTypeHintSyntax"/>
<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHintSpacing"/>
<rule ref="SlevomatCodingStandard.TypeHints.UnionTypeHintFormat">
<properties>
<property name="withSpaces" value="no"/>
<property name="shortNullable" value="yes"/>
<property name="nullPosition" value="last"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Arrays.DisallowImplicitArrayCreation"/>
<rule ref="SlevomatCodingStandard.Arrays.TrailingArrayComma">
<properties>
Expand Down Expand Up @@ -508,4 +501,5 @@
<rule ref="InfinityloopCodingStandard.Namespaces.UseDoesNotStartWithBackslash"/>
<rule ref="InfinityloopCodingStandard.ControlStructures.RequireMultiLineNullCoalesce"/>
<rule ref="InfinityloopCodingStandard.ControlStructures.SwitchCommentSpacing"/>
<rule ref="InfinityloopCodingStandard.TypeHints.UnionTypeHintFormat"/>
</ruleset>
8 changes: 4 additions & 4 deletions README.md
Expand Up @@ -78,6 +78,10 @@ Enforces null coalesce operator to be reformatted to new line

Checks that there is a certain number of blank lines between code and comment

#### InfinityloopCodingStandard.TypeHints.UnionTypeHintFormat :wrench:

Improved version of Slevomat UnionTypeHintFormat with added formatting of multiline unions

### Slevomat sniffs

Detailed list of Slevomat sniffs with configured settings. Some sniffs are not included, either because we dont find them helpful, the are too strict, or collide with their counter-sniff (require/disallow pairs).
Expand Down Expand Up @@ -220,10 +224,6 @@ Excluded sniffs:
- SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue
- SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing
- SlevomatCodingStandard.TypeHints.PropertyTypeHintSpacing
- SlevomatCodingStandard.TypeHints.UnionTypeHintFormat
- withSpaces: no
- shortNullable: yes
- nullPosition: last
- SlevomatCodingStandard.Namespaces.DisallowGroupUse
- SlevomatCodingStandard.Namespaces.FullyQualifiedExceptions
- specialExceptionNames: false
Expand Down

0 comments on commit 6958894

Please sign in to comment.