Skip to content

Commit

Permalink
added UnionTypeHintFormat
Browse files Browse the repository at this point in the history
  • Loading branch information
vossik committed May 23, 2021
1 parent be51329 commit f75a11f
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 f75a11f

Please sign in to comment.