From f75a11f8f828bb7a8172a187a3cd3c56ac436344 Mon Sep 17 00:00:00 2001 From: Vossik Date: Sun, 23 May 2021 23:50:45 +0200 Subject: [PATCH] added UnionTypeHintFormat --- .../TypeHints/UnionTypeHintFormatSniff.php | 268 ++++++++++++++++++ InfinityloopCodingStandard/ruleset.xml | 8 +- README.md | 8 +- 3 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 InfinityloopCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php diff --git a/InfinityloopCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php b/InfinityloopCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php new file mode 100644 index 0000000..3e28550 --- /dev/null +++ b/InfinityloopCodingStandard/Sniffs/TypeHints/UnionTypeHintFormatSniff.php @@ -0,0 +1,268 @@ + + */ + 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(); + } +} diff --git a/InfinityloopCodingStandard/ruleset.xml b/InfinityloopCodingStandard/ruleset.xml index 1025146..e618450 100644 --- a/InfinityloopCodingStandard/ruleset.xml +++ b/InfinityloopCodingStandard/ruleset.xml @@ -459,13 +459,6 @@ - - - - - - - @@ -508,4 +501,5 @@ + diff --git a/README.md b/README.md index f9a916f..3e1ce40 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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