|\?->)~'; /** @var bool|null */ public $enable = null; /** * @return array */ public function register(): array { return [ T_IS_IDENTICAL, T_IS_NOT_IDENTICAL, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $identicalPointer */ public function process(File $phpcsFile, $identicalPointer): int { $this->enable = SniffSettingsHelper::isEnabledByPhpVersion($this->enable, 80000); if (!$this->enable) { return $identicalPointer + 1; } $tokens = $phpcsFile->getTokens(); [$pointerBeforeIdentical, $pointerAfterIdentical] = $this->getIdenticalData($phpcsFile, $identicalPointer); if ($tokens[$pointerBeforeIdentical]['code'] !== T_NULL && $tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return $identicalPointer + 1; } [$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer] = $this->getConditionData( $phpcsFile, $pointerBeforeIdentical, $pointerAfterIdentical ); if ($identificatorStartPointer === null || $identificatorEndPointer === null) { return $identicalPointer + 1; } $isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL; $identificator = IdentificatorHelper::getContent($phpcsFile, $identificatorStartPointer, $identificatorEndPointer); $pointerAfterCondition = TokenHelper::findNextEffective( $phpcsFile, ($isYoda ? $identificatorEndPointer : $pointerAfterIdentical) + 1 ); $allowedBooleanCondition = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL ? T_BOOLEAN_AND : T_BOOLEAN_OR; if ($tokens[$pointerAfterCondition]['code'] === $allowedBooleanCondition) { return $this->checkNextCondition($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition); } if ($tokens[$pointerAfterCondition]['code'] === T_INLINE_THEN) { $this->checkTernaryOperator($phpcsFile, $identicalPointer, $conditionStartPointer, $identificator, $pointerAfterCondition); return $pointerAfterCondition + 1; } return $identicalPointer + 1; } private function checkTernaryOperator( File $phpcsFile, int $identicalPointer, int $conditionStartPointer, string $identificator, int $inlineThenPointer ): void { $tokens = $phpcsFile->getTokens(); $ternaryOperatorStartPointer = TernaryOperatorHelper::getStartPointer($phpcsFile, $inlineThenPointer); $searchStartPointer = $ternaryOperatorStartPointer; do { $booleanOperatorPointer = TokenHelper::findNext($phpcsFile, Tokens::$booleanOperators, $searchStartPointer, $inlineThenPointer); if ($booleanOperatorPointer === null) { break; } $identicalPointer = TokenHelper::findNext( $phpcsFile, [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], $searchStartPointer, $booleanOperatorPointer ); if ($identicalPointer === null) { return; } $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1); if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return; } $searchStartPointer = $booleanOperatorPointer + 1; } while (true); $pointerBeforeCondition = TokenHelper::findPreviousEffective($phpcsFile, $conditionStartPointer - 1); if (in_array($tokens[$pointerBeforeCondition]['code'], [T_BOOLEAN_AND, T_BOOLEAN_OR], true)) { $previousIdenticalPointer = TokenHelper::findPreviousLocal( $phpcsFile, [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], $pointerBeforeCondition ); if ($previousIdenticalPointer !== null) { [$pointerBeforePreviousIdentical, $pointerAfterPreviousIdentical] = $this->getIdenticalData( $phpcsFile, $previousIdenticalPointer ); [$previousIdentificatorStartPointer, $previousIdentificatorEndPointer] = $this->getConditionData( $phpcsFile, $pointerBeforePreviousIdentical, $pointerAfterPreviousIdentical ); if ($previousIdentificatorStartPointer !== null && $previousIdentificatorEndPointer !== null) { $previousIdentificator = IdentificatorHelper::getContent( $phpcsFile, $previousIdentificatorStartPointer, $previousIdentificatorEndPointer ); if (!self::areIdentificatorsCompatible($previousIdentificator, $identificator)) { return; } } } } $defaultInElse = $tokens[$identicalPointer]['code'] === T_IS_NOT_IDENTICAL; $inlineElsePointer = TernaryOperatorHelper::getElsePointer($phpcsFile, $inlineThenPointer); $inlineElseEndPointer = TernaryOperatorHelper::getEndPointer($phpcsFile, $inlineThenPointer, $inlineElsePointer); if ($defaultInElse) { $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineThenPointer); if ($nextIdentificatorPointers === null) { return; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1) !== $inlineElsePointer) { return; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer ); $firstPointerInElse = TokenHelper::findNextEffective($phpcsFile, $inlineElsePointer + 1); $defaultContent = TokenHelper::getContent($phpcsFile, $firstPointerInElse, $inlineElseEndPointer); $conditionEndPointer = $inlineElseEndPointer; } else { $nullPointer = TokenHelper::findNextEffective($phpcsFile, $inlineThenPointer + 1); if ($tokens[$nullPointer]['code'] !== T_NULL) { return; } if (TokenHelper::findNextEffective($phpcsFile, $nullPointer + 1) !== $inlineElsePointer) { return; } $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $inlineElsePointer); if ($nextIdentificatorPointers === null) { return; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; if ($nextIdentificatorEndPointer !== $inlineElseEndPointer) { return; } $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer ); $defaultContent = trim(TokenHelper::getContent($phpcsFile, $inlineThenPointer + 1, $inlineElsePointer - 1)); $conditionEndPointer = $nextIdentificatorEndPointer; } $fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR); if (!$fix) { return; } $conditionContent = sprintf('%s?%s', $identificator, $identificatorDifference); if (strtolower($defaultContent) !== 'null') { $conditionContent .= sprintf(' ?? %s', $defaultContent); } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $conditionStartPointer, $conditionEndPointer, $conditionContent); $phpcsFile->fixer->endChangeset(); } private function checkNextCondition( File $phpcsFile, int $identicalPointer, int $conditionStartPointer, string $identificator, int $nextConditionBooleanPointer ): int { $nextIdentificatorPointers = $this->getNextIdentificator($phpcsFile, $nextConditionBooleanPointer); if ($nextIdentificatorPointers === null) { return $nextConditionBooleanPointer; } [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer] = $nextIdentificatorPointers; $nextIdentificator = IdentificatorHelper::getContent($phpcsFile, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer); if (!$this->areIdentificatorsCompatible($identificator, $nextIdentificator)) { return $nextIdentificatorEndPointer; } $pointerAfterNexIdentificator = TokenHelper::findNextEffective($phpcsFile, $nextIdentificatorEndPointer + 1); $tokens = $phpcsFile->getTokens(); if ( $tokens[$pointerAfterNexIdentificator]['code'] !== $tokens[$identicalPointer]['code'] && !in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_INLINE_THEN, T_SEMICOLON], true) ) { return $pointerAfterNexIdentificator; } if (!in_array($tokens[$pointerAfterNexIdentificator]['code'], [T_IS_IDENTICAL, T_IS_NOT_IDENTICAL], true)) { return $pointerAfterNexIdentificator; } $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $pointerAfterNexIdentificator + 1); if ($tokens[$pointerAfterIdentical]['code'] !== T_NULL) { return $pointerAfterNexIdentificator; } $identificatorDifference = $this->getIdentificatorDifference( $phpcsFile, $identificator, $nextIdentificatorStartPointer, $nextIdentificatorEndPointer ); $fix = $phpcsFile->addFixableError('Operator ?-> is required.', $identicalPointer, self::CODE_REQUIRED_NULL_SAFE_OBJECT_OPERATOR); if (!$fix) { return $pointerAfterNexIdentificator; } $isConditionOfTernaryOperator = TernaryOperatorHelper::isConditionOfTernaryOperator($phpcsFile, $identicalPointer); $phpcsFile->fixer->beginChangeset(); FixerHelper::change( $phpcsFile, $conditionStartPointer, $nextIdentificatorEndPointer, sprintf('%s?%s', $identificator, $identificatorDifference) ); $phpcsFile->fixer->endChangeset(); if ($isConditionOfTernaryOperator) { return TokenHelper::findNext($phpcsFile, T_INLINE_THEN, $identicalPointer + 1); } return $pointerAfterNexIdentificator; } /** * @return array|null */ private function getNextIdentificator(File $phpcsFile, int $pointerBefore): ?array { /** @var int $nextIdentificatorStartPointer */ $nextIdentificatorStartPointer = TokenHelper::findNextEffective($phpcsFile, $pointerBefore + 1); $nextIdentificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $nextIdentificatorStartPointer); if ($nextIdentificatorEndPointer === null) { return null; } return [$nextIdentificatorStartPointer, $nextIdentificatorEndPointer]; } private function findIdentificatorStart(File $phpcsFile, int $identificatorEndPointer): ?int { $tokens = $phpcsFile->getTokens(); if ($tokens[$identificatorEndPointer]['code'] === T_CLOSE_PARENTHESIS) { $pointerBeforeParenthesisOpener = TokenHelper::findPreviousEffective( $phpcsFile, $tokens[$identificatorEndPointer]['parenthesis_opener'] - 1 ); $identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $pointerBeforeParenthesisOpener); } else { $identificatorStartPointer = IdentificatorHelper::findStartPointer($phpcsFile, $identificatorEndPointer); } if ($identificatorStartPointer !== null) { $pointerBeforeIdentificatorStart = TokenHelper::findPreviousEffective($phpcsFile, $identificatorStartPointer - 1); if (in_array( $tokens[$pointerBeforeIdentificatorStart]['code'], [T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true )) { $pointerBeforeOperator = TokenHelper::findPreviousEffective($phpcsFile, $pointerBeforeIdentificatorStart - 1); return $this->findIdentificatorStart($phpcsFile, $pointerBeforeOperator); } } return $identificatorStartPointer; } private function findIdentificatorEnd(File $phpcsFile, int $identificatorStartPointer): ?int { $tokens = $phpcsFile->getTokens(); $identificatorEndPointer = $tokens[$identificatorStartPointer]['code'] === T_STRING ? $identificatorStartPointer : IdentificatorHelper::findEndPointer($phpcsFile, $identificatorStartPointer); if ($identificatorEndPointer !== null) { $pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1); if ($tokens[$pointerAfterIdentificatorEnd]['code'] === T_OPEN_PARENTHESIS) { $identificatorEndPointer = $tokens[$pointerAfterIdentificatorEnd]['parenthesis_closer']; $pointerAfterIdentificatorEnd = TokenHelper::findNextEffective($phpcsFile, $identificatorEndPointer + 1); } if (in_array( $tokens[$pointerAfterIdentificatorEnd]['code'], [T_DOUBLE_COLON, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true )) { $pointerAfterOperator = TokenHelper::findNextEffective($phpcsFile, $pointerAfterIdentificatorEnd + 1); return $this->findIdentificatorEnd($phpcsFile, $pointerAfterOperator); } } return $identificatorEndPointer; } private function areIdentificatorsCompatible(string $first, string $second): bool { /** @var list $firstParts */ $firstParts = preg_split(self::OPERATOR_REGEXP, $first, -1, PREG_SPLIT_DELIM_CAPTURE); /** @var list $secondParts */ $secondParts = preg_split(self::OPERATOR_REGEXP, $second, -1, PREG_SPLIT_DELIM_CAPTURE); $minPartsCount = min(count($firstParts), count($secondParts)); for ($i = 0; $i < $minPartsCount; $i++) { if ($firstParts[$i] === '?->' && $secondParts[$i] === '->') { continue; } if ($firstParts[$i] !== $secondParts[$i]) { return false; } } return array_key_exists($minPartsCount, $secondParts) && $secondParts[$minPartsCount] === '->'; } private function getIdentificatorDifference( File $phpcsFile, string $identificator, int $nextIdentificatorStartPointer, int $nextIdentificatorEndPointer ): string { $objectOperatorsCountInIdentificator = substr_count($identificator, '->'); $tokens = $phpcsFile->getTokens(); $objectOperatorsCountInNextIdentificator = 0; $differencePointer = $nextIdentificatorStartPointer; for ($i = $nextIdentificatorStartPointer; $i <= $nextIdentificatorEndPointer; $i++) { if (in_array($tokens[$i]['code'], [T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR], true)) { $objectOperatorsCountInNextIdentificator++; } if ($objectOperatorsCountInNextIdentificator > $objectOperatorsCountInIdentificator) { $differencePointer = $i; break; } } return TokenHelper::getContent($phpcsFile, $differencePointer, $nextIdentificatorEndPointer); } /** * @return array{0: int, 1: int} */ private function getIdenticalData(File $phpcsFile, int $identicalPointer): array { /** @var int $pointerBeforeIdentical */ $pointerBeforeIdentical = TokenHelper::findPreviousEffective($phpcsFile, $identicalPointer - 1); /** @var int $pointerAfterIdentical */ $pointerAfterIdentical = TokenHelper::findNextEffective($phpcsFile, $identicalPointer + 1); return [$pointerBeforeIdentical, $pointerAfterIdentical]; } /** * @return array{0: int|null, 1: int|null, 2: int|null} */ private function getConditionData(File $phpcsFile, int $pointerBeforeIdentical, int $pointerAfterIdentical): array { $tokens = $phpcsFile->getTokens(); $isYoda = $tokens[$pointerBeforeIdentical]['code'] === T_NULL; if ($isYoda) { $identificatorStartPointer = $pointerAfterIdentical; $identificatorEndPointer = $this->findIdentificatorEnd($phpcsFile, $identificatorStartPointer); $conditionStartPointer = $pointerBeforeIdentical; } else { $identificatorEndPointer = $pointerBeforeIdentical; $identificatorStartPointer = $this->findIdentificatorStart($phpcsFile, $identificatorEndPointer); $conditionStartPointer = $identificatorStartPointer; } return [$identificatorStartPointer, $identificatorEndPointer, $conditionStartPointer]; } }