Skip to content
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

added UnionTypeHintFormat #11

Merged
merged 1 commit into from May 23, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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