├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── code_analysis.yaml ├── LICENSE ├── composer.json ├── config └── symplify.php ├── ecs.php ├── phpstan.neon ├── rector.php └── src ├── DocBlock └── UselessDocBlockCleaner.php ├── Enum └── BlockBorderType.php ├── Exception └── ShouldNotHappenException.php ├── Fixer ├── AbstractSymplifyFixer.php ├── Annotation │ ├── RemoveMethodNameDuplicateDescriptionFixer.php │ ├── RemovePHPStormAnnotationFixer.php │ └── RemovePropertyVariableNameDescriptionFixer.php ├── ArrayNotation │ ├── ArrayListItemNewlineFixer.php │ ├── ArrayOpenerAndCloserNewlineFixer.php │ └── StandaloneLineInMultilineArrayFixer.php ├── Commenting │ ├── ParamReturnAndVarTagMalformsFixer.php │ └── RemoveUselessDefaultCommentFixer.php ├── LineLength │ └── LineLengthFixer.php ├── Naming │ ├── ClassNameResolver.php │ ├── MethodNameResolver.php │ └── PropertyNameResolver.php ├── Spacing │ ├── MethodChainingNewlineFixer.php │ ├── SpaceAfterCommaHereNowDocFixer.php │ ├── StandaloneLineConstructorParamFixer.php │ └── StandaloneLinePromotedPropertyFixer.php └── Strict │ └── BlankLineAfterStrictTypesFixer.php ├── TokenAnalyzer ├── ChainMethodCallAnalyzer.php ├── DocblockRelatedParamNamesResolver.php ├── FunctionCallNameMatcher.php ├── HeredocAnalyzer.php ├── Naming │ └── MethodNameResolver.php ├── NewlineAnalyzer.php └── ParamNewliner.php ├── TokenRunner ├── Analyzer │ └── FixerAnalyzer │ │ ├── ArrayAnalyzer.php │ │ ├── BlockFinder.php │ │ ├── CallAnalyzer.php │ │ ├── IndentDetector.php │ │ └── TokenSkipper.php ├── Arrays │ └── ArrayItemNewliner.php ├── Contract │ └── DocBlock │ │ └── MalformWorkerInterface.php ├── DocBlock │ └── MalformWorker │ │ ├── InlineVarMalformWorker.php │ │ ├── InlineVariableDocBlockMalformWorker.php │ │ ├── MissingParamNameMalformWorker.php │ │ ├── MissingVarNameMalformWorker.php │ │ ├── ParamNameReferenceMalformWorker.php │ │ ├── ParamNameTypoMalformWorker.php │ │ ├── SuperfluousReturnNameMalformWorker.php │ │ ├── SuperfluousVarNameMalformWorker.php │ │ └── SwitchedTypeAndNameMalformWorker.php ├── Enum │ └── LineKind.php ├── Exception │ ├── MissingImplementationException.php │ └── TokenNotFoundException.php ├── TokenFinder.php ├── Transformer │ └── FixerTransformer │ │ ├── FirstLineLengthResolver.php │ │ ├── LineLengthCloserTransformer.php │ │ ├── LineLengthOpenerTransformer.php │ │ ├── LineLengthResolver.php │ │ ├── LineLengthTransformer.php │ │ ├── TokensInliner.php │ │ └── TokensNewliner.php ├── Traverser │ ├── ArrayBlockInfoFinder.php │ └── TokenReverser.php ├── ValueObject │ ├── BlockInfo.php │ ├── LineLengthAndPosition.php │ ├── TokenKinds.php │ └── Wrapper │ │ └── FixerWrapper │ │ └── ArrayWrapper.php ├── ValueObjectFactory │ └── LineLengthAndPositionFactory.php ├── Whitespace │ └── IndentResolver.php └── Wrapper │ └── FixerWrapper │ └── ArrayWrapperFactory.php └── ValueObject ├── BlockInfoMetadata.php └── CodingStandardConfig.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: tomasvotruba 3 | custom: https://www.paypal.me/rectorphp 4 | -------------------------------------------------------------------------------- /.github/workflows/code_analysis.yaml: -------------------------------------------------------------------------------- 1 | name: Code Analysis 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | # see https://github.com/composer/composer/issues/9368#issuecomment-718112361 11 | COMPOSER_ROOT_VERSION: "dev-main" 12 | 13 | jobs: 14 | code_analysis: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | actions: 19 | - 20 | name: 'PHPStan' 21 | run: composer phpstan --ansi 22 | 23 | - 24 | name: 'Composer Validate' 25 | run: composer validate --ansi 26 | 27 | - 28 | name: 'Rector' 29 | run: composer rector --ansi 30 | 31 | - 32 | name: 'Coding Standard' 33 | run: composer fix-cs --ansi 34 | 35 | - 36 | name: 'Tests' 37 | run: vendor/bin/phpunit 38 | 39 | - 40 | name: 'Check Active Classes' 41 | run: vendor/bin/class-leak check src --ansi 42 | 43 | name: ${{ matrix.actions.name }} 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | # see https://github.com/shivammathur/setup-php 50 | - uses: shivammathur/setup-php@v2 51 | with: 52 | php-version: 8.2 53 | coverage: none 54 | 55 | # composer install cache - https://github.com/ramsey/composer-install 56 | - uses: "ramsey/composer-install@v2" 57 | 58 | # Override code from symplify/coding-standard shipped with ECS 59 | - run: | 60 | rm -rf vendor/symplify/easy-coding-standard/vendor/symplify/coding-standard/src 61 | ln -s $PWD/src vendor/symplify/easy-coding-standard/vendor/symplify/coding-standard/ 62 | 63 | - run: ${{ matrix.actions.run }} 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) 2020 Tomas Votruba (https://tomasvotruba.com) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symplify/coding-standard", 3 | "description": "Set of Symplify rules for PHP_CodeSniffer and PHP CS Fixer.", 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=8.2", 7 | "nette/utils": "^4.0", 8 | "friendsofphp/php-cs-fixer": "^3.75.0" 9 | }, 10 | "require-dev": { 11 | "symplify/easy-coding-standard": "^12.5", 12 | "squizlabs/php_codesniffer": "^3.12", 13 | "phpunit/phpunit": "^11.5", 14 | "phpstan/extension-installer": "^1.4", 15 | "phpstan/phpstan": "^2.1", 16 | "rector/rector": "^2.0.13", 17 | "symplify/phpstan-extensions": "^12.0", 18 | "tomasvotruba/class-leak": "^2.0", 19 | "tracy/tracy": "^2.10" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Symplify\\CodingStandard\\": "src" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Symplify\\CodingStandard\\Tests\\": "tests" 29 | } 30 | }, 31 | "config": { 32 | "allow-plugins": { 33 | "phpstan/extension-installer": true 34 | } 35 | }, 36 | "scripts": { 37 | "check-cs": "vendor/bin/ecs check --ansi", 38 | "fix-cs": "vendor/bin/ecs check --fix --ansi", 39 | "phpstan": "vendor/bin/phpstan analyse --ansi", 40 | "rector": "vendor/bin/rector process --dry-run --ansi" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/symplify.php: -------------------------------------------------------------------------------- 1 | rules([ 22 | // docblocks and comments 23 | RemovePHPStormAnnotationFixer::class, 24 | ParamReturnAndVarTagMalformsFixer::class, 25 | RemoveUselessDefaultCommentFixer::class, 26 | RemoveMethodNameDuplicateDescriptionFixer::class, 27 | RemovePropertyVariableNameDescriptionFixer::class, 28 | 29 | // arrays 30 | ArrayListItemNewlineFixer::class, 31 | ArrayOpenerAndCloserNewlineFixer::class, 32 | StandaloneLinePromotedPropertyFixer::class, 33 | 34 | // newlines 35 | MethodChainingNewlineFixer::class, 36 | SpaceAfterCommaHereNowDocFixer::class, 37 | BlankLineAfterStrictTypesFixer::class, 38 | 39 | // line length 40 | LineLengthFixer::class, 41 | ]); 42 | 43 | $ecsConfig->ruleWithConfiguration(GeneralPhpdocAnnotationRemoveFixer::class, [ 44 | 'annotations' => ['throws', 'author', 'package', 'group', 'covers', 'category'], 45 | ]); 46 | }; 47 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/config', 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ]) 13 | ->withRootFiles() 14 | ->withPreparedSets(psr12: true, common: true); 15 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | 4 | errorFormat: symplify 5 | 6 | paths: 7 | - src 8 | - config 9 | - tests 10 | 11 | excludePaths: 12 | - '*/tests/**/Source/*' 13 | - '*/tests/**/Fixture/*' 14 | 15 | ignoreErrors: 16 | # partial enum 17 | - '#Method Symplify\\CodingStandard\\TokenRunner\\Analyzer\\FixerAnalyzer\\BlockFinder\:\:(getBlockTypeByContent|getBlockTypeByToken)\(\) never returns \d+ so it can be removed from the return type#' 18 | 19 | - 20 | path: tests/bootstrap.php 21 | message: '#Instantiated class PHP_CodeSniffer\\Util\\Tokens not found#' 22 | 23 | - '#Constant T_OPEN_CURLY_BRACKET|T_START_NOWDOC not found#' 24 | - '#Method Symplify\\CodingStandard\\TokenRunner\\Traverser\\ArrayBlockInfoFinder\:\:reverseTokens\(\) should return array but returns array#' 25 | 26 | # unused generics 27 | - '#Class (.*?) implements generic interface PhpCsFixer\\Fixer\\ConfigurableFixerInterface but does not specify its types\: TFixerInputConfig, TFixerComputedConfig#' 28 | 29 | # conditional check to allow various php versions 30 | - 31 | message: '#Comparison operation ">\=" between int<80200, 80499> and (.*?) is always true#' 32 | path: src/TokenAnalyzer/DocblockRelatedParamNamesResolver.php 33 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([__DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/tests']) 9 | ->withRootFiles() 10 | ->withPhpSets() 11 | ->withPreparedSets(codeQuality: true, codingStyle: true, naming: true, earlyReturn: true, privatization: true) 12 | ->withImportNames() 13 | ->withSkip([ 14 | '*/Source/*', 15 | '*/Fixture/*', 16 | ]); 17 | -------------------------------------------------------------------------------- /src/DocBlock/UselessDocBlockCleaner.php: -------------------------------------------------------------------------------- 1 | getContent(); 72 | 73 | $cleanedCommentLines = []; 74 | 75 | foreach (explode("\n", $docContent) as $key => $commentLine) { 76 | if ($this->isClassLikeName($commentLine, $classLikeName)) { 77 | continue; 78 | } 79 | 80 | foreach (self::CLEANING_REGEXES as $cleaningRegex) { 81 | $commentLine = Strings::replace($commentLine, $cleaningRegex); 82 | } 83 | 84 | $cleanedCommentLines[$key] = $commentLine; 85 | } 86 | 87 | // remove empty lines 88 | $cleanedCommentLines = array_filter($cleanedCommentLines); 89 | $cleanedCommentLines = array_values($cleanedCommentLines); 90 | 91 | // is totally empty? 92 | if ($this->isEmptyDocblock($cleanedCommentLines)) { 93 | return ''; 94 | } 95 | 96 | $commentText = implode("\n", $cleanedCommentLines); 97 | 98 | // run multilines regex on final result 99 | return Strings::replace($commentText, self::DOCTRINE_GENERATED_COMMENT_REGEX); 100 | } 101 | 102 | /** 103 | * @param string[] $commentLines 104 | */ 105 | private function isEmptyDocblock(array $commentLines): bool 106 | { 107 | if (count($commentLines) !== 2) { 108 | return false; 109 | } 110 | 111 | $startCommentLine = $commentLines[0]; 112 | $endCommentLine = $commentLines[1]; 113 | 114 | return $startCommentLine === '/**' && trim($endCommentLine) === '*/'; 115 | } 116 | 117 | private function isClassLikeName(string $commentLine, ?string $classLikeName): bool 118 | { 119 | if ($classLikeName === null) { 120 | return false; 121 | } 122 | 123 | return trim($commentLine, '* ') === $classLikeName; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Enum/BlockBorderType.php: -------------------------------------------------------------------------------- 1 | methodNameResolver = new MethodNameResolver(); 33 | } 34 | 35 | public function getDefinition(): FixerDefinitionInterface 36 | { 37 | return new FixerDefinition(self::ERROR_MESSAGE, []); 38 | } 39 | 40 | /** 41 | * @param Tokens $tokens 42 | */ 43 | public function isCandidate(Tokens $tokens): bool 44 | { 45 | if (! $tokens->isTokenKindFound(T_FUNCTION)) { 46 | return false; 47 | } 48 | 49 | return $tokens->isAnyTokenKindsFound([T_DOC_COMMENT, T_COMMENT]); 50 | } 51 | 52 | /** 53 | * @param Tokens $tokens 54 | */ 55 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 56 | { 57 | $reversedTokens = $this->tokenReverser->reverse($tokens); 58 | 59 | foreach ($reversedTokens as $index => $token) { 60 | if (! $token->isGivenKind([T_DOC_COMMENT, T_COMMENT])) { 61 | continue; 62 | } 63 | 64 | $methodName = $this->methodNameResolver->resolve($tokens, $index); 65 | if ($methodName === null) { 66 | continue; 67 | } 68 | 69 | // skip if not setter or getter 70 | $originalDocContent = $token->getContent(); 71 | 72 | $hasChanged = false; 73 | 74 | $docblockLines = explode("\n", $originalDocContent); 75 | foreach ($docblockLines as $key => $docblockLine) { 76 | $spacelessDocblockLine = Strings::replace($docblockLine, '#[\s\n]+#', ''); 77 | if (strtolower($spacelessDocblockLine) !== strtolower('*' . $methodName)) { 78 | continue; 79 | } 80 | 81 | $hasChanged = true; 82 | unset($docblockLines[$key]); 83 | } 84 | 85 | if (! $hasChanged) { 86 | continue; 87 | } 88 | 89 | $tokens[$index] = new Token([T_DOC_COMMENT, implode("\n", $docblockLines)]); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Fixer/Annotation/RemovePHPStormAnnotationFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 44 | */ 45 | public function isCandidate(Tokens $tokens): bool 46 | { 47 | return $tokens->isAnyTokenKindsFound([T_DOC_COMMENT, T_COMMENT]); 48 | } 49 | 50 | /** 51 | * @param Tokens $tokens 52 | */ 53 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 54 | { 55 | $reversedTokens = $this->tokenReverser->reverse($tokens); 56 | 57 | foreach ($reversedTokens as $index => $token) { 58 | if (! $token->isGivenKind([T_DOC_COMMENT, T_COMMENT])) { 59 | continue; 60 | } 61 | 62 | $originalDocContent = $token->getContent(); 63 | $cleanedDocContent = Strings::replace($originalDocContent, self::CREATED_BY_PHPSTORM_DOC_REGEX, ''); 64 | if ($cleanedDocContent !== '') { 65 | continue; 66 | } 67 | 68 | // remove token 69 | $tokens->clearAt($index); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Fixer/Annotation/RemovePropertyVariableNameDescriptionFixer.php: -------------------------------------------------------------------------------- 1 | propertyNameResolver = new PropertyNameResolver(); 39 | } 40 | 41 | public function getDefinition(): FixerDefinitionInterface 42 | { 43 | return new FixerDefinition(self::ERROR_MESSAGE, []); 44 | } 45 | 46 | /** 47 | * @param Tokens $tokens 48 | */ 49 | public function isCandidate(Tokens $tokens): bool 50 | { 51 | if (! $tokens->isTokenKindFound(T_VARIABLE)) { 52 | return false; 53 | } 54 | 55 | return $tokens->isAnyTokenKindsFound([T_DOC_COMMENT, T_COMMENT]); 56 | } 57 | 58 | /** 59 | * @param Tokens $tokens 60 | */ 61 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 62 | { 63 | $reversedTokens = $this->tokenReverser->reverse($tokens); 64 | 65 | foreach ($reversedTokens as $index => $token) { 66 | if (! $token->isGivenKind([T_DOC_COMMENT, T_COMMENT])) { 67 | continue; 68 | } 69 | 70 | $propertyName = $this->propertyNameResolver->resolve($tokens, $index); 71 | if ($propertyName === null) { 72 | continue; 73 | } 74 | 75 | // skip if not setter or getter 76 | $originalDocContent = $token->getContent(); 77 | 78 | preg_match_all(self::VAR_REGEX, $originalDocContent, $matches); 79 | 80 | if (count($matches[0]) !== 1) { 81 | continue; 82 | } 83 | 84 | $hasChanged = false; 85 | 86 | $docblockLines = explode("\n", $originalDocContent); 87 | foreach ($docblockLines as $key => $docblockLine) { 88 | if (! str_ends_with($docblockLine, ' ' . $propertyName)) { 89 | continue; 90 | } 91 | 92 | if (! preg_match(self::VAR_REGEX, $docblockLine)) { 93 | continue; 94 | } 95 | 96 | // remove last x characters 97 | $docblockLine = Strings::substring($docblockLine, 0, -strlen(' ' . $propertyName)); 98 | 99 | $hasChanged = true; 100 | $docblockLines[$key] = rtrim($docblockLine); 101 | } 102 | 103 | if (! $hasChanged) { 104 | continue; 105 | } 106 | 107 | $tokens[$index] = new Token([T_DOC_COMMENT, implode("\n", $docblockLines)]); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Fixer/ArrayNotation/ArrayListItemNewlineFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 47 | */ 48 | public function isCandidate(Tokens $tokens): bool 49 | { 50 | if (! $tokens->isAnyTokenKindsFound(TokenKinds::ARRAY_OPEN_TOKENS)) { 51 | return false; 52 | } 53 | 54 | return $tokens->isTokenKindFound(T_DOUBLE_ARROW); 55 | } 56 | 57 | /** 58 | * @param Tokens $tokens 59 | */ 60 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 61 | { 62 | $arrayBlockInfos = $this->arrayBlockInfoFinder->findArrayOpenerBlockInfos($tokens); 63 | foreach ($arrayBlockInfos as $arrayBlockInfo) { 64 | if (! $this->arrayAnalyzer->isIndexedList($tokens, $arrayBlockInfo)) { 65 | continue; 66 | } 67 | 68 | $this->arrayItemNewliner->fixArrayOpener($tokens, $arrayBlockInfo); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Fixer/ArrayNotation/ArrayOpenerAndCloserNewlineFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 54 | */ 55 | public function isCandidate(Tokens $tokens): bool 56 | { 57 | if (! $tokens->isAnyTokenKindsFound(TokenKinds::ARRAY_OPEN_TOKENS)) { 58 | return false; 59 | } 60 | 61 | return $tokens->isTokenKindFound(T_DOUBLE_ARROW); 62 | } 63 | 64 | /** 65 | * @param Tokens $tokens 66 | */ 67 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 68 | { 69 | $blockInfos = $this->arrayBlockInfoFinder->findArrayOpenerBlockInfos($tokens); 70 | 71 | $blockInfoMetadatas = []; 72 | 73 | foreach ($blockInfos as $blockInfo) { 74 | $blockInfoMetadatas[$blockInfo->getStart()] = new BlockInfoMetadata(BlockBorderType::OPENER, $blockInfo); 75 | $blockInfoMetadatas[$blockInfo->getEnd()] = new BlockInfoMetadata(BlockBorderType::CLOSER, $blockInfo); 76 | } 77 | 78 | // sort from the highest position to the lowest, so we respect the changed tokens from bottom to the top convention 79 | krsort($blockInfoMetadatas); 80 | 81 | foreach ($blockInfoMetadatas as $blockInfoMetadata) { 82 | $this->fixPositionAndType($blockInfoMetadata, $tokens); 83 | } 84 | } 85 | 86 | /** 87 | * @param Tokens $tokens 88 | */ 89 | private function fixPositionAndType(BlockInfoMetadata $blockInfoMetadata, Tokens $tokens): void 90 | { 91 | $blockInfo = $blockInfoMetadata->getBlockInfo(); 92 | if ($this->isNextTokenAlsoArrayOpener($tokens, $blockInfo->getStart())) { 93 | return; 94 | } 95 | 96 | // no items 97 | $itemCount = $this->arrayAnalyzer->getItemCount($tokens, $blockInfo); 98 | if ($itemCount === 0) { 99 | return; 100 | } 101 | 102 | if (! $this->arrayAnalyzer->isIndexedList($tokens, $blockInfo)) { 103 | return; 104 | } 105 | 106 | // closer must run before the opener, as tokens as added by traversing up 107 | if ($blockInfoMetadata->getBlockType() === BlockBorderType::CLOSER) { 108 | $this->handleArrayCloser($tokens, $blockInfo->getEnd()); 109 | } elseif ($blockInfoMetadata->getBlockType() === BlockBorderType::OPENER) { 110 | $this->handleArrayOpener($tokens, $blockInfo->getStart()); 111 | } 112 | } 113 | 114 | /** 115 | * @param Tokens $tokens 116 | */ 117 | private function isNextTokenAlsoArrayOpener(Tokens $tokens, int $index): bool 118 | { 119 | $nextToken = $this->getNextMeaningfulToken($tokens, $index); 120 | if (! $nextToken instanceof Token) { 121 | return false; 122 | } 123 | 124 | return $nextToken->isGivenKind(TokenKinds::ARRAY_OPEN_TOKENS); 125 | } 126 | 127 | /** 128 | * @param Tokens $tokens 129 | */ 130 | private function handleArrayCloser(Tokens $tokens, int $arrayCloserPosition): void 131 | { 132 | $preArrayCloserPosition = $arrayCloserPosition - 1; 133 | 134 | $previousCloserToken = $tokens[$preArrayCloserPosition] ?? null; 135 | if (! $previousCloserToken instanceof Token) { 136 | return; 137 | } 138 | 139 | // already whitespace 140 | if (\str_contains($previousCloserToken->getContent(), "\n")) { 141 | return; 142 | } 143 | 144 | $tokens->ensureWhitespaceAtIndex($preArrayCloserPosition, 1, $this->whitespacesFixerConfig->getLineEnding()); 145 | } 146 | 147 | /** 148 | * @param Tokens $tokens 149 | */ 150 | private function handleArrayOpener(Tokens $tokens, int $arrayOpenerPosition): void 151 | { 152 | $postArrayOpenerPosition = $arrayOpenerPosition + 1; 153 | 154 | $nextToken = $tokens[$postArrayOpenerPosition] ?? null; 155 | if (! $nextToken instanceof Token) { 156 | return; 157 | } 158 | 159 | // already is whitespace 160 | if (\str_contains($nextToken->getContent(), "\n")) { 161 | return; 162 | } 163 | 164 | $tokens->ensureWhitespaceAtIndex($postArrayOpenerPosition, 0, $this->whitespacesFixerConfig->getLineEnding()); 165 | } 166 | 167 | /** 168 | * @param Tokens $tokens 169 | */ 170 | private function getNextMeaningfulToken(Tokens $tokens, int $index): ?Token 171 | { 172 | $nextMeaningfulTokenPosition = $tokens->getNextMeaningfulToken($index); 173 | if ($nextMeaningfulTokenPosition === null) { 174 | return null; 175 | } 176 | 177 | return $tokens[$nextMeaningfulTokenPosition]; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Fixer/ArrayNotation/StandaloneLineInMultilineArrayFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 54 | */ 55 | public function isCandidate(Tokens $tokens): bool 56 | { 57 | if (! $tokens->isAnyTokenKindsFound(TokenKinds::ARRAY_OPEN_TOKENS)) { 58 | return false; 59 | } 60 | 61 | return $tokens->isTokenKindFound(T_DOUBLE_ARROW); 62 | } 63 | 64 | /** 65 | * @param Tokens $tokens 66 | */ 67 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 68 | { 69 | foreach ($tokens as $index => $token) { 70 | if (! $token->isGivenKind(TokenKinds::ARRAY_OPEN_TOKENS)) { 71 | continue; 72 | } 73 | 74 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $index); 75 | if (! $blockInfo instanceof BlockInfo) { 76 | continue; 77 | } 78 | 79 | if ($this->shouldSkipNestedArrayValue($tokens, $blockInfo)) { 80 | return; 81 | } 82 | 83 | $this->tokensNewliner->breakItems($blockInfo, $tokens, LineKind::ARRAYS); 84 | } 85 | } 86 | 87 | /** 88 | * @param Tokens $tokens 89 | */ 90 | private function shouldSkipNestedArrayValue(Tokens $tokens, BlockInfo $blockInfo): bool 91 | { 92 | $arrayWrapper = $this->arrayWrapperFactory->createFromTokensAndBlockInfo($tokens, $blockInfo); 93 | if (! $arrayWrapper->isAssociativeArray()) { 94 | return true; 95 | } 96 | 97 | if ($arrayWrapper->getItemCount() === 1 && ! $arrayWrapper->isFirstItemArray()) { 98 | $previousTokenPosition = $tokens->getPrevMeaningfulToken($blockInfo->getStart()); 99 | if ($previousTokenPosition === null) { 100 | return false; 101 | } 102 | 103 | /** @var Token $previousToken */ 104 | $previousToken = $tokens[$previousTokenPosition]; 105 | return ! $previousToken->isGivenKind(T_DOUBLE_ARROW); 106 | } 107 | 108 | return false; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Fixer/Commenting/ParamReturnAndVarTagMalformsFixer.php: -------------------------------------------------------------------------------- 1 | malformWorkers = [ 60 | $inlineVariableDocBlockMalformWorker, 61 | $inlineVarMalformWorker, 62 | $missingParamNameMalformWorker, 63 | $missingVarNameMalformWorker, 64 | $paramNameReferenceMalformWorker, 65 | $paramNameTypoMalformWorker, 66 | $superfluousReturnNameMalformWorker, 67 | $superfluousVarNameMalformWorker, 68 | $switchedTypeAndNameMalformWorker, 69 | ]; 70 | } 71 | 72 | public function getDefinition(): FixerDefinitionInterface 73 | { 74 | return new FixerDefinition(self::ERROR_MESSAGE, []); 75 | } 76 | 77 | /** 78 | * @param Tokens $tokens 79 | */ 80 | public function isCandidate(Tokens $tokens): bool 81 | { 82 | if (! $tokens->isAnyTokenKindsFound([T_DOC_COMMENT, T_COMMENT])) { 83 | return false; 84 | } 85 | 86 | $reversedTokens = $this->tokenReverser->reverse($tokens); 87 | 88 | foreach ($reversedTokens as $index => $token) { 89 | if (! $token->isGivenKind([T_CALLABLE])) { 90 | continue; 91 | } 92 | 93 | if (! (isset($tokens[$index + 3]) && $tokens[$index + 3]->getContent() === ')')) { 94 | continue; 95 | } 96 | 97 | return false; 98 | } 99 | 100 | return $tokens->isAnyTokenKindsFound([T_FUNCTION, T_VARIABLE]); 101 | } 102 | 103 | /** 104 | * @param Tokens $tokens 105 | */ 106 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 107 | { 108 | $reversedTokens = $this->tokenReverser->reverse($tokens); 109 | 110 | foreach ($reversedTokens as $index => $token) { 111 | if (! $token->isGivenKind([T_DOC_COMMENT, T_COMMENT])) { 112 | continue; 113 | } 114 | 115 | $docContent = $token->getContent(); 116 | if (! Strings::match($docContent, self::TYPE_ANNOTATION_REGEX)) { 117 | continue; 118 | } 119 | 120 | $originalDocContent = $docContent; 121 | foreach ($this->malformWorkers as $malformWorker) { 122 | $docContent = $malformWorker->work($docContent, $tokens, $index); 123 | } 124 | 125 | if ($docContent === $originalDocContent) { 126 | continue; 127 | } 128 | 129 | $tokens[$index] = new Token([T_DOC_COMMENT, $docContent]); 130 | } 131 | } 132 | 133 | /** 134 | * Must run before 135 | * 136 | * @see \PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer::getPriority() 137 | */ 138 | public function getPriority(): int 139 | { 140 | return -37; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Fixer/Commenting/RemoveUselessDefaultCommentFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 41 | */ 42 | public function isCandidate(Tokens $tokens): bool 43 | { 44 | return $tokens->isAnyTokenKindsFound([T_DOC_COMMENT, T_COMMENT]); 45 | } 46 | 47 | public function getPriority(): int 48 | { 49 | /** must run before @see \PhpCsFixer\Fixer\Basic\BracesFixer to cleanup spaces */ 50 | return 40; 51 | } 52 | 53 | /** 54 | * @param Tokens $tokens 55 | */ 56 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 57 | { 58 | $reversedTokens = $this->tokenReverser->reverse($tokens); 59 | 60 | foreach ($reversedTokens as $index => $token) { 61 | if (! $token->isGivenKind([T_DOC_COMMENT, T_COMMENT])) { 62 | continue; 63 | } 64 | 65 | $classLikeName = $this->classNameResolver->resolveClassName($fileInfo, $tokens); 66 | 67 | $originalContent = $token->getContent(); 68 | $cleanedDocContent = $this->uselessDocBlockCleaner->clearDocTokenContent($token, $classLikeName); 69 | 70 | if ($cleanedDocContent === '') { 71 | // remove token 72 | $tokens->clearTokenAndMergeSurroundingWhitespace($index); 73 | } elseif ($cleanedDocContent !== $originalContent) { 74 | // update in case of other contents 75 | $tokens[$index] = new Token([T_DOC_COMMENT, $cleanedDocContent]); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Fixer/LineLength/LineLengthFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 82 | */ 83 | public function isCandidate(Tokens $tokens): bool 84 | { 85 | return $tokens->isAnyTokenKindsFound([ 86 | // "[" 87 | T_ARRAY, 88 | // "array"() 89 | CT::T_ARRAY_SQUARE_BRACE_OPEN, 90 | '(', 91 | ')', 92 | // "function" 93 | T_FUNCTION, 94 | // "use" (...) 95 | CT::T_USE_LAMBDA, 96 | // "new" 97 | T_NEW, 98 | // "#[" 99 | T_ATTRIBUTE, 100 | ]); 101 | } 102 | 103 | /** 104 | * @param Tokens $tokens 105 | */ 106 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 107 | { 108 | // function arguments, function call parameters, lambda use() 109 | for ($position = count($tokens) - 1; $position >= 0; --$position) { 110 | /** @var Token $token */ 111 | $token = $tokens[$position]; 112 | 113 | if ($token->equals(')')) { 114 | $this->processMethodCall($tokens, $position); 115 | continue; 116 | } 117 | 118 | // opener 119 | if ($token->isGivenKind([T_ATTRIBUTE, T_FUNCTION, CT::T_USE_LAMBDA, T_NEW])) { 120 | $this->processFunctionOrArray($tokens, $position); 121 | continue; 122 | } 123 | 124 | // closer 125 | if (! $token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) { 126 | continue; 127 | } 128 | 129 | if (! $token->isArray()) { 130 | continue; 131 | } 132 | 133 | $this->processFunctionOrArray($tokens, $position); 134 | } 135 | } 136 | 137 | /** 138 | * Must run before 139 | * 140 | * @see \PhpCsFixer\Fixer\ArrayNotation\TrimArraySpacesFixer::getPriority() 141 | */ 142 | public function getPriority(): int 143 | { 144 | return 5; 145 | } 146 | 147 | /** 148 | * @param array $configuration 149 | */ 150 | public function configure(array $configuration): void 151 | { 152 | $this->lineLength = $configuration[self::LINE_LENGTH] ?? self::DEFAULT_LINE_LENGHT; 153 | $this->breakLongLines = $configuration[self::BREAK_LONG_LINES] ?? true; 154 | $this->inlineShortLines = $configuration[self::INLINE_SHORT_LINES] ?? true; 155 | } 156 | 157 | public function getConfigurationDefinition(): FixerConfigurationResolverInterface 158 | { 159 | throw new ShouldNotHappenException(); 160 | } 161 | 162 | /** 163 | * @param Tokens $tokens 164 | */ 165 | private function processMethodCall(Tokens $tokens, int $position): void 166 | { 167 | $methodNamePosition = $this->functionCallNameMatcher->matchName($tokens, $position); 168 | if ($methodNamePosition === null) { 169 | return; 170 | } 171 | 172 | $blockInfo = $this->blockFinder->findInTokensByPositionAndContent($tokens, $methodNamePosition, '('); 173 | if (! $blockInfo instanceof BlockInfo) { 174 | return; 175 | } 176 | 177 | // has comments => dangerous to change: https://github.com/symplify/symplify/issues/973 178 | $comments = $tokens->findGivenKind(T_COMMENT, $blockInfo->getStart(), $blockInfo->getEnd()); 179 | if ($comments !== []) { 180 | return; 181 | } 182 | 183 | $this->lineLengthTransformer->fixStartPositionToEndPosition( 184 | $blockInfo, 185 | $tokens, 186 | $this->lineLength, 187 | $this->breakLongLines, 188 | $this->inlineShortLines 189 | ); 190 | } 191 | 192 | /** 193 | * @param Tokens $tokens 194 | */ 195 | private function processFunctionOrArray(Tokens $tokens, int $position): void 196 | { 197 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 198 | if (! $blockInfo instanceof BlockInfo) { 199 | return; 200 | } 201 | 202 | // @todo is __construct() class method and is newline parma enabled? → skip it 203 | if ($this->standaloneLineConstructorParamFixer && $this->methodNameResolver->isMethodName( 204 | $tokens, 205 | $position, 206 | '__construct' 207 | )) { 208 | return; 209 | } 210 | 211 | if ($this->shouldSkip($tokens, $blockInfo)) { 212 | return; 213 | } 214 | 215 | $this->lineLengthTransformer->fixStartPositionToEndPosition( 216 | $blockInfo, 217 | $tokens, 218 | $this->lineLength, 219 | $this->breakLongLines, 220 | $this->inlineShortLines 221 | ); 222 | } 223 | 224 | /** 225 | * @param Tokens $tokens 226 | */ 227 | private function shouldSkip(Tokens $tokens, BlockInfo $blockInfo): bool 228 | { 229 | // no items inside => skip 230 | if ($blockInfo->getEnd() - $blockInfo->getStart() <= 1) { 231 | return true; 232 | } 233 | 234 | if ($this->heredocAnalyzer->isHerenowDoc($tokens, $blockInfo)) { 235 | return true; 236 | } 237 | 238 | // is array with indexed values "=>" 239 | $doubleArrowTokens = $tokens->findGivenKind(T_DOUBLE_ARROW, $blockInfo->getStart(), $blockInfo->getEnd()); 240 | if ($doubleArrowTokens !== []) { 241 | return true; 242 | } 243 | 244 | // has comments => dangerous to change: https://github.com/symplify/symplify/issues/973 245 | return (bool) $tokens->findGivenKind(T_COMMENT, $blockInfo->getStart(), $blockInfo->getEnd()); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Fixer/Naming/ClassNameResolver.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $classNameByFilePath = []; 15 | 16 | /** 17 | * @param Tokens $tokens 18 | */ 19 | public function resolveClassName(SplFileInfo $splFileInfo, Tokens $tokens): ?string 20 | { 21 | $filePath = $splFileInfo->getRealPath(); 22 | 23 | if (isset($this->classNameByFilePath[$filePath])) { 24 | return $this->classNameByFilePath[$filePath]; 25 | } 26 | 27 | $classLikeName = $this->resolveFromTokens($tokens); 28 | if (! is_string($classLikeName)) { 29 | return null; 30 | } 31 | 32 | $this->classNameByFilePath[$filePath] = $classLikeName; 33 | 34 | return $classLikeName; 35 | } 36 | 37 | /** 38 | * @param Tokens $tokens 39 | */ 40 | private function resolveFromTokens(Tokens $tokens): ?string 41 | { 42 | foreach ($tokens as $position => $token) { 43 | if (! $token->isGivenKind([T_CLASS, T_TRAIT, T_INTERFACE])) { 44 | continue; 45 | } 46 | 47 | $nextNextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($position + 1); 48 | $nextNextMeaningfulToken = $tokens[$nextNextMeaningfulTokenIndex]; 49 | 50 | // skip anonymous classes 51 | if (! $nextNextMeaningfulToken->isGivenKind(T_STRING)) { 52 | continue; 53 | } 54 | 55 | return $nextNextMeaningfulToken->getContent(); 56 | } 57 | 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Fixer/Naming/MethodNameResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function resolve(Tokens $tokens, int $currentPosition): ?string 16 | { 17 | foreach ($tokens as $position => $token) { 18 | if ($position <= $currentPosition) { 19 | continue; 20 | } 21 | 22 | if (! $token->isGivenKind([T_FUNCTION])) { 23 | continue; 24 | } 25 | 26 | $nextNextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($position + 1); 27 | $nextNextMeaningfulToken = $tokens[$nextNextMeaningfulTokenIndex]; 28 | 29 | // skip anonymous functions 30 | if (! $nextNextMeaningfulToken->isGivenKind(T_STRING)) { 31 | continue; 32 | } 33 | 34 | return $nextNextMeaningfulToken->getContent(); 35 | } 36 | 37 | return null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Fixer/Naming/PropertyNameResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function resolve(Tokens $tokens, int $currentPosition): ?string 16 | { 17 | foreach ($tokens as $position => $token) { 18 | if ($position <= $currentPosition) { 19 | continue; 20 | } 21 | 22 | if (! $token->isGivenKind([T_VARIABLE])) { 23 | continue; 24 | } 25 | 26 | return $token->getContent(); 27 | } 28 | 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Fixer/Spacing/MethodChainingNewlineFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 52 | */ 53 | public function isCandidate(Tokens $tokens): bool 54 | { 55 | return $tokens->isAnyTokenKindsFound([T_OBJECT_OPERATOR]); 56 | } 57 | 58 | /** 59 | * @param Tokens $tokens 60 | */ 61 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 62 | { 63 | // function arguments, function call parameters, lambda use() 64 | for ($index = 1, $count = count($tokens); $index < $count; ++$index) { 65 | $currentToken = $tokens[$index]; 66 | if (! $currentToken->isGivenKind(T_OBJECT_OPERATOR)) { 67 | continue; 68 | } 69 | 70 | if (! $this->shouldPrefixNewline($tokens, $index)) { 71 | continue; 72 | } 73 | 74 | $tokens->ensureWhitespaceAtIndex($index, 0, $this->whitespacesFixerConfig->getLineEnding()); 75 | ++$index; 76 | } 77 | } 78 | 79 | /** 80 | * @param Tokens $tokens 81 | */ 82 | private function shouldPrefixNewline(Tokens $tokens, int $objectOperatorIndex): bool 83 | { 84 | for ($i = $objectOperatorIndex; $i >= 0; --$i) { 85 | /** @var Token $currentToken */ 86 | $currentToken = $tokens[$i]; 87 | 88 | if ($currentToken->equals(')')) { 89 | return $this->shouldBracketPrefix($tokens, $i, $objectOperatorIndex); 90 | } 91 | 92 | if ($currentToken->isGivenKind([T_NEW, T_VARIABLE])) { 93 | return false; 94 | } 95 | 96 | if ($currentToken->getContent() === '(') { 97 | return false; 98 | } 99 | } 100 | 101 | return false; 102 | } 103 | 104 | /** 105 | * @param Tokens $tokens 106 | */ 107 | private function isDoubleBracket(Tokens $tokens, int $position): bool 108 | { 109 | /** @var int $nextTokenPosition */ 110 | $nextTokenPosition = $tokens->getNextNonWhitespace($position); 111 | 112 | /** @var Token $nextToken */ 113 | $nextToken = $tokens[$nextTokenPosition]; 114 | return $nextToken->getContent() === ')'; 115 | } 116 | 117 | /** 118 | * Matches e.g.: - app([ ])->some() 119 | * 120 | * @param Tokens $tokens 121 | */ 122 | private function isPrecededByOpenedCallInAnotherBracket(Tokens $tokens, int $position): bool 123 | { 124 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 125 | if (! $blockInfo instanceof BlockInfo) { 126 | return false; 127 | } 128 | 129 | return $tokens->isPartialCodeMultiline($blockInfo->getStart(), $blockInfo->getEnd()); 130 | } 131 | 132 | /** 133 | * @param Tokens $tokens 134 | */ 135 | private function shouldBracketPrefix(Tokens $tokens, int $position, int $objectOperatorIndex): bool 136 | { 137 | if ($this->isDoubleBracket($tokens, $position)) { 138 | return false; 139 | } 140 | 141 | if ($this->chainMethodCallAnalyzer->isPartOfMethodCallOrArray($tokens, $position)) { 142 | return false; 143 | } 144 | 145 | if ($this->chainMethodCallAnalyzer->isPrecededByFuncCall($tokens, $position)) { 146 | return false; 147 | } 148 | 149 | if ($this->isPrecededByOpenedCallInAnotherBracket($tokens, $position)) { 150 | return false; 151 | } 152 | 153 | // all good, there is a newline 154 | return ! $tokens->isPartialCodeMultiline($position, $objectOperatorIndex); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Fixer/Spacing/SpaceAfterCommaHereNowDocFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 32 | */ 33 | public function isCandidate(Tokens $tokens): bool 34 | { 35 | return $tokens->isAnyTokenKindsFound([T_START_HEREDOC, T_START_NOWDOC]); 36 | } 37 | 38 | /** 39 | * @param Tokens $tokens 40 | */ 41 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 42 | { 43 | // function arguments, function call parameters, lambda use() 44 | for ($position = count($tokens) - 1; $position >= 0; --$position) { 45 | /** @var Token $token */ 46 | $token = $tokens[$position]; 47 | 48 | if (! $token->isGivenKind(T_END_HEREDOC)) { 49 | continue; 50 | } 51 | 52 | // nothing 53 | if (! isset($tokens[$position + 1])) { 54 | continue; 55 | } 56 | 57 | /** @var Token $nextToken */ 58 | $nextToken = $tokens[$position + 1]; 59 | if (! in_array($nextToken->getContent(), [',', ']'], true)) { 60 | continue; 61 | } 62 | 63 | $tokens->ensureWhitespaceAtIndex($position + 1, 0, PHP_EOL); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Fixer/Spacing/StandaloneLineConstructorParamFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 49 | */ 50 | public function isCandidate(Tokens $tokens): bool 51 | { 52 | return $tokens->isTokenKindFound(T_FUNCTION); 53 | } 54 | 55 | /** 56 | * @param Tokens $tokens 57 | */ 58 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 59 | { 60 | // function arguments, function call parameters, lambda use() 61 | for ($position = count($tokens) - 1; $position >= 0; --$position) { 62 | /** @var Token $token */ 63 | $token = $tokens[$position]; 64 | 65 | if (! $token->isGivenKind(T_FUNCTION)) { 66 | continue; 67 | } 68 | 69 | if (! $this->methodNameResolver->isMethodName($tokens, $position, '__construct')) { 70 | continue; 71 | } 72 | 73 | $this->paramNewliner->processFunction($tokens, $position); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Fixer/Spacing/StandaloneLinePromotedPropertyFixer.php: -------------------------------------------------------------------------------- 1 | $tokens 50 | */ 51 | public function isCandidate(Tokens $tokens): bool 52 | { 53 | return $tokens->isAnyTokenKindsFound([ 54 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, 55 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, 56 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, 57 | ]); 58 | } 59 | 60 | /** 61 | * @param Tokens $tokens 62 | */ 63 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 64 | { 65 | // function arguments, function call parameters, lambda use() 66 | for ($position = count($tokens) - 1; $position >= 0; --$position) { 67 | /** @var Token $token */ 68 | $token = $tokens[$position]; 69 | 70 | if (! $token->isGivenKind([T_FUNCTION])) { 71 | continue; 72 | } 73 | 74 | if (! $this->methodNameResolver->isMethodName($tokens, $position, '__construct')) { 75 | continue; 76 | } 77 | 78 | $this->paramNewliner->processFunction($tokens, $position); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Fixer/Strict/BlankLineAfterStrictTypesFixer.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | private readonly array $declareStrictTypeTokens; 35 | 36 | public function __construct( 37 | private readonly WhitespacesFixerConfig $whitespacesFixerConfig 38 | ) { 39 | $this->declareStrictTypeTokens = [ 40 | new Token([T_DECLARE, 'declare']), 41 | new Token('('), 42 | new Token([T_STRING, 'strict_types']), 43 | new Token('='), 44 | new Token([T_LNUMBER, '1']), 45 | new Token(')'), 46 | new Token(';'), 47 | ]; 48 | } 49 | 50 | public function getDefinition(): FixerDefinitionInterface 51 | { 52 | return new FixerDefinition(self::ERROR_MESSAGE, []); 53 | } 54 | 55 | /** 56 | * @param Tokens $tokens 57 | */ 58 | public function isCandidate(Tokens $tokens): bool 59 | { 60 | return $tokens->isAllTokenKindsFound([T_OPEN_TAG, T_WHITESPACE, T_DECLARE, T_STRING, '=', T_LNUMBER, ';']); 61 | } 62 | 63 | /** 64 | * @param Tokens $tokens 65 | */ 66 | public function fix(SplFileInfo $fileInfo, Tokens $tokens): void 67 | { 68 | $sequenceLocation = $tokens->findSequence($this->declareStrictTypeTokens, 1, 15); 69 | if ($sequenceLocation === null) { 70 | return; 71 | } 72 | 73 | $semicolonPosition = array_key_last($sequenceLocation); 74 | 75 | // empty file 76 | if (! isset($tokens[$semicolonPosition + 2])) { 77 | return; 78 | } 79 | 80 | $lineEnding = $this->whitespacesFixerConfig->getLineEnding(); 81 | 82 | $tokens->ensureWhitespaceAtIndex($semicolonPosition + 1, 0, $lineEnding . $lineEnding); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/ChainMethodCallAnalyzer.php: -------------------------------------------------------------------------------- 1 | some(), app()->some(), (clone app)->some() 22 | * 23 | * @param Tokens $tokens 24 | */ 25 | public function isPrecededByFuncCall(Tokens $tokens, int $position): bool 26 | { 27 | for ($i = $position; $i >= 0; --$i) { 28 | /** @var Token $currentToken */ 29 | $currentToken = $tokens[$i]; 30 | 31 | if ($currentToken->getContent() === 'clone') { 32 | return true; 33 | } 34 | 35 | if ($currentToken->getContent() === '(') { 36 | return $this->newlineAnalyzer->doesContentBeforeBracketRequireNewline($tokens, $i); 37 | } 38 | 39 | if ($this->newlineAnalyzer->isNewlineToken($currentToken)) { 40 | return false; 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | 47 | /** 48 | * Matches e.g. someMethod($this->some()->method()), [$this->some()->method()] 49 | * 50 | * @param Tokens $tokens 51 | */ 52 | public function isPartOfMethodCallOrArray(Tokens $tokens, int $position): bool 53 | { 54 | $this->bracketNesting = 0; 55 | 56 | for ($i = $position; $i >= 0; --$i) { 57 | /** @var Token $currentToken */ 58 | $currentToken = $tokens[$i]; 59 | 60 | // break 61 | if ($this->newlineAnalyzer->isNewlineToken($currentToken)) { 62 | return false; 63 | } 64 | 65 | if ($this->isBreakingChar($currentToken)) { 66 | return true; 67 | } 68 | 69 | if ($this->shouldBreakOnBracket($currentToken)) { 70 | return true; 71 | } 72 | } 73 | 74 | return false; 75 | } 76 | 77 | private function isBreakingChar(Token $currentToken): bool 78 | { 79 | if ($currentToken->isGivenKind([CT::T_ARRAY_SQUARE_BRACE_OPEN, T_ARRAY, T_DOUBLE_COLON])) { 80 | return true; 81 | } 82 | 83 | if ($currentToken->getContent() === '[') { 84 | return true; 85 | } 86 | 87 | return $currentToken->getContent() === '.'; 88 | } 89 | 90 | private function shouldBreakOnBracket(Token $token): bool 91 | { 92 | if ($token->getContent() === ')') { 93 | --$this->bracketNesting; 94 | return false; 95 | } 96 | 97 | if ($token->getContent() === '(') { 98 | if ($this->bracketNesting !== 0) { 99 | ++$this->bracketNesting; 100 | return false; 101 | } 102 | 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/DocblockRelatedParamNamesResolver.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $functionTokens; 17 | 18 | private readonly FunctionsAnalyzer $functionsAnalyzer; 19 | 20 | public function __construct( 21 | ) { 22 | $this->functionsAnalyzer = new FunctionsAnalyzer(); 23 | 24 | $this->functionTokens = [ 25 | new Token([T_FUNCTION, 'function']), 26 | ]; 27 | 28 | // only in PHP 7.4+ 29 | if ($this->doesFnTokenExist()) { 30 | $this->functionTokens[] = new Token([T_FN, 'fn']); 31 | } 32 | } 33 | 34 | /** 35 | * @return string[] 36 | * @param Tokens $tokens 37 | */ 38 | public function resolve(Tokens $tokens, int $docTokenPosition): array 39 | { 40 | $functionTokenPosition = $tokens->getNextTokenOfKind($docTokenPosition, $this->functionTokens); 41 | if ($functionTokenPosition === null) { 42 | return []; 43 | } 44 | 45 | /** @var array $functionArgumentAnalyses */ 46 | $functionArgumentAnalyses = $this->functionsAnalyzer->getFunctionArguments($tokens, $functionTokenPosition); 47 | 48 | return array_keys($functionArgumentAnalyses); 49 | } 50 | 51 | private function doesFnTokenExist(): bool 52 | { 53 | return PHP_VERSION_ID >= 70400 54 | && defined('T_FN'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/FunctionCallNameMatcher.php: -------------------------------------------------------------------------------- 1 | $tokens 17 | */ 18 | public function matchName(Tokens $tokens, int $position): ?int 19 | { 20 | try { 21 | $blockStart = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $position); 22 | } catch (Throwable) { 23 | // not a block start 24 | return null; 25 | } 26 | 27 | $previousTokenPosition = $blockStart - 1; 28 | /** @var Token $possibleMethodNameToken */ 29 | $possibleMethodNameToken = $tokens[$previousTokenPosition]; 30 | 31 | // not a "methodCall()" 32 | if (! $possibleMethodNameToken->isGivenKind(T_STRING)) { 33 | return null; 34 | } 35 | 36 | // starts with small letter? 37 | $content = $possibleMethodNameToken->getContent(); 38 | if (! ctype_lower($content[0])) { 39 | return null; 40 | } 41 | 42 | // is "someCall()"? we don't care, there are no arguments 43 | if ($tokens[$blockStart + 1]->equals(')')) { 44 | return null; 45 | } 46 | 47 | return $previousTokenPosition; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/HeredocAnalyzer.php: -------------------------------------------------------------------------------- 1 | $tokens 15 | */ 16 | public function isHerenowDoc(Tokens $tokens, BlockInfo $blockInfo): bool 17 | { 18 | // heredoc/nowdoc => skip 19 | $nextToken = $this->getNextMeaningfulToken($tokens, $blockInfo->getStart()); 20 | if (! $nextToken instanceof Token) { 21 | return false; 22 | } 23 | 24 | return \str_contains($nextToken->getContent(), '<<<'); 25 | } 26 | 27 | /** 28 | * @param Tokens $tokens 29 | */ 30 | private function getNextMeaningfulToken(Tokens $tokens, int $index): ?Token 31 | { 32 | $nextMeaningfulTokenPosition = $tokens->getNextMeaningfulToken($index); 33 | if ($nextMeaningfulTokenPosition === null) { 34 | return null; 35 | } 36 | 37 | return $tokens[$nextMeaningfulTokenPosition]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/Naming/MethodNameResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function isMethodName(Tokens $tokens, int $position, string $desiredMethodName): bool 16 | { 17 | $methodName = $this->getMethodName($tokens, $position); 18 | if (! is_string($methodName)) { 19 | return false; 20 | } 21 | 22 | return $methodName === $desiredMethodName; 23 | } 24 | 25 | /** 26 | * @param Tokens $tokens 27 | */ 28 | private function getMethodName(Tokens $tokens, int $position): ?string 29 | { 30 | /** @var Token $currentToken */ 31 | $currentToken = $tokens[$position]; 32 | if (! $currentToken->isGivenKind(T_FUNCTION)) { 33 | return null; 34 | } 35 | 36 | $nextToken = $this->getNextMeaningfulToken($tokens, $position); 37 | if (! $nextToken instanceof Token) { 38 | return null; 39 | } 40 | 41 | return $nextToken->getContent(); 42 | } 43 | 44 | /** 45 | * @param Tokens $tokens 46 | */ 47 | private function getNextMeaningfulToken(Tokens $tokens, int $index): ?Token 48 | { 49 | $nextMeaningfulTokenPosition = $tokens->getNextMeaningfulToken($index); 50 | if ($nextMeaningfulTokenPosition === null) { 51 | return null; 52 | } 53 | 54 | return $tokens[$nextMeaningfulTokenPosition]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/NewlineAnalyzer.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function doesContentBeforeBracketRequireNewline(Tokens $tokens, int $i): bool 16 | { 17 | $previousMeaningfulTokenPosition = $tokens->getPrevNonWhitespace($i); 18 | if ($previousMeaningfulTokenPosition === null) { 19 | return false; 20 | } 21 | 22 | $previousToken = $tokens[$previousMeaningfulTokenPosition]; 23 | if (! $previousToken->isGivenKind(T_STRING)) { 24 | return false; 25 | } 26 | 27 | $previousPreviousMeaningfulTokenPosition = $tokens->getPrevNonWhitespace($previousMeaningfulTokenPosition); 28 | if ($previousPreviousMeaningfulTokenPosition === null) { 29 | return false; 30 | } 31 | 32 | $previousPreviousToken = $tokens[$previousPreviousMeaningfulTokenPosition]; 33 | if ($previousPreviousToken->getContent() === '{') { 34 | return true; 35 | } 36 | 37 | // is a function 38 | return $previousPreviousToken->isGivenKind([T_RETURN, T_DOUBLE_COLON, T_OPEN_CURLY_BRACKET]); 39 | } 40 | 41 | public function isNewlineToken(Token $currentToken): bool 42 | { 43 | if (! $currentToken->isWhitespace()) { 44 | return false; 45 | } 46 | 47 | return \str_contains($currentToken->getContent(), "\n"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/TokenAnalyzer/ParamNewliner.php: -------------------------------------------------------------------------------- 1 | $tokens 24 | */ 25 | public function processFunction(Tokens $tokens, int $position): void 26 | { 27 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 28 | if (! $blockInfo instanceof BlockInfo) { 29 | return; 30 | } 31 | 32 | $this->tokensNewliner->breakItems($blockInfo, $tokens, LineKind::CALLS); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/TokenRunner/Analyzer/FixerAnalyzer/ArrayAnalyzer.php: -------------------------------------------------------------------------------- 1 | $tokens 21 | */ 22 | public function getItemCount(Tokens $tokens, BlockInfo $blockInfo): int 23 | { 24 | $nextMeanninfulPosition = $tokens->getNextMeaningfulToken($blockInfo->getStart()); 25 | if ($nextMeanninfulPosition === null) { 26 | return 0; 27 | } 28 | 29 | /** @var Token $nextMeaningfulToken */ 30 | $nextMeaningfulToken = $tokens[$nextMeanninfulPosition]; 31 | 32 | // no elements 33 | if ($this->isArrayCloser($nextMeaningfulToken)) { 34 | return 0; 35 | } 36 | 37 | $itemCount = 1; 38 | $this->traverseArrayWithoutNesting($tokens, $blockInfo, static function (Token $token) use ( 39 | &$itemCount 40 | ): void { 41 | if ($token->getContent() === ',') { 42 | ++$itemCount; 43 | } 44 | }); 45 | 46 | return $itemCount; 47 | } 48 | 49 | /** 50 | * @param Tokens $tokens 51 | */ 52 | public function isIndexedList(Tokens $tokens, BlockInfo $blockInfo): bool 53 | { 54 | $isIndexedList = false; 55 | $this->traverseArrayWithoutNesting($tokens, $blockInfo, static function (Token $token) use ( 56 | &$isIndexedList 57 | ): void { 58 | if ($token->isGivenKind(T_DOUBLE_ARROW)) { 59 | $isIndexedList = true; 60 | } 61 | }); 62 | 63 | return $isIndexedList; 64 | } 65 | 66 | /** 67 | * @param Tokens $tokens 68 | * @param callable(Token $token, int $i, Tokens $tokens): void $callable 69 | */ 70 | public function traverseArrayWithoutNesting(Tokens $tokens, BlockInfo $blockInfo, callable $callable): void 71 | { 72 | for ($i = $blockInfo->getEnd() - 1; $i >= $blockInfo->getStart() + 1; --$i) { 73 | $i = $this->tokenSkipper->skipBlocksReversed($tokens, $i); 74 | 75 | /** @var Token $token */ 76 | $token = $tokens[$i]; 77 | $callable($token, $i, $tokens); 78 | } 79 | } 80 | 81 | private function isArrayCloser(Token $token): bool 82 | { 83 | if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) { 84 | return true; 85 | } 86 | 87 | return $token->getContent() === ')'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TokenRunner/Analyzer/FixerAnalyzer/BlockFinder.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private const CONTENT_TO_BLOCK_TYPE = [ 20 | '(' => Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 21 | ')' => Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 22 | '[' => Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, 23 | ']' => Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, 24 | '{' => Tokens::BLOCK_TYPE_CURLY_BRACE, 25 | '}' => Tokens::BLOCK_TYPE_CURLY_BRACE, 26 | '#[' => Tokens::BLOCK_TYPE_ATTRIBUTE, 27 | ]; 28 | 29 | /** 30 | * @var string[] 31 | */ 32 | private const START_EDGES = ['(', '[', '{']; 33 | 34 | /** 35 | * Accepts position to both start and end token, e.g. (, ), [, ], {, } also to: "array"(, "function" ...(, "use"(, 36 | * "new" ...( 37 | * 38 | * @param Tokens $tokens 39 | */ 40 | public function findInTokensByEdge(Tokens $tokens, int $position): ?BlockInfo 41 | { 42 | $token = $tokens[$position]; 43 | 44 | if ($token->isGivenKind(T_ATTRIBUTE)) { 45 | return $this->createAttributeBlockInfo($tokens, $position); 46 | } 47 | 48 | // shift "array" to "(", event its position 49 | if ($token->isGivenKind(T_ARRAY)) { 50 | $position = $tokens->getNextMeaningfulToken($position); 51 | 52 | if ($position === null) { 53 | return null; 54 | } 55 | 56 | /** @var Token $token */ 57 | $token = $tokens[$position]; 58 | } 59 | 60 | if ($token->isGivenKind([T_FUNCTION, CT::T_USE_LAMBDA, T_NEW])) { 61 | $position = $tokens->getNextTokenOfKind($position, ['(', ';']); 62 | 63 | if ($position === null) { 64 | return null; 65 | } 66 | 67 | /** @var Token $token */ 68 | $token = $tokens[$position]; 69 | 70 | // end of line was sooner => has no block 71 | if ($token->equals(';')) { 72 | return null; 73 | } 74 | 75 | if ($token->equals('(')) { 76 | $closingPosition = $tokens->getNextMeaningfulToken($position); 77 | if ($closingPosition !== null) { 78 | $closingToken = $tokens[$closingPosition]; 79 | if ($closingToken->equals(')')) { 80 | // function has no arguments 81 | return null; 82 | } 83 | } 84 | } 85 | } 86 | 87 | $blockType = $this->getBlockTypeByToken($token); 88 | 89 | return $this->createBlockInfo($token, $position, $tokens, $blockType); 90 | } 91 | 92 | /** 93 | * @param Tokens $tokens 94 | */ 95 | public function findInTokensByPositionAndContent(Tokens $tokens, int $position, string $content): ?BlockInfo 96 | { 97 | $blockStart = $tokens->getNextTokenOfKind($position, [$content]); 98 | if ($blockStart === null) { 99 | return null; 100 | } 101 | 102 | $blockType = $this->getBlockTypeByContent($content); 103 | 104 | return new BlockInfo($blockStart, $tokens->findBlockEnd($blockType, $blockStart)); 105 | } 106 | 107 | /** 108 | * @return Tokens::BLOCK_TYPE_* 109 | */ 110 | private function getBlockTypeByContent(string $content): int 111 | { 112 | if (isset(self::CONTENT_TO_BLOCK_TYPE[$content])) { 113 | return self::CONTENT_TO_BLOCK_TYPE[$content]; 114 | } 115 | 116 | throw new MissingImplementationException(sprintf( 117 | 'Implementation is missing for "%s" in "%s". Just add it to "%s" property with proper block type', 118 | $content, 119 | __METHOD__, 120 | '$contentToBlockType' 121 | )); 122 | } 123 | 124 | /** 125 | * @return Tokens::BLOCK_TYPE_* 126 | */ 127 | private function getBlockTypeByToken(Token $token): int 128 | { 129 | if ($token->isArray()) { 130 | if (in_array($token->getContent(), ['[', ']'], true)) { 131 | return Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE; 132 | } 133 | 134 | return Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE; 135 | } 136 | 137 | return $this->getBlockTypeByContent($token->getContent()); 138 | } 139 | 140 | /** 141 | * @param Tokens $tokens 142 | * @param Tokens::BLOCK_TYPE_* $blockType 143 | */ 144 | private function createBlockInfo(Token $token, int $position, Tokens $tokens, int $blockType): ?BlockInfo 145 | { 146 | try { 147 | if (in_array($token->getContent(), self::START_EDGES, true)) { 148 | $blockStart = $position; 149 | $blockEnd = $tokens->findBlockEnd($blockType, $blockStart); 150 | } else { 151 | $blockEnd = $position; 152 | $blockStart = $tokens->findBlockStart($blockType, $blockEnd); 153 | } 154 | } catch (Throwable) { 155 | // intentionally, no edge found 156 | return null; 157 | } 158 | 159 | return new BlockInfo($blockStart, $blockEnd); 160 | } 161 | 162 | /** 163 | * @param Tokens $tokens 164 | */ 165 | private function createAttributeBlockInfo(Tokens $tokens, int $position): ?BlockInfo 166 | { 167 | // find optional attribute opener, "#[Some()]" 168 | $openerPosition = $tokens->getNextTokenOfKind($position, ['(']); 169 | if (is_int($openerPosition)) { 170 | $position = $openerPosition; 171 | } 172 | 173 | /** @var Token $token */ 174 | $token = $tokens[$position]; 175 | 176 | return $this->createBlockInfo($token, $position, $tokens, Tokens::BLOCK_TYPE_ATTRIBUTE); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/TokenRunner/Analyzer/FixerAnalyzer/CallAnalyzer.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function isMethodCall(Tokens $tokens, int $bracketPosition): bool 16 | { 17 | $objectToken = new Token([T_OBJECT_OPERATOR, '->']); 18 | $whitespaceToken = new Token([T_WHITESPACE, ' ']); 19 | 20 | $previousTokenOfKindPosition = $tokens->getPrevTokenOfKind($bracketPosition, [$objectToken, $whitespaceToken]); 21 | 22 | // probably a function call 23 | if ($previousTokenOfKindPosition === null) { 24 | return false; 25 | } 26 | 27 | /** @var Token $token */ 28 | $token = $tokens[$previousTokenOfKindPosition]; 29 | 30 | return $token->isGivenKind(T_OBJECT_OPERATOR); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TokenRunner/Analyzer/FixerAnalyzer/IndentDetector.php: -------------------------------------------------------------------------------- 1 | $tokens 20 | */ 21 | public function detectOnPosition(Tokens $tokens, int $startIndex): int 22 | { 23 | $indent = $this->whitespacesFixerConfig->getIndent(); 24 | 25 | for ($i = $startIndex; $i > 0; --$i) { 26 | /** @var Token $token */ 27 | $token = $tokens[$i]; 28 | 29 | $lastNewlinePos = strrpos($token->getContent(), "\n"); 30 | 31 | if ($token->isWhitespace() && ! $this->containsOnlySpaces($token->getContent())) { 32 | return substr_count($token->getContent(), $indent, (int) $lastNewlinePos); 33 | } 34 | 35 | if ($lastNewlinePos !== false) { 36 | return substr_count($token->getContent(), $indent, $lastNewlinePos); 37 | } 38 | } 39 | 40 | return 0; 41 | } 42 | 43 | private function containsOnlySpaces(string $tokenContent): bool 44 | { 45 | return trim($tokenContent, ' ') === ''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/TokenRunner/Analyzer/FixerAnalyzer/TokenSkipper.php: -------------------------------------------------------------------------------- 1 | $tokens 23 | */ 24 | public function skipBlocks(Tokens $tokens, int $position): int 25 | { 26 | if (! isset($tokens[$position])) { 27 | throw new TokenNotFoundException($position); 28 | } 29 | 30 | $token = $tokens[$position]; 31 | 32 | if ($token->getContent() === '{') { 33 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 34 | if (! $blockInfo instanceof BlockInfo) { 35 | return $position; 36 | } 37 | 38 | return $blockInfo->getEnd(); 39 | } 40 | 41 | if ($token->isGivenKind([CT::T_ARRAY_SQUARE_BRACE_OPEN, T_ARRAY])) { 42 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 43 | if (! $blockInfo instanceof BlockInfo) { 44 | return $position; 45 | } 46 | 47 | return $blockInfo->getEnd(); 48 | } 49 | 50 | return $position; 51 | } 52 | 53 | /** 54 | * @param Tokens $tokens 55 | */ 56 | public function skipBlocksReversed(Tokens $tokens, int $position): int 57 | { 58 | /** @var Token $token */ 59 | $token = $tokens[$position]; 60 | if (! $token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE) && ! $token->equals(')')) { 61 | return $position; 62 | } 63 | 64 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $position); 65 | if (! $blockInfo instanceof BlockInfo) { 66 | throw new ShouldNotHappenException(); 67 | } 68 | 69 | return $blockInfo->getStart(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TokenRunner/Arrays/ArrayItemNewliner.php: -------------------------------------------------------------------------------- 1 | $tokens 23 | */ 24 | public function fixArrayOpener(Tokens $tokens, BlockInfo $blockInfo): void 25 | { 26 | $this->arrayAnalyzer->traverseArrayWithoutNesting( 27 | $tokens, 28 | $blockInfo, 29 | function (Token $token, int $position, Tokens $tokens): void { 30 | if ($token->getContent() !== ',') { 31 | return; 32 | } 33 | 34 | $nextTokenPosition = $position + 1; 35 | $nextToken = $tokens[$nextTokenPosition] ?? null; 36 | if (! $nextToken instanceof Token) { 37 | return; 38 | } 39 | 40 | if (\str_contains($nextToken->getContent(), "\n")) { 41 | return; 42 | } 43 | 44 | $lookaheadPosition = $tokens->getNonWhitespaceSibling($position, 1, " \t\r\0\x0B"); 45 | if ($lookaheadPosition !== null && $tokens[$lookaheadPosition]->isGivenKind(T_COMMENT)) { 46 | return; 47 | } 48 | 49 | if ($nextToken->getContent() === '{') { 50 | return; 51 | } 52 | 53 | $tokens->ensureWhitespaceAtIndex($nextTokenPosition, 0, $this->whitespacesFixerConfig->getLineEnding()); 54 | } 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/TokenRunner/Contract/DocBlock/MalformWorkerInterface.php: -------------------------------------------------------------------------------- 1 | $tokens 14 | */ 15 | public function work(string $docContent, Tokens $tokens, int $position): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/InlineVarMalformWorker.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | */ 23 | public function work(string $docContent, Tokens $tokens, int $position): string 24 | { 25 | /** @var Token $token */ 26 | $token = $tokens[$position]; 27 | 28 | if (! $token->isGivenKind(T_COMMENT)) { 29 | return $docContent; 30 | } 31 | 32 | return Strings::replace($docContent, self::SINGLE_ASTERISK_START_REGEX, '/**$1'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/InlineVariableDocBlockMalformWorker.php: -------------------------------------------------------------------------------- 1 | $tokens 34 | */ 35 | public function work(string $docContent, Tokens $tokens, int $position): string 36 | { 37 | if (! $this->isVariableComment($tokens, $position)) { 38 | return $docContent; 39 | } 40 | 41 | // more than 2 newlines - keep it 42 | if (substr_count($docContent, "\n") > 2) { 43 | return $docContent; 44 | } 45 | 46 | // asterisk start 47 | $docContent = Strings::replace($docContent, self::SINGLE_ASTERISK_START_REGEX, '/**$1'); 48 | 49 | // inline 50 | $docContent = Strings::replace($docContent, self::SPACE_REGEX, ' '); 51 | 52 | // remove asterisk leftover 53 | return Strings::replace($docContent, self::ASTERISK_LEFTOVERS_REGEX, '$1'); 54 | } 55 | 56 | /** 57 | * @param Tokens $tokens 58 | */ 59 | private function isVariableComment(Tokens $tokens, int $position): bool 60 | { 61 | $nextPosition = $tokens->getNextMeaningfulToken($position); 62 | if ($nextPosition === null) { 63 | return false; 64 | } 65 | 66 | $nextNextPosition = $tokens->getNextMeaningfulToken($nextPosition + 2); 67 | if ($nextNextPosition === null) { 68 | return false; 69 | } 70 | 71 | /** @var Token $nextNextToken */ 72 | $nextNextToken = $tokens[$nextNextPosition]; 73 | if ($nextNextToken->isGivenKind([T_STATIC, T_FUNCTION])) { 74 | return false; 75 | } 76 | 77 | // is inline variable 78 | /** @var Token $nextToken */ 79 | $nextToken = $tokens[$nextPosition]; 80 | return $nextToken->isGivenKind(T_VARIABLE); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/MissingParamNameMalformWorker.php: -------------------------------------------------------------------------------- 1 | $tokens 42 | */ 43 | public function work(string $docContent, Tokens $tokens, int $position): string 44 | { 45 | $argumentNames = $this->docblockRelatedParamNamesResolver->resolve($tokens, $position); 46 | if ($argumentNames === []) { 47 | return $docContent; 48 | } 49 | 50 | $missingArgumentNames = $this->filterOutExistingParamNames($docContent, $argumentNames); 51 | if ($missingArgumentNames === []) { 52 | return $docContent; 53 | } 54 | 55 | $docBlock = new DocBlock($docContent); 56 | 57 | $this->completeMissingArgumentNames($missingArgumentNames, $argumentNames, $docBlock); 58 | 59 | return $docBlock->getContent(); 60 | } 61 | 62 | /** 63 | * @param string[] $functionArgumentNames 64 | * @return string[] 65 | */ 66 | private function filterOutExistingParamNames(string $docContent, array $functionArgumentNames): array 67 | { 68 | foreach ($functionArgumentNames as $key => $functionArgumentName) { 69 | $pattern = '# ' . preg_quote($functionArgumentName, '#') . '\b#'; 70 | if (Strings::match($docContent, $pattern)) { 71 | unset($functionArgumentNames[$key]); 72 | } 73 | } 74 | 75 | return array_values($functionArgumentNames); 76 | } 77 | 78 | /** 79 | * @param string[] $missingArgumentNames 80 | * @param string[] $argumentNames 81 | */ 82 | private function completeMissingArgumentNames( 83 | array $missingArgumentNames, 84 | array $argumentNames, 85 | DocBlock $docBlock 86 | ): void { 87 | foreach ($missingArgumentNames as $key => $missingArgumentName) { 88 | $newArgumentName = $this->resolveNewArgumentName($argumentNames, $missingArgumentName, $key); 89 | 90 | $lines = $docBlock->getLines(); 91 | foreach ($lines as $line) { 92 | if ($this->shouldSkipLine($line)) { 93 | continue; 94 | } 95 | 96 | $newLineContent = $this->createNewLineContent($newArgumentName, $line); 97 | $line->setContent($newLineContent); 98 | continue 2; 99 | } 100 | } 101 | } 102 | 103 | /** 104 | * @param string[] $argumentNames 105 | */ 106 | private function resolveNewArgumentName(array $argumentNames, string $missingArgumentName, int $key): string 107 | { 108 | if (array_search($missingArgumentName, $argumentNames, true)) { 109 | return $missingArgumentName; 110 | } 111 | 112 | return $argumentNames[$key]; 113 | } 114 | 115 | private function shouldSkipLine(Line $line): bool 116 | { 117 | if (! \str_contains($line->getContent(), self::PARAM_ANNOTATOIN_START_REGEX)) { 118 | return true; 119 | } 120 | 121 | // already has a param name 122 | if (Strings::match($line->getContent(), self::PARAM_WITH_NAME_REGEX)) { 123 | return true; 124 | } 125 | 126 | $match = Strings::match($line->getContent(), self::PARAM_WITHOUT_NAME_REGEX); 127 | return $match === null; 128 | } 129 | 130 | private function createNewLineContent(string $newArgumentName, Line $line): string 131 | { 132 | // @see https://regex101.com/r/4FL49H/1 133 | $missingDollarSignPattern = '#(@param\s+([\w\|\[\]\\\\]+\s)?)(' . ltrim($newArgumentName, '$') . ')#'; 134 | 135 | // missing \$ case - possibly own worker 136 | if (Strings::match($line->getContent(), $missingDollarSignPattern)) { 137 | return Strings::replace($line->getContent(), $missingDollarSignPattern, '$1$$3'); 138 | } 139 | 140 | $replacement = '@param $1 ' . $newArgumentName . '$2' . "\n"; 141 | 142 | return Strings::replace($line->getContent(), self::PARAM_WITHOUT_NAME_REGEX, $replacement); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/MissingVarNameMalformWorker.php: -------------------------------------------------------------------------------- 1 | \/\*\* @(?:psalm-|phpstan-)?var )(?[\\\\\w\|-|]+)(?\s+\*\/)$#'; 19 | 20 | /** 21 | * @param Tokens $tokens 22 | */ 23 | public function work(string $docContent, Tokens $tokens, int $position): string 24 | { 25 | if (! Strings::match($docContent, self::VAR_WITHOUT_NAME_REGEX)) { 26 | return $docContent; 27 | } 28 | 29 | $nextVariableToken = $this->getNextVariableToken($tokens, $position); 30 | if (! $nextVariableToken instanceof Token) { 31 | return $docContent; 32 | } 33 | 34 | return Strings::replace( 35 | $docContent, 36 | self::VAR_WITHOUT_NAME_REGEX, 37 | static fn (array $match): string => $match['open'] . $match['type'] . ' ' . $nextVariableToken->getContent() . $match['close'] 38 | ); 39 | } 40 | 41 | /** 42 | * @param Tokens $tokens 43 | */ 44 | private function getNextVariableToken(Tokens $tokens, int $position): ?Token 45 | { 46 | $nextMeaningfulTokenPosition = $tokens->getNextMeaningfulToken($position); 47 | if ($nextMeaningfulTokenPosition === null) { 48 | return null; 49 | } 50 | 51 | $nextToken = $tokens[$nextMeaningfulTokenPosition] ?? null; 52 | if (! $nextToken instanceof Token) { 53 | return null; 54 | } 55 | 56 | if (! $nextToken->isGivenKind(T_VARIABLE)) { 57 | return null; 58 | } 59 | 60 | return $nextToken; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/ParamNameReferenceMalformWorker.php: -------------------------------------------------------------------------------- 1 | @param(.*?))&(?\$\w+)#'; 19 | 20 | /** 21 | * @param Tokens $tokens 22 | */ 23 | public function work(string $docContent, Tokens $tokens, int $position): string 24 | { 25 | return Strings::replace( 26 | $docContent, 27 | self::PARAM_NAME_REGEX, 28 | static fn ($match): string => $match['param'] . $match['paramName'] 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/ParamNameTypoMalformWorker.php: -------------------------------------------------------------------------------- 1 | callable)?(.*?)(?\$\w+)#'; 22 | 23 | public function __construct( 24 | private DocblockRelatedParamNamesResolver $docblockRelatedParamNamesResolver 25 | ) { 26 | } 27 | 28 | /** 29 | * @param Tokens $tokens 30 | */ 31 | public function work(string $docContent, Tokens $tokens, int $position): string 32 | { 33 | $argumentNames = $this->docblockRelatedParamNamesResolver->resolve($tokens, $position); 34 | if ($argumentNames === []) { 35 | return $docContent; 36 | } 37 | 38 | $paramNames = $this->getParamNames($docContent); 39 | 40 | $missArgumentNames = []; 41 | // remove correct params 42 | foreach ($argumentNames as $key => $argumentName) { 43 | if (in_array($argumentName, $paramNames, true)) { 44 | $paramPosition = array_search($argumentName, $paramNames, true); 45 | unset($paramNames[$paramPosition]); 46 | } else { 47 | $missArgumentNames[$key] = $argumentName; 48 | } 49 | } 50 | 51 | // nothing to edit, all arguments are correct or there are no more @param annotations 52 | if ($missArgumentNames === []) { 53 | return $docContent; 54 | } 55 | 56 | if ($paramNames === []) { 57 | return $docContent; 58 | } 59 | 60 | return $this->fixTypos($argumentNames, $missArgumentNames, $paramNames, $docContent); 61 | } 62 | 63 | /** 64 | * @return string[] 65 | */ 66 | private function getParamNames(string $docContent): array 67 | { 68 | $paramAnnotations = $this->getAnnotationsOfType($docContent, 'param'); 69 | 70 | $paramNames = []; 71 | foreach ($paramAnnotations as $paramAnnotation) { 72 | $match = Strings::match($paramAnnotation->getContent(), self::PARAM_NAME_REGEX); 73 | if (isset($match['paramName'])) { 74 | // skip callables, as they contain nested params 75 | if (isset($match['callable']) && $match['callable'] === 'callable') { 76 | continue; 77 | } 78 | 79 | $paramNames[] = $match['paramName']; 80 | } 81 | } 82 | 83 | return $paramNames; 84 | } 85 | 86 | /** 87 | * @return Annotation[] 88 | */ 89 | private function getAnnotationsOfType(string $docContent, string $type): array 90 | { 91 | $docBlock = new DocBlock($docContent); 92 | 93 | return $docBlock->getAnnotationsOfType($type); 94 | } 95 | 96 | /** 97 | * @param string[] $argumentNames 98 | * @param string[] $missArgumentNames 99 | * @param string[] $paramNames 100 | */ 101 | private function fixTypos(array $argumentNames, array $missArgumentNames, array $paramNames, string $docContent): string 102 | { 103 | // A table of permuted params. initialized by $argumentName instead of $paramNames is correct 104 | $replacedParams = array_fill_keys($argumentNames, false); 105 | 106 | foreach ($missArgumentNames as $key => $argumentName) { 107 | // 1. the same position 108 | if (! isset($paramNames[$key])) { 109 | continue; 110 | } 111 | 112 | $typoName = $paramNames[$key]; 113 | $replacePattern = '#@param(.*?)(' . preg_quote($typoName, '#') . '\b)#'; 114 | 115 | $docContent = Strings::replace($docContent, $replacePattern, static function (array $matched) use ($argumentName, &$replacedParams) { 116 | $paramName = $matched[2]; 117 | 118 | // 2. If the PHPDoc $paramName is one of the existing $argumentNames and has not already been replaced, it will be deferred 119 | if (isset($replacedParams[$paramName]) && ! $replacedParams[$paramName]) { 120 | $replacedParams[$paramName] = true; 121 | 122 | return $matched[0]; 123 | } 124 | 125 | // 3. Otherwise, replace $paramName with $argumentName in the @param line 126 | return sprintf('@param%s%s', $matched[1], $argumentName); 127 | }); 128 | } 129 | 130 | return $docContent; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/SuperfluousReturnNameMalformWorker.php: -------------------------------------------------------------------------------- 1 | @(?:psalm-|phpstan-)?return)(?\s+[|\\\\\w]+)?(\s+)(?<' . self::VARIABLE_NAME_PART . '>\$[\w]+)#'; 20 | 21 | /** 22 | * @var string[] 23 | */ 24 | private const ALLOWED_VARIABLE_NAMES = ['$this']; 25 | 26 | /** 27 | * @var string 28 | * @see https://regex101.com/r/IE9fA6/1 29 | */ 30 | private const VARIABLE_NAME_REGEX = '#\$\w+#'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private const VARIABLE_NAME_PART = 'variableName'; 36 | 37 | /** 38 | * @param Tokens $tokens 39 | */ 40 | public function work(string $docContent, Tokens $tokens, int $position): string 41 | { 42 | $docBlock = new DocBlock($docContent); 43 | 44 | $lines = $docBlock->getLines(); 45 | foreach ($lines as $line) { 46 | $match = Strings::match($line->getContent(), self::RETURN_VARIABLE_NAME_REGEX); 47 | if ($match === null) { 48 | continue; 49 | } 50 | 51 | if ($this->shouldSkip($match, $line->getContent())) { 52 | continue; 53 | } 54 | 55 | $newLineContent = Strings::replace( 56 | $line->getContent(), 57 | self::RETURN_VARIABLE_NAME_REGEX, 58 | static function (array $match) { 59 | $replacement = $match['tag']; 60 | if ($match['type'] !== []) { 61 | $replacement .= $match['type']; 62 | } 63 | 64 | return $replacement; 65 | } 66 | ); 67 | 68 | $line->setContent($newLineContent); 69 | } 70 | 71 | return $docBlock->getContent(); 72 | } 73 | 74 | /** 75 | * @param array $match 76 | */ 77 | private function shouldSkip(array $match, string $content): bool 78 | { 79 | if (in_array($match[self::VARIABLE_NAME_PART], self::ALLOWED_VARIABLE_NAMES, true)) { 80 | return true; 81 | } 82 | 83 | // has multiple return values? "@return array $one, $two" 84 | return count(Strings::matchAll($content, self::VARIABLE_NAME_REGEX)) >= 2; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/SuperfluousVarNameMalformWorker.php: -------------------------------------------------------------------------------- 1 | @(?:psalm-|phpstan-)?var)(?\s+[|\\\\\w]+)?(\s+)(?\$[\w]+)#'; 26 | 27 | /** 28 | * @param Tokens $tokens 29 | */ 30 | public function work(string $docContent, Tokens $tokens, int $position): string 31 | { 32 | if ($this->shouldSkip($tokens, $position)) { 33 | return $docContent; 34 | } 35 | 36 | $docBlock = new DocBlock($docContent); 37 | 38 | $lines = $docBlock->getLines(); 39 | foreach ($lines as $line) { 40 | $match = Strings::match($line->getContent(), self::VAR_VARIABLE_NAME_REGEX); 41 | if ($match === null) { 42 | continue; 43 | } 44 | 45 | $newLineContent = Strings::replace( 46 | $line->getContent(), 47 | self::VAR_VARIABLE_NAME_REGEX, 48 | static function (array $match): string { 49 | $replacement = $match['tag']; 50 | if ($match['type'] !== []) { 51 | $replacement .= $match['type']; 52 | } 53 | 54 | if (Strings::match($match['propertyName'], self::THIS_VARIABLE_REGEX)) { 55 | return $match['tag'] . ' self'; 56 | } 57 | 58 | return $replacement; 59 | } 60 | ); 61 | 62 | $line->setContent($newLineContent); 63 | } 64 | 65 | return $docBlock->getContent(); 66 | } 67 | 68 | /** 69 | * Is property doc block? 70 | * 71 | * @param Tokens $tokens 72 | */ 73 | private function shouldSkip(Tokens $tokens, int $position): bool 74 | { 75 | $nextMeaningfulTokenPosition = $tokens->getNextMeaningfulToken($position); 76 | 77 | // nothing to change 78 | if ($nextMeaningfulTokenPosition === null) { 79 | return true; 80 | } 81 | 82 | /** @var Token $nextMeaningfulToken */ 83 | $nextMeaningfulToken = $tokens[$nextMeaningfulTokenPosition]; 84 | 85 | // should be protected/private/public/static, to know we're property 86 | return ! $nextMeaningfulToken->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_STATIC]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/TokenRunner/DocBlock/MalformWorker/SwitchedTypeAndNameMalformWorker.php: -------------------------------------------------------------------------------- 1 | \$\w+)(\s+)(?[|\\\\\w\[\]\<\>]+)#'; 20 | 21 | /** 22 | * @param Tokens $tokens 23 | */ 24 | public function work(string $docContent, Tokens $tokens, int $position): string 25 | { 26 | $docBlock = new DocBlock($docContent); 27 | 28 | $lines = $docBlock->getLines(); 29 | foreach ($lines as $line) { 30 | // $value is first, instead of type is first 31 | $match = Strings::match($line->getContent(), self::NAME_THEN_TYPE_REGEX); 32 | if ($match === null) { 33 | continue; 34 | } 35 | 36 | if ($match['name'] === '') { 37 | continue; 38 | } 39 | 40 | if ($match['type'] === '') { 41 | continue; 42 | } 43 | 44 | // skip random words that look like type without autolaoding 45 | if (in_array($match['type'], ['The', 'Set'], true)) { 46 | continue; 47 | } 48 | 49 | $newLine = Strings::replace($line->getContent(), self::NAME_THEN_TYPE_REGEX, '@$1$2$5$4$3'); 50 | $line->setContent($newLine); 51 | } 52 | 53 | return $docBlock->getContent(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/TokenRunner/Enum/LineKind.php: -------------------------------------------------------------------------------- 1 | $tokens 15 | */ 16 | public function getPreviousMeaningfulToken(Tokens $tokens, int | Token $position): Token 17 | { 18 | if (is_int($position)) { 19 | return $this->findPreviousTokenByPosition($tokens, $position); 20 | } 21 | 22 | return $this->findPreviousTokenByToken($tokens, $position); 23 | } 24 | 25 | /** 26 | * @param Tokens $tokens 27 | */ 28 | private function findPreviousTokenByPosition(Tokens $tokens, int $position): Token 29 | { 30 | $previousPosition = $position - 1; 31 | if (! isset($tokens[$previousPosition])) { 32 | throw new ShouldNotHappenException(); 33 | } 34 | 35 | return $tokens[$previousPosition]; 36 | } 37 | 38 | /** 39 | * @param Tokens $tokens 40 | */ 41 | private function findPreviousTokenByToken(Tokens $tokens, Token $positionToken): Token 42 | { 43 | $position = $this->resolvePositionByToken($tokens, $positionToken); 44 | return $this->findPreviousTokenByPosition($tokens, $position - 1); 45 | } 46 | 47 | /** 48 | * @param Tokens $tokens 49 | */ 50 | private function resolvePositionByToken(Tokens $tokens, Token $positionToken): int 51 | { 52 | foreach ($tokens as $position => $token) { 53 | if ($token === $positionToken) { 54 | return $position; 55 | } 56 | } 57 | 58 | throw new ShouldNotHappenException(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/FirstLineLengthResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 23 | */ 24 | public function resolveFromTokensAndStartPosition(Tokens $tokens, BlockInfo $blockInfo): int 25 | { 26 | // compute from here to start of line 27 | $currentPosition = $blockInfo->getStart(); 28 | 29 | // collect length of tokens on current line which precede token at $currentPosition 30 | $lineLengthAndPosition = $this->lineLengthAndPositionFactory->createFromTokensAndLineStartPosition( 31 | $tokens, 32 | $currentPosition 33 | ); 34 | $lineLength = $lineLengthAndPosition->getLineLength(); 35 | $currentPosition = $lineLengthAndPosition->getCurrentPosition(); 36 | 37 | /** @var Token $currentToken */ 38 | $currentToken = $tokens[$currentPosition]; 39 | 40 | // includes indent in the beginning 41 | $lineLength += strlen($currentToken->getContent()); 42 | 43 | // minus end of lines, do not count line feeds as characters 44 | $endOfLineCount = substr_count($currentToken->getContent(), "\n"); 45 | $lineLength -= $endOfLineCount; 46 | 47 | // compute from here to end of line 48 | $currentPosition = $blockInfo->getStart() + 1; 49 | 50 | // collect length of tokens on current line which follow token at $currentPosition 51 | while (! $this->isEndOFArgumentsLine($tokens, $currentPosition)) { 52 | /** @var Token $currentToken */ 53 | $currentToken = $tokens[$currentPosition]; 54 | 55 | // in case of multiline string, we are interested in length of the part on current line only 56 | $explode = explode("\n", $currentToken->getContent(), 2); 57 | // string follows current token, so we are interested in beginning only 58 | $lineLength += strlen($explode[0]); 59 | 60 | ++$currentPosition; 61 | 62 | if (count($explode) > 1) { 63 | // no longer need to continue searching for end of arguments 64 | break; 65 | } 66 | 67 | if (! isset($tokens[$currentPosition])) { 68 | break; 69 | } 70 | } 71 | 72 | return $lineLength; 73 | } 74 | 75 | /** 76 | * @param Tokens $tokens 77 | */ 78 | private function isEndOFArgumentsLine(Tokens $tokens, int $position): bool 79 | { 80 | if (! isset($tokens[$position])) { 81 | throw new TokenNotFoundException($position); 82 | } 83 | 84 | if (\str_starts_with($tokens[$position]->getContent(), "\n")) { 85 | return true; 86 | } 87 | 88 | return $tokens[$position]->isGivenKind(CT::T_USE_LAMBDA); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/LineLengthCloserTransformer.php: -------------------------------------------------------------------------------- 1 | $tokens 25 | */ 26 | public function insertNewlineBeforeClosingIfNeeded( 27 | Tokens $tokens, 28 | BlockInfo $blockInfo, 29 | int $kind, 30 | string $newlineIndentWhitespace, 31 | string $closingBracketNewlineIndentWhitespace 32 | ): void { 33 | $isMethodCall = $this->callAnalyzer->isMethodCall($tokens, $blockInfo->getStart()); 34 | $endIndex = $blockInfo->getEnd(); 35 | 36 | $previousToken = $this->tokenFinder->getPreviousMeaningfulToken($tokens, $endIndex); 37 | $previousPreviousToken = $this->tokenFinder->getPreviousMeaningfulToken($tokens, $previousToken); 38 | 39 | // special case, if the function is followed by array - method([...]) - but not - method([[...]])) 40 | if ($this->shouldAddNewlineEarlier($previousToken, $previousPreviousToken, $isMethodCall, $kind)) { 41 | $tokens->ensureWhitespaceAtIndex($endIndex - 1, 0, $newlineIndentWhitespace); 42 | return; 43 | } 44 | 45 | $tokens->ensureWhitespaceAtIndex($endIndex - 1, 1, $closingBracketNewlineIndentWhitespace); 46 | } 47 | 48 | private function shouldAddNewlineEarlier( 49 | Token $previousToken, 50 | Token $previousPreviousToken, 51 | bool $isMethodCall, 52 | int $kind 53 | ): bool { 54 | if ($isMethodCall) { 55 | return false; 56 | } 57 | 58 | if ($kind !== LineKind::CALLS) { 59 | return false; 60 | } 61 | 62 | if (! $previousToken->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) { 63 | return false; 64 | } 65 | 66 | if ($this->isEmptyArray($previousPreviousToken)) { 67 | return false; 68 | } 69 | 70 | return ! $previousPreviousToken->isGivenKind([CT::T_ARRAY_SQUARE_BRACE_CLOSE, CT::T_ARRAY_SQUARE_BRACE_OPEN]); 71 | } 72 | 73 | private function isEmptyArray(Token $token): bool 74 | { 75 | if (! $token->isArray()) { 76 | return false; 77 | } 78 | 79 | return trim($token->getContent()) === ''; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/LineLengthOpenerTransformer.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | */ 23 | public function insertNewlineAfterOpeningIfNeeded( 24 | Tokens $tokens, 25 | int $blockStartIndex, 26 | string $newlineIndentWhitespace 27 | ): void { 28 | if (! isset($tokens[$blockStartIndex + 1])) { 29 | throw new TokenNotFoundException($blockStartIndex + 1); 30 | } 31 | 32 | /** @var Token $nextToken */ 33 | $nextToken = $tokens[$blockStartIndex + 1]; 34 | 35 | if ($nextToken->isGivenKind(T_WHITESPACE)) { 36 | $tokens->ensureWhitespaceAtIndex($blockStartIndex + 1, 0, $newlineIndentWhitespace); 37 | return; 38 | } 39 | 40 | // special case, if the function is followed by array - method([...]) 41 | if ($nextToken->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]) && ! $this->callAnalyzer->isMethodCall( 42 | $tokens, 43 | $blockStartIndex 44 | )) { 45 | $tokens->ensureWhitespaceAtIndex($blockStartIndex + 1, 1, $newlineIndentWhitespace); 46 | return; 47 | } 48 | 49 | $tokens->ensureWhitespaceAtIndex($blockStartIndex, 1, $newlineIndentWhitespace); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/LineLengthResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 15 | */ 16 | public function getLengthFromStartEnd(Tokens $tokens, BlockInfo $blockInfo): int 17 | { 18 | $lineLength = 0; 19 | 20 | // compute from function to start of line 21 | $start = $blockInfo->getStart(); 22 | while (! $this->isNewLineOrOpenTag($tokens, $start)) { 23 | $lineLength += strlen($tokens[$start]->getContent()); 24 | --$start; 25 | 26 | if (! isset($tokens[$start])) { 27 | break; 28 | } 29 | } 30 | 31 | // get spaces to first line 32 | $lineLength += strlen($tokens[$start]->getContent()); 33 | 34 | // get length from start of function till end of arguments - with spaces as one 35 | $lineLength += $this->getLengthFromFunctionStartToEndOfArguments($blockInfo, $tokens); 36 | 37 | // get length from end or arguments to first line break 38 | $lineLength += $this->getLengthFromEndOfArgumentToLineBreak($blockInfo, $tokens); 39 | 40 | return $lineLength; 41 | } 42 | 43 | /** 44 | * @param Tokens $tokens 45 | */ 46 | private function isNewLineOrOpenTag(Tokens $tokens, int $position): bool 47 | { 48 | /** @var Token $currentToken */ 49 | $currentToken = $tokens[$position]; 50 | 51 | if (\str_starts_with($currentToken->getContent(), "\n")) { 52 | return true; 53 | } 54 | 55 | return $currentToken->isGivenKind(T_OPEN_TAG); 56 | } 57 | 58 | /** 59 | * @param Tokens $tokens 60 | */ 61 | private function getLengthFromFunctionStartToEndOfArguments(BlockInfo $blockInfo, Tokens $tokens): int 62 | { 63 | $length = 0; 64 | 65 | $start = $blockInfo->getStart(); 66 | 67 | while ($start < $blockInfo->getEnd()) { 68 | /** @var Token $currentToken */ 69 | $currentToken = $tokens[$start]; 70 | 71 | if ($currentToken->isGivenKind(T_WHITESPACE)) { 72 | ++$length; 73 | ++$start; 74 | continue; 75 | } 76 | 77 | $length += strlen($currentToken->getContent()); 78 | ++$start; 79 | 80 | if (! isset($tokens[$start])) { 81 | break; 82 | } 83 | } 84 | 85 | return $length; 86 | } 87 | 88 | /** 89 | * @param Tokens $tokens 90 | */ 91 | private function getLengthFromEndOfArgumentToLineBreak(BlockInfo $blockInfo, Tokens $tokens): int 92 | { 93 | $length = 0; 94 | 95 | $end = $blockInfo->getEnd(); 96 | 97 | /** @var Token $currentToken */ 98 | $currentToken = $tokens[$end]; 99 | 100 | while (! \str_starts_with($currentToken->getContent(), "\n")) { 101 | $length += strlen($currentToken->getContent()); 102 | ++$end; 103 | 104 | if (! isset($tokens[$end])) { 105 | break; 106 | } 107 | 108 | /** @var Token $currentToken */ 109 | $currentToken = $tokens[$end]; 110 | } 111 | 112 | return $length; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/LineLengthTransformer.php: -------------------------------------------------------------------------------- 1 | $tokens 25 | */ 26 | public function fixStartPositionToEndPosition( 27 | BlockInfo $blockInfo, 28 | Tokens $tokens, 29 | int $lineLength, 30 | bool $breakLongLines, 31 | bool $inlineShortLine 32 | ): void { 33 | if ($this->hasPromotedProperty($tokens, $blockInfo)) { 34 | return; 35 | } 36 | 37 | $firstLineLength = $this->firstLineLengthResolver->resolveFromTokensAndStartPosition($tokens, $blockInfo); 38 | if ($firstLineLength > $lineLength && $breakLongLines) { 39 | $this->tokensNewliner->breakItems($blockInfo, $tokens, LineKind::CALLS); 40 | return; 41 | } 42 | 43 | $fullLineLength = $this->lineLengthResolver->getLengthFromStartEnd($tokens, $blockInfo); 44 | if ($fullLineLength > $lineLength) { 45 | return; 46 | } 47 | 48 | if (! $inlineShortLine) { 49 | return; 50 | } 51 | 52 | $this->tokensInliner->inlineItems($tokens, $blockInfo); 53 | } 54 | 55 | /** 56 | * @param Tokens $tokens 57 | */ 58 | private function hasPromotedProperty(Tokens $tokens, BlockInfo $blockInfo): bool 59 | { 60 | $resultByKind = $tokens->findGivenKind([ 61 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, 62 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, 63 | CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, 64 | ], $blockInfo->getStart(), $blockInfo->getEnd()); 65 | 66 | return (bool) array_filter($resultByKind); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/TokensInliner.php: -------------------------------------------------------------------------------- 1 | $tokens 21 | */ 22 | public function inlineItems(Tokens $tokens, BlockInfo $blockInfo): void 23 | { 24 | // replace line feeds with " " 25 | for ($i = $blockInfo->getStart() + 1; $i < $blockInfo->getEnd(); ++$i) { 26 | /** @var Token $currentToken */ 27 | $currentToken = $tokens[$i]; 28 | $i = $this->tokenSkipper->skipBlocks($tokens, $i); 29 | if (! $currentToken->isGivenKind(T_WHITESPACE)) { 30 | continue; 31 | } 32 | 33 | /** @var Token $previousToken */ 34 | $previousToken = $tokens[$i - 1]; 35 | 36 | /** @var Token $nextToken */ 37 | $nextToken = $tokens[$i + 1]; 38 | 39 | // do not clear before *doc end, removing spaces breaks stuff 40 | if ($previousToken->isGivenKind([T_START_HEREDOC, T_END_HEREDOC])) { 41 | continue; 42 | } 43 | 44 | // clear space after opening and before closing bracket 45 | if ($this->isBlockStartOrEnd($previousToken, $nextToken)) { 46 | $tokens->clearAt($i); 47 | continue; 48 | } 49 | 50 | $tokens[$i] = new Token([T_WHITESPACE, ' ']); 51 | } 52 | } 53 | 54 | private function isBlockStartOrEnd(Token $previousToken, Token $nextToken): bool 55 | { 56 | if (in_array($previousToken->getContent(), ['(', '['], true)) { 57 | return true; 58 | } 59 | 60 | return in_array($nextToken->getContent(), [')', ']'], true); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/TokenRunner/Transformer/FixerTransformer/TokensNewliner.php: -------------------------------------------------------------------------------- 1 | $tokens 28 | */ 29 | public function breakItems(BlockInfo $blockInfo, Tokens $tokens, int $kind): void 30 | { 31 | // from bottom top, to prevent skipping ids 32 | // e.g when token is added in the middle, the end index does now point to earlier element! 33 | $currentNewlineIndentWhitespace = $this->indentResolver->resolveCurrentNewlineIndentWhitespace( 34 | $tokens, 35 | $blockInfo->getStart() 36 | ); 37 | 38 | $newlineIndentWhitespace = $this->indentResolver->resolveNewlineIndentWhitespace( 39 | $tokens, 40 | $blockInfo->getStart() 41 | ); 42 | 43 | // 1. break before arguments closing 44 | $this->lineLengthCloserTransformer->insertNewlineBeforeClosingIfNeeded( 45 | $tokens, 46 | $blockInfo, 47 | $kind, 48 | $currentNewlineIndentWhitespace, 49 | $this->indentResolver->resolveClosingBracketNewlineWhitespace($tokens, $blockInfo->getStart()), 50 | ); 51 | 52 | // again, from the bottom to the top 53 | for ($i = $blockInfo->getEnd() - 1; $i >= $blockInfo->getStart(); --$i) { 54 | /** @var Token $currentToken */ 55 | $currentToken = $tokens[$i]; 56 | 57 | $i = $this->tokenSkipper->skipBlocksReversed($tokens, $i); 58 | 59 | // 2. new line after each comma ",", instead of just space 60 | if ($currentToken->getContent() === ',') { 61 | if ($this->isLastItem($tokens, $i)) { 62 | continue; 63 | } 64 | 65 | if ($this->isFollowedByComment($tokens, $i)) { 66 | continue; 67 | } 68 | 69 | $tokens->ensureWhitespaceAtIndex($i + 1, 0, $newlineIndentWhitespace); 70 | } 71 | } 72 | 73 | // 3. break after arguments opening 74 | $this->lineLengthOpenerTransformer->insertNewlineAfterOpeningIfNeeded( 75 | $tokens, 76 | $blockInfo->getStart(), 77 | $newlineIndentWhitespace 78 | ); 79 | } 80 | 81 | /** 82 | * Has already newline? usually the last line => skip to prevent double spacing 83 | * 84 | * @param Tokens $tokens 85 | */ 86 | private function isLastItem(Tokens $tokens, int $position): bool 87 | { 88 | $nextPosition = $position + 1; 89 | if (! isset($tokens[$nextPosition])) { 90 | throw new TokenNotFoundException($nextPosition); 91 | } 92 | 93 | $tokenContent = $tokens[$nextPosition]->getContent(); 94 | return \str_contains($tokenContent, $this->whitespacesFixerConfig->getLineEnding()); 95 | } 96 | 97 | /** 98 | * @param Tokens $tokens 99 | */ 100 | private function isFollowedByComment(Tokens $tokens, int $i): bool 101 | { 102 | $nextToken = $tokens[$i + 1]; 103 | $nextNextToken = $tokens[$i + 2]; 104 | 105 | if ($nextNextToken->isComment()) { 106 | return true; 107 | } 108 | 109 | // if next token is just space, turn it to newline 110 | if (! $nextToken->isWhitespace(' ')) { 111 | return false; 112 | } 113 | 114 | return $nextNextToken->isComment(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/TokenRunner/Traverser/ArrayBlockInfoFinder.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | * @return BlockInfo[] 23 | */ 24 | public function findArrayOpenerBlockInfos(Tokens $tokens): array 25 | { 26 | $reversedTokens = $this->reverseTokens($tokens); 27 | 28 | $blockInfos = []; 29 | foreach ($reversedTokens as $index => $token) { 30 | if (! $token->isGivenKind(TokenKinds::ARRAY_OPEN_TOKENS)) { 31 | continue; 32 | } 33 | 34 | $blockInfo = $this->blockFinder->findInTokensByEdge($tokens, $index); 35 | if (! $blockInfo instanceof BlockInfo) { 36 | continue; 37 | } 38 | 39 | $blockInfos[] = $blockInfo; 40 | } 41 | 42 | return $blockInfos; 43 | } 44 | 45 | /** 46 | * @param Tokens $tokens 47 | * @return Token[] 48 | */ 49 | private function reverseTokens(Tokens $tokens): array 50 | { 51 | return array_reverse($tokens->toArray(), true); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/TokenRunner/Traverser/TokenReverser.php: -------------------------------------------------------------------------------- 1 | $tokens 20 | * @return Token[] 21 | */ 22 | public function reverse(Tokens $tokens): array 23 | { 24 | $reversedTokens = array_reverse($tokens->toArray(), true); 25 | 26 | // remove null values 27 | return array_filter($reversedTokens); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/TokenRunner/ValueObject/BlockInfo.php: -------------------------------------------------------------------------------- 1 | start; 18 | } 19 | 20 | public function getEnd(): int 21 | { 22 | return $this->end; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TokenRunner/ValueObject/LineLengthAndPosition.php: -------------------------------------------------------------------------------- 1 | lineLength; 18 | } 19 | 20 | public function getCurrentPosition(): int 21 | { 22 | return $this->currentPosition; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TokenRunner/ValueObject/TokenKinds.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | */ 23 | public function __construct( 24 | private Tokens $tokens, 25 | private BlockInfo $blockInfo, 26 | private TokenSkipper $tokenSkipper 27 | ) { 28 | } 29 | 30 | public function isAssociativeArray(): bool 31 | { 32 | for ($i = $this->blockInfo->getStart() + 1; $i <= $this->blockInfo->getEnd() - 1; ++$i) { 33 | $i = $this->tokenSkipper->skipBlocks($this->tokens, $i); 34 | 35 | $token = $this->tokens[$i]; 36 | if ($token->isGivenKind(T_DOUBLE_ARROW)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | public function getItemCount(): int 45 | { 46 | $itemCount = 0; 47 | for ($i = $this->blockInfo->getEnd() - 1; $i >= $this->blockInfo->getStart(); --$i) { 48 | $i = $this->tokenSkipper->skipBlocksReversed($this->tokens, $i); 49 | 50 | $token = $this->tokens[$i]; 51 | if ($token->isGivenKind(T_DOUBLE_ARROW)) { 52 | ++$itemCount; 53 | } 54 | } 55 | 56 | return $itemCount; 57 | } 58 | 59 | public function isFirstItemArray(): bool 60 | { 61 | for ($i = $this->blockInfo->getEnd() - 1; $i >= $this->blockInfo->getStart(); --$i) { 62 | $i = $this->tokenSkipper->skipBlocksReversed($this->tokens, $i); 63 | 64 | /** @var Token $token */ 65 | $token = $this->tokens[$i]; 66 | if ($token->isGivenKind(T_DOUBLE_ARROW)) { 67 | $nextTokenAfterArrowPosition = $this->tokens->getNextNonWhitespace($i); 68 | if ($nextTokenAfterArrowPosition === null) { 69 | return false; 70 | } 71 | 72 | /** @var Token $nextToken */ 73 | $nextToken = $this->tokens[$nextTokenAfterArrowPosition]; 74 | 75 | return $nextToken->isGivenKind(self::ARRAY_OPEN_TOKENS); 76 | } 77 | } 78 | 79 | return false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/TokenRunner/ValueObjectFactory/LineLengthAndPositionFactory.php: -------------------------------------------------------------------------------- 1 | $tokens 16 | */ 17 | public function createFromTokensAndLineStartPosition(Tokens $tokens, int $currentPosition): LineLengthAndPosition 18 | { 19 | $length = 0; 20 | 21 | while (! $this->isNewLineOrOpenTag($tokens, $currentPosition)) { 22 | // in case of multiline string, we are interested in length of the part on current line only 23 | if (! isset($tokens[$currentPosition])) { 24 | throw new TokenNotFoundException($currentPosition); 25 | } 26 | 27 | /** @var Token $currentToken */ 28 | $currentToken = $tokens[$currentPosition]; 29 | 30 | $explode = explode("\n", $currentToken->getContent()); 31 | // string precedes current token, so we are interested in end part only 32 | $lastSection = end($explode); 33 | $length += strlen($lastSection); 34 | 35 | --$currentPosition; 36 | 37 | if (count($explode) > 1) { 38 | // no longer need to continue searching for newline 39 | break; 40 | } 41 | 42 | if (! isset($tokens[$currentPosition])) { 43 | break; 44 | } 45 | } 46 | 47 | return new LineLengthAndPosition($length, $currentPosition); 48 | } 49 | 50 | /** 51 | * @param Tokens $tokens 52 | */ 53 | private function isNewLineOrOpenTag(Tokens $tokens, int $position): bool 54 | { 55 | if (! isset($tokens[$position])) { 56 | throw new TokenNotFoundException($position); 57 | } 58 | 59 | if (\str_starts_with($tokens[$position]->getContent(), "\n")) { 60 | return true; 61 | } 62 | 63 | return $tokens[$position]->isGivenKind(T_OPEN_TAG); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/TokenRunner/Whitespace/IndentResolver.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | */ 23 | public function resolveClosingBracketNewlineWhitespace(Tokens $tokens, int $startIndex): string 24 | { 25 | $indentLevel = $this->indentDetector->detectOnPosition($tokens, $startIndex); 26 | return $this->whitespacesFixerConfig->getLineEnding() . str_repeat( 27 | $this->whitespacesFixerConfig->getIndent(), 28 | $indentLevel 29 | ); 30 | } 31 | 32 | /** 33 | * @param Tokens $tokens 34 | */ 35 | public function resolveCurrentNewlineIndentWhitespace(Tokens $tokens, int $index): string 36 | { 37 | $indentLevel = $this->indentDetector->detectOnPosition($tokens, $index); 38 | $indentWhitespace = str_repeat($this->whitespacesFixerConfig->getIndent(), $indentLevel); 39 | 40 | return $this->whitespacesFixerConfig->getLineEnding() . $indentWhitespace; 41 | } 42 | 43 | /** 44 | * @param Tokens $tokens 45 | */ 46 | public function resolveNewlineIndentWhitespace(Tokens $tokens, int $index): string 47 | { 48 | $indentWhitespace = $this->resolveIndentWhitespace($tokens, $index); 49 | return $this->whitespacesFixerConfig->getLineEnding() . $indentWhitespace; 50 | } 51 | 52 | /** 53 | * @param Tokens $tokens 54 | */ 55 | private function resolveIndentWhitespace(Tokens $tokens, int $index): string 56 | { 57 | $indentLevel = $this->indentDetector->detectOnPosition($tokens, $index); 58 | return str_repeat($this->whitespacesFixerConfig->getIndent(), $indentLevel + 1); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/TokenRunner/Wrapper/FixerWrapper/ArrayWrapperFactory.php: -------------------------------------------------------------------------------- 1 | $tokens 22 | */ 23 | public function createFromTokensAndBlockInfo(Tokens $tokens, BlockInfo $blockInfo): ArrayWrapper 24 | { 25 | return new ArrayWrapper($tokens, $blockInfo, $this->tokenSkipper); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ValueObject/BlockInfoMetadata.php: -------------------------------------------------------------------------------- 1 | blockType; 20 | } 21 | 22 | public function getBlockInfo(): BlockInfo 23 | { 24 | return $this->blockInfo; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ValueObject/CodingStandardConfig.php: -------------------------------------------------------------------------------- 1 |