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