├── .github └── workflows │ └── run-checks-tests.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── ecs.php ├── examples ├── Type.php └── ValidClass.php ├── extension.neon ├── phpunit.xml ├── src ├── CsFixer │ ├── ForbiddenDumpFixer.php │ ├── ForbiddenPrivateVisibilityFixer.php │ ├── MissingButtonTypeFixer.php │ ├── OrmJoinColumnRequireNullableFixer.php │ └── Phpdoc │ │ ├── AbstractMissingAnnotationsFixer.php │ │ ├── InheritDocFormatFixer.php │ │ ├── MissingParamAnnotationsFixer.php │ │ └── MissingReturnAnnotationFixer.php ├── Exception │ └── NamespaceNotFoundException.php ├── Finder │ └── FileFinder.php ├── Helper │ ├── CyclomaticComplexitySniffSetting.php │ ├── FqnNameResolver.php │ ├── Naming.php │ ├── PhpToDocTypeTransformer.php │ └── PhpdocRegex.php ├── Phpstan │ ├── EntityDataObjectPropertyHasNoTypehintRule.php │ ├── EntityShouldHaveFactoryRule.php │ ├── InjectedPropertiesInTestsExtension.php │ ├── OrmPropertyGetterAndSetterHasNoTypehintRule.php │ └── OrmPropertyHasNoTypehintRule.php ├── SetList │ └── SetList.php └── Sniffs │ ├── ConstantVisibilityRequiredSniff.php │ ├── ForbiddenDoctrineDefaultValueSniff.php │ ├── ForbiddenDoctrineInheritanceSniff.php │ ├── ForbiddenDumpSniff.php │ ├── ForbiddenExitSniff.php │ ├── ForbiddenSuperGlobalSniff.php │ ├── ForceLateStaticBindingForProtectedConstantsSniff.php │ ├── ObjectIsCreatedByFactorySniff.php │ ├── RequireOverrideAttributeSniff.php │ └── ValidVariableNameSniff.php └── tests ├── Unit ├── CsFixer │ ├── AbstractFixerTestCase.php │ ├── ChainedFixer.php │ ├── Constraint │ │ └── IsIdenticalString.php │ ├── ForbiddenDumpFixer │ │ ├── ForbiddenDumpFixerTest.php │ │ ├── correct │ │ │ └── correct.html.twig │ │ ├── fixed │ │ │ └── fixed.html.twig │ │ └── wrong │ │ │ └── wrong.html.twig │ ├── ForbiddenPrivateVisibilityFixer │ │ ├── ForbiddenPrivateVisibilityFixerTest.php │ │ ├── correct │ │ │ ├── correct-final.php │ │ │ ├── correct.php │ │ │ └── ignored-namespace.php │ │ ├── fixed │ │ │ ├── constructor_property_promotion.php │ │ │ └── fixed.php │ │ └── wrong │ │ │ ├── constructor_property_promotion.php │ │ │ └── wrong.php │ ├── MissingButtonTypeFixer │ │ ├── MissingButtonTypeFixerTest.php │ │ ├── correct │ │ │ └── correct.html.twig │ │ ├── fixed │ │ │ └── fixed.html.twig │ │ └── wrong │ │ │ └── wrong.html.twig │ ├── OrmJoinColumnRequireNullableFixer │ │ ├── OrmJoinColumnRequireNullableFixerTest.php │ │ ├── correct │ │ │ ├── many_to_one_missing_join_column.php │ │ │ ├── many_to_one_missing_nullable_param.php │ │ │ ├── one_to_many.php │ │ │ └── one_to_one_missing_nullable_param.php │ │ ├── fixed │ │ │ ├── many_to_one_missing_join_column.php │ │ │ ├── many_to_one_missing_nullable_param.php │ │ │ ├── one_to_one_missing_join_column.php │ │ │ ├── one_to_one_missing_nullable_param.php │ │ │ └── one_to_one_multiline_missing_nullable_param.php │ │ └── wrong │ │ │ ├── many_to_one_missing_join_column.php │ │ │ ├── many_to_one_missing_nullable_param.php │ │ │ ├── one_to_one_missing_join_column.php │ │ │ ├── one_to_one_missing_nullable_param.php │ │ │ └── one_to_one_multiline_missing_nullable_param.php │ └── Phpdoc │ │ ├── FunctionAnnotationFixer │ │ ├── FunctionAnnotationFixerTest.php │ │ ├── Source │ │ │ ├── NamespacedType.php │ │ │ └── Naming.php │ │ ├── correct │ │ │ └── correct.php │ │ ├── fixed │ │ │ ├── fixed.php │ │ │ ├── fixed2.php │ │ │ └── fixed3.php │ │ └── wrong │ │ │ ├── wrong.php │ │ │ ├── wrong2.php │ │ │ └── wrong3.php │ │ ├── InheritDocFormatFixer │ │ ├── InheritDocFormatFixerTest.php │ │ ├── base │ │ │ └── base.php │ │ ├── fixed │ │ │ └── fixed.php │ │ └── wrong │ │ │ └── wrong.php │ │ ├── MissingParamAnnotationsFixer │ │ ├── MissingParamAnnotationsFixerTest.php │ │ ├── Source │ │ │ ├── NamespacedType.php │ │ │ └── Naming.php │ │ ├── correct │ │ │ └── correct.php │ │ ├── fixed │ │ │ ├── fixed.php │ │ │ ├── fixed2.php │ │ │ ├── fixed3.php │ │ │ ├── fixed4.php │ │ │ ├── fixed5.php │ │ │ ├── fixed6.php │ │ │ ├── fixed7.php │ │ │ └── fixed8.php │ │ └── wrong │ │ │ ├── wrong.php │ │ │ ├── wrong2.php │ │ │ ├── wrong3.php │ │ │ ├── wrong4.php │ │ │ ├── wrong5.php │ │ │ ├── wrong6.php │ │ │ ├── wrong7.php │ │ │ └── wrong8.php │ │ └── MissingReturnAnnotationFixer │ │ ├── MissingReturnAnnotationFixerTest.php │ │ ├── Source │ │ └── NamespacedType.php │ │ ├── correct │ │ └── correct.php │ │ ├── fixed │ │ ├── fixed.php │ │ ├── fixed2.php │ │ └── fixed3.php │ │ └── wrong │ │ ├── wrong.php │ │ ├── wrong2.php │ │ └── wrong3.php └── Sniffs │ ├── AbstractSniffTestCase.php │ ├── ConstantVisibilityRequiredSniff │ ├── ConstantVisibilityRequiredSniffTest.php │ ├── correct │ │ ├── Annotation.php │ │ ├── MixedVisibilities.php │ │ ├── MultipleValues.php │ │ ├── OutsideClass.php │ │ ├── SingleValueAfterMethodWithoutNamespace.php │ │ ├── SingleValueWithoutNamespace.php │ │ ├── Various.php │ │ └── noClass.php │ └── wrong │ │ ├── MissingAnnotation.php │ │ ├── MixedAtTheEnd.php │ │ ├── MixedInTheMiddle.php │ │ ├── SingleValue.php │ │ ├── SingleValueAfterMethodWithoutNamespace.php │ │ └── Various.php │ ├── ForbiddenDoctrineDefaultValueSniff │ ├── ForbiddenDoctrineDefaultValueSniffTest.php │ ├── correct │ │ ├── invalid_docblock_annotation.php │ │ └── missing_default_value_annotation.php │ └── wrong │ │ ├── default_value_annotation.php │ │ ├── different_order_annotation.php │ │ ├── multiline_annotation.php │ │ ├── spaces_around_annotation.php │ │ └── split_annotation.php │ ├── ForbiddenDoctrineInheritanceSniff │ ├── Correct │ │ ├── ClassWithoutComment.php │ │ ├── EntityWithoutInheritanceMapping.php │ │ └── fileWithoutClass.php │ ├── ForbiddenDoctrineInheritanceSniffTest.php │ └── Wrong │ │ ├── ClassWithFullNamespaceInheritanceMapping.php │ │ └── EntityWithOrmInheritanceMapping.php │ ├── ForbiddenDumpSniff │ ├── ForbiddenDumpSniffTest.php │ └── wrong │ │ ├── d.php.inc │ │ ├── dump.php.inc │ │ ├── print_r.php.inc │ │ ├── var_dump.php.inc │ │ └── var_export.php.inc │ ├── ForbiddenExitSniff │ ├── ForbiddenExitSniffTest.php │ └── wrong │ │ └── wrong.php.inc │ ├── ForbiddenSuperGlobalSniff │ ├── ForbiddenSuperGlobalSniffTest.php │ └── wrong │ │ ├── env.php.inc │ │ └── post.php.inc │ ├── ForceLateStaticBindingForProtectedConstantsSniff │ ├── ForceLateStaticBindingForProtectedConstantsSniffTest.php │ ├── fixed │ │ ├── SelfWithMethodsAndVariables.php │ │ └── SingleValue.php │ └── wrong │ │ ├── SelfWithMethodsAndVariables.php │ │ └── SingleValue.php │ ├── ObjectIsCreatedByFactorySniff │ ├── Correct │ │ └── PostFactory.php │ ├── ObjectIsCreatedByFactorySniffTest.php │ └── Wrong │ │ ├── PostFactory.php │ │ └── SomeController.php │ └── ValidVariableNameSniff │ ├── ValidVariableNameSniffTest.php │ └── wrong │ └── wrong.inc ├── autoload.php └── config.yaml /.github/workflows/run-checks-tests.yaml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | concurrency: 3 | group: ${{ github.ref }} 4 | cancel-in-progress: true 5 | name: "Checks and tests" 6 | jobs: 7 | checks-and-tests: 8 | name: Run checks and tests in PHP ${{ matrix.php-versions }} ${{ matrix.composer-prefered-dependencies }} 9 | runs-on: ubuntu-22.04 10 | strategy: 11 | matrix: 12 | php-versions: ['8.3'] 13 | composer-prefered-dependencies: ['--prefer-lowest', ''] 14 | fail-fast: false 15 | steps: 16 | - name: Sleep for 15 seconds to ensure that split packages has been promoted to packagist.org 17 | run: sleep 15s 18 | shell: bash 19 | - name: GIT checkout branch - ${{ github.ref }} 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.ref }} 23 | - name: Install PHP, extensions and tools 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php-versions }} 27 | extensions: bcmath, gd, intl, pdo_pgsql, redis, pgsql, zip 28 | tools: composer 29 | - name: Install Composer dependencies 30 | run: composer update --optimize-autoloader --no-interaction ${{ matrix.composer-prefered-dependencies }} 31 | - name: Run PHPUnit 32 | run: php vendor/bin/phpunit tests 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /composer.lock 3 | /nbproject 4 | /vendor 5 | /.phpunit.cache 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your contributions to Shopsys Coding Standards package. 4 | Together we are making Shopsys Platform better. 5 | 6 | This repository is READ-ONLY. 7 | If you want to [report issues](https://github.com/shopsys/shopsys/issues/new) and/or send [pull requests](https://github.com/shopsys/shopsys/compare), 8 | please use the main [Shopsys repository](https://github.com/shopsys/shopsys). 9 | 10 | Please check our [Contribution Guide](https://github.com/shopsys/shopsys/blob/HEAD/CONTRIBUTING.md) before contributing. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2023 Shopsys s.r.o., http://www.shopsys.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 11 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 12 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 14 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopsys Coding Standards 2 | 3 | [![Downloads](https://img.shields.io/packagist/dt/shopsys/coding-standards.svg)](https://packagist.org/packages/shopsys/coding-standards) 4 | 5 | Shopsys Coding Standards are based on [PSR-2](http://www.php-fig.org/psr/psr-2/). 6 | 7 | This project bundles tools along with predefined rulesets for automated checks of Shopsys Coding Standards that we use in many Shopsys projects. 8 | The repository also contains [few custom rules](#custom-rules). 9 | 10 | This repository is maintained by [shopsys/shopsys] monorepo, information about changes is in its `CHANGELOG` file. 11 | 12 | Provided tools: 13 | 14 | - [PHP-Parallel-Lint](https://github.com/JakubOnderka/PHP-Parallel-Lint) 15 | - [EasyCodingStandard](https://github.com/Symplify/EasyCodingStandard) that combines [PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) and [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) 16 | 17 | For further information see official documentation of those tools. 18 | 19 | ## Installation 20 | 21 | ```bash 22 | php composer require shopsys/coding-standards 23 | ``` 24 | 25 | ## Usage 26 | 27 | Create `ecs.php` config file in your project which includes predefined ruleset. 28 | You can take [the config from the project-base](https://github.com/shopsys/shopsys/blob/HEAD/project-base/app/ecs.php) repository as an inspiration. 29 | You can also customize the rules and even add your own sniffs and fixers in the config. 30 | 31 | In terminal, run following commands: 32 | 33 | ```bash 34 | php vendor/bin/parallel-lint /path/to/project 35 | php vendor/bin/ecs check /path/to/project --config=/path/to/project/custom-coding-standard.yml 36 | ``` 37 | 38 | ## Custom rules 39 | 40 | ### Rules for [PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) 41 | 42 | #### `Shopsys/missing_button_type` 43 | 44 | All `'), 26 | new CodeSample(""), 27 | ], 28 | ); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function isCandidate(Tokens $tokens): bool 35 | { 36 | return true; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function isRisky(): bool 43 | { 44 | return false; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function fix(SplFileInfo $file, Tokens $tokens): void 51 | { 52 | $code = preg_replace_callback( 53 | '@()@imsu', 54 | function ($matches) { 55 | $beginning = $matches[1]; 56 | $attributes = $matches[2]; 57 | $end = $matches[3]; 58 | 59 | if (!preg_match('@(?:^|\s+)type=@', $attributes)) { 60 | $attributes .= ' type="button"'; 61 | } 62 | 63 | return $beginning . $attributes . $end; 64 | }, 65 | $tokens->generateCode(), 66 | ); 67 | 68 | $tokens->setCode($code); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function getName(): string 75 | { 76 | return 'Shopsys/missing_button_type'; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function getPriority(): int 83 | { 84 | return 0; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function supports(SplFileInfo $file): bool 91 | { 92 | return preg_match('/\.html(?:\.twig)?$/ui', $file->getFilename()) === 1; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/CsFixer/OrmJoinColumnRequireNullableFixer.php: -------------------------------------------------------------------------------- 1 | findGivenKind(T_DOC_COMMENT) as $index => $token) { 69 | $doc = new DocBlock($token->getContent()); 70 | 71 | foreach ($doc->getAnnotations() as $annotation) { 72 | if ($this->isRelationAnnotation($annotation)) { 73 | $this->fixRelationAnnotation($doc, $annotation); 74 | $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]); 75 | } 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getName(): string 84 | { 85 | return 'Shopsys/orm_join_column_require_nullable'; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getPriority(): int 92 | { 93 | return 0; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function supports(SplFileInfo $file): bool 100 | { 101 | return preg_match('/\.php$/ui', $file->getFilename()) === 1; 102 | } 103 | 104 | /** 105 | * @param \PhpCsFixer\DocBlock\Annotation $annotation 106 | * @return bool 107 | */ 108 | private function isRelationAnnotation(Annotation $annotation): bool 109 | { 110 | return preg_match('~@ORM\\\(ManyToOne|OneToOne)\\(~', $annotation->getContent()) === 1; 111 | } 112 | 113 | /** 114 | * @param \PhpCsFixer\DocBlock\DocBlock $doc 115 | * @param \PhpCsFixer\DocBlock\Annotation $relationAnnotation 116 | */ 117 | private function fixRelationAnnotation(DocBlock $doc, Annotation $relationAnnotation): void 118 | { 119 | $joinColumnAnnotation = $this->findJoinColumnAnnotation($doc); 120 | 121 | if ($joinColumnAnnotation === null) { 122 | $this->addJoinColumnAnnotation($doc, $relationAnnotation); 123 | } elseif (preg_match('~(,|\\(|\\*\\s)\\s*nullable\\s*=~', $joinColumnAnnotation->getContent()) !== 1) { 124 | $this->extendJoinColumnAnnotation($doc, $joinColumnAnnotation); 125 | } 126 | } 127 | 128 | /** 129 | * @param \PhpCsFixer\DocBlock\DocBlock $doc 130 | * @return \PhpCsFixer\DocBlock\Annotation|null 131 | */ 132 | private function findJoinColumnAnnotation(DocBlock $doc): ?Annotation 133 | { 134 | foreach ($doc->getAnnotations() as $annotation) { 135 | if (preg_match('~@ORM\\\JoinColumn\\(~', $annotation->getContent()) === 1) { 136 | return $annotation; 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | 143 | /** 144 | * @param \PhpCsFixer\DocBlock\DocBlock $doc 145 | * @param \PhpCsFixer\DocBlock\Annotation $relationAnnotation 146 | */ 147 | private function addJoinColumnAnnotation(DocBlock $doc, Annotation $relationAnnotation): void 148 | { 149 | $matches = null; 150 | preg_match_all('~\\s*\*~', $relationAnnotation->getContent(), $matches); 151 | $lastLine = $doc->getLine($relationAnnotation->getEnd()); 152 | $lastLine->setContent($lastLine->getContent() . $matches[0][0] . ' @ORM\JoinColumn(nullable=false)' . "\n"); 153 | } 154 | 155 | /** 156 | * @param \PhpCsFixer\DocBlock\DocBlock $doc 157 | * @param \PhpCsFixer\DocBlock\Annotation $joinColumnAnnotation 158 | */ 159 | private function extendJoinColumnAnnotation(DocBlock $doc, Annotation $joinColumnAnnotation): void 160 | { 161 | $firstLine = $doc->getLine($joinColumnAnnotation->getStart()); 162 | 163 | if (preg_match('~\\)\\s*$~', $firstLine->getContent()) === 1) { 164 | $firstLine->setContent(preg_replace( 165 | '~(@ORM\\\JoinColumn\\()~', 166 | '$1nullable=false, ', 167 | $firstLine->getContent(), 168 | )); 169 | } else { 170 | $matches = null; 171 | preg_match_all('~\\s*\*~', $joinColumnAnnotation->getContent(), $matches); 172 | $newText = "\n" . $matches[0][0] . ' nullable=false,'; 173 | $firstLine->setContent( 174 | preg_replace('~(@ORM\\\JoinColumn\\()~', '$1' . $newText, $firstLine->getContent()), 175 | ); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/CsFixer/Phpdoc/AbstractMissingAnnotationsFixer.php: -------------------------------------------------------------------------------- 1 | count() - 1; 50 | 51 | for ($index = $limit; $index > 0; --$index) { 52 | $token = $tokens[$index]; 53 | 54 | if (!$token->isGivenKind(T_FUNCTION)) { 55 | continue; 56 | } 57 | 58 | if ($this->shouldSkipFunctionToken($tokens, $index)) { 59 | continue; 60 | } 61 | 62 | $docToken = $this->getDocToken($tokens, $index); 63 | 64 | if ($docToken !== null && $this->shouldSkipDocToken($docToken)) { 65 | continue; 66 | } 67 | 68 | $this->processFunctionToken($tokens, $index, $docToken); 69 | } 70 | } 71 | 72 | /** 73 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 74 | * @param int $index 75 | * @param \PhpCsFixer\Tokenizer\Token|null $docToken 76 | */ 77 | abstract protected function processFunctionToken(Tokens $tokens, int $index, ?Token $docToken): void; 78 | 79 | /** 80 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 81 | * @return bool 82 | */ 83 | public function isCandidate(Tokens $tokens): bool 84 | { 85 | return $tokens->isTokenKindFound(T_FUNCTION); 86 | } 87 | 88 | /** 89 | * @return bool 90 | */ 91 | public function isRisky(): bool 92 | { 93 | return false; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function getName(): string 100 | { 101 | return static::class; 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function getPriority(): int 108 | { 109 | return 0; 110 | } 111 | 112 | /** 113 | * @param \SplFileInfo $file 114 | * @return bool 115 | */ 116 | public function supports(SplFileInfo $file): bool 117 | { 118 | return (bool)Strings::match($file->getFilename(), '#\.php$#ui'); 119 | } 120 | 121 | /** 122 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 123 | * @param int $index 124 | * @return bool 125 | */ 126 | protected function shouldSkipFunctionToken(Tokens $tokens, int $index): bool 127 | { 128 | $nextTokenPosition = $tokens->getNextMeaningfulToken($index); 129 | 130 | // anonymous functions 131 | return !$tokens[$nextTokenPosition]->isGivenKind(T_STRING); 132 | } 133 | 134 | /** 135 | * @param \PhpCsFixer\Tokenizer\Token $docToken 136 | * @return bool 137 | */ 138 | protected function shouldSkipDocToken(Token $docToken): bool 139 | { 140 | if (stripos($docToken->getContent(), 'inheritdoc') !== false) { 141 | return true; 142 | } 143 | 144 | // ignore one-line phpdocs like `/** foo */`, as there is no place to put new annotations 145 | return strpos($docToken->getContent(), "\n") === false; 146 | } 147 | 148 | /** 149 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 150 | * @param int $index 151 | * @return string 152 | */ 153 | protected function resolveIndent(Tokens $tokens, int $index): string 154 | { 155 | return str_repeat( 156 | $this->whitespacesFixerConfig->getIndent(), 157 | $this->indentDetector->detectOnPosition($tokens, $index), 158 | ); 159 | } 160 | 161 | /** 162 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 163 | * @param int $index 164 | * @return int 165 | */ 166 | protected function getDocIndex(Tokens $tokens, int $index): int 167 | { 168 | do { 169 | $index = $tokens->getPrevNonWhitespace($index); 170 | 171 | $index = $this->skipAttributes($tokens, $index); 172 | } while ($tokens[$index]->isGivenKind( 173 | [T_STATIC, T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT, T_ATTRIBUTE, CT::T_ATTRIBUTE_CLOSE], 174 | )); 175 | 176 | return $index; 177 | } 178 | 179 | /** 180 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 181 | * @param int $index 182 | * @return int 183 | */ 184 | public function skipAttributes(Tokens $tokens, int $index): int 185 | { 186 | if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { 187 | $depth = 1; 188 | 189 | while ($depth > 0 && $index > 0) { 190 | $index--; 191 | 192 | if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { 193 | $depth++; 194 | } elseif ($tokens[$index]->isGivenKind(T_ATTRIBUTE)) { 195 | $depth--; 196 | } 197 | } 198 | } 199 | 200 | return $index; 201 | } 202 | 203 | /** 204 | * @param \PhpCsFixer\DocBlock\Line[] $newLines 205 | * @param string $indent 206 | * @return string 207 | */ 208 | protected function createDocContentFromLinesAndIndent(array $newLines, string $indent): string 209 | { 210 | $lines = []; 211 | $lines[] = '/**' . $this->whitespacesFixerConfig->getLineEnding(); 212 | $lines = array_merge($lines, $newLines); 213 | $lines[] = $indent . ' */'; 214 | 215 | return implode('', $lines); 216 | } 217 | 218 | /** 219 | * @param \PhpCsFixer\Tokenizer\Token $docToken 220 | * @param \PhpCsFixer\DocBlock\Line[] $newLines 221 | * @return string 222 | */ 223 | protected function createDocContentFromDocTokenAndNewLines(Token $docToken, array $newLines): string 224 | { 225 | $doc = new DocBlock($docToken->getContent()); 226 | $lines = $doc->getLines(); 227 | 228 | array_splice( 229 | $lines, 230 | $this->resolveOffset($docToken, $newLines), 231 | 0, 232 | $newLines, 233 | ); 234 | 235 | return implode('', $lines); 236 | } 237 | 238 | /** 239 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 240 | * @param int $index 241 | * @return int 242 | */ 243 | protected function getNewDocIndex(Tokens $tokens, int $index): int 244 | { 245 | for ($i = $index; $i > 0; --$i) { 246 | if ($this->isWhitespaceWithNewline($tokens, $i)) { 247 | if (!$tokens[$i - 1]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { 248 | return $i + 1; 249 | } 250 | 251 | return $this->skipAttributes($tokens, $i - 1); 252 | } 253 | } 254 | 255 | return $index; 256 | } 257 | 258 | /** 259 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 260 | * @param int $index 261 | * @param \PhpCsFixer\Tokenizer\Token $docToken 262 | * @param \PhpCsFixer\DocBlock\Line[] $newLines 263 | */ 264 | protected function updateDocWithLines(Tokens $tokens, int $index, Token $docToken, array $newLines): void 265 | { 266 | $docBlockIndex = $this->getDocIndex($tokens, $index); 267 | $docContent = $this->createDocContentFromDocTokenAndNewLines($docToken, $newLines); 268 | 269 | $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $docContent]); 270 | } 271 | 272 | /** 273 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 274 | * @param int $index 275 | * @param \PhpCsFixer\DocBlock\Line[] $newLines 276 | * @param string $indent 277 | */ 278 | protected function addDocWithLines(Tokens $tokens, int $index, array $newLines, string $indent): void 279 | { 280 | $docBlockIndex = $this->getNewDocIndex($tokens, $index); 281 | $docContent = $this->createDocContentFromLinesAndIndent($newLines, $indent); 282 | 283 | $tokens->insertAt($docBlockIndex, new Token([T_DOC_COMMENT, $docContent])); 284 | $whitespaceAfterDocBlock = $this->whitespacesFixerConfig->getLineEnding() . $indent; 285 | $tokens->ensureWhitespaceAtIndex($docBlockIndex, 1, $whitespaceAfterDocBlock); 286 | } 287 | 288 | /** 289 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 290 | * @param int $index 291 | * @return \PhpCsFixer\Tokenizer\Token|null 292 | */ 293 | private function getDocToken(Tokens $tokens, int $index): ?Token 294 | { 295 | $docIndex = $this->getDocIndex($tokens, $index); 296 | $docToken = $tokens[$docIndex]; 297 | 298 | if ($docToken->isGivenKind(T_DOC_COMMENT)) { 299 | return $docToken; 300 | } 301 | 302 | return null; 303 | } 304 | 305 | /** 306 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 307 | * @param int $index 308 | * @return bool 309 | */ 310 | private function isWhitespaceWithNewline(Tokens $tokens, int $index): bool 311 | { 312 | if (!$tokens[$index]->isWhitespace()) { 313 | return false; 314 | } 315 | 316 | $content = $tokens[$index]->getContent(); 317 | 318 | return Strings::contains($content, $this->whitespacesFixerConfig->getLineEnding()); 319 | } 320 | 321 | /** 322 | * @param \PhpCsFixer\Tokenizer\Token $docToken 323 | * @param \PhpCsFixer\DocBlock\Line[] $newLines 324 | * @return int 325 | */ 326 | private function resolveOffset(Token $docToken, array $newLines): int 327 | { 328 | foreach ($newLines as $newLine) { 329 | if ( 330 | Strings::contains($newLine->getContent(), '@param') 331 | && Strings::contains($docToken->getContent(), '@param') 332 | ) { 333 | return $this->getLastParamLinePosition($docToken) + 1; 334 | } 335 | } 336 | 337 | $doc = new DocBlock($docToken->getContent()); 338 | 339 | return count($doc->getLines()) - 1; 340 | } 341 | 342 | /** 343 | * @param \PhpCsFixer\Tokenizer\Token $docToken 344 | * @return int|null 345 | */ 346 | private function getLastParamLinePosition(Token $docToken): ?int 347 | { 348 | $doc = new DocBlock($docToken->getContent()); 349 | 350 | $lastParamLine = null; 351 | 352 | foreach ($doc->getAnnotationsOfType('param') as $annotation) { 353 | $lastParamLine = max($lastParamLine, $annotation->getEnd()); 354 | } 355 | 356 | return $lastParamLine; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/CsFixer/Phpdoc/InheritDocFormatFixer.php: -------------------------------------------------------------------------------- 1 | findGivenKind(T_DOC_COMMENT) as $index => $token) { 60 | $doc = new DocBlock($token->getContent()); 61 | 62 | foreach ($doc->getLines() as $line) { 63 | if ($this->isInheritDocCandidate($line)) { 64 | $this->fixInheritDoc($line); 65 | 66 | $tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]); 67 | } 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getName(): string 76 | { 77 | return 'Shopsys/inherit_doc_format'; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getPriority(): int 84 | { 85 | return 0; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function supports(SplFileInfo $file): bool 92 | { 93 | return preg_match('/\.php$/ui', $file->getFilename()) === 1; 94 | } 95 | 96 | /** 97 | * @param \PhpCsFixer\DocBlock\Line $line 98 | * @return bool 99 | */ 100 | private function isInheritDocCandidate(Line $line): bool 101 | { 102 | return preg_match('~\{?@[Ii]nherit[dD]oc}?~', $line->getContent()) === 1; 103 | } 104 | 105 | /** 106 | * @param \PhpCsFixer\DocBlock\Line $line 107 | */ 108 | private function fixInheritDoc(Line $line): void 109 | { 110 | $line->setContent( 111 | preg_replace( 112 | '~\{?@[Ii]nherit[dD]oc}?~', 113 | '{@inheritdoc}', 114 | $line->getContent(), 115 | ), 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/CsFixer/Phpdoc/MissingParamAnnotationsFixer.php: -------------------------------------------------------------------------------- 1 | functionsAnalyzer->getFunctionArguments($tokens, $index); 38 | 39 | if (count($argumentAnalyses) === 0) { 40 | return; 41 | } 42 | 43 | if ($docToken !== null) { 44 | $argumentAnalyses = $this->filterArgumentAnalysesFromExistingParamAnnotations( 45 | $argumentAnalyses, 46 | $docToken, 47 | ); 48 | } 49 | 50 | // all arguments have annotations → skip 51 | if (count($argumentAnalyses) === 0) { 52 | return; 53 | } 54 | 55 | $indent = $this->resolveIndent($tokens, $index); 56 | 57 | $newLines = $this->createParamLinesFromArgumentAnalyses($tokens, $argumentAnalyses, $indent); 58 | 59 | if ($docToken !== null) { 60 | $this->updateDocWithLines($tokens, $index, $docToken, $newLines); 61 | 62 | return; 63 | } 64 | 65 | $this->addDocWithLines($tokens, $index, $newLines, $indent); 66 | } 67 | 68 | /** 69 | * @param \PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis[] $argumentAnalyses 70 | * @param \PhpCsFixer\Tokenizer\Token $docToken 71 | * @return array 72 | */ 73 | private function filterArgumentAnalysesFromExistingParamAnnotations( 74 | array $argumentAnalyses, 75 | Token $docToken, 76 | ): array { 77 | $doc = new DocBlock($docToken->getContent()); 78 | 79 | foreach ($doc->getAnnotationsOfType('param') as $annotation) { 80 | $matches = Strings::match($annotation->getContent(), PhpdocRegex::ARGUMENT_NAME_PATTERN); 81 | 82 | if (isset($matches[1])) { 83 | unset($argumentAnalyses[$matches[1]]); 84 | } 85 | } 86 | 87 | return $argumentAnalyses; 88 | } 89 | 90 | /** 91 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 92 | * @param \PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis $argumentAnalyses 93 | * @param string $indent 94 | * @return \PhpCsFixer\DocBlock\Line[] 95 | */ 96 | private function createParamLinesFromArgumentAnalyses( 97 | Tokens $tokens, 98 | array $argumentAnalyses, 99 | string $indent, 100 | ): array { 101 | $lines = []; 102 | 103 | foreach ($argumentAnalyses as $argumentAnalysis) { 104 | $type = $this->phpToDocTypeTransformer->transform( 105 | $tokens, 106 | $argumentAnalysis->getTypeAnalysis(), 107 | $argumentAnalysis->getDefault(), 108 | ); 109 | 110 | $lines[] = new Line(sprintf( 111 | '%s * @param %s %s%s', 112 | $indent, 113 | $type ?: 'mixed', 114 | $argumentAnalysis->getName(), 115 | $this->whitespacesFixerConfig->getLineEnding(), 116 | )); 117 | } 118 | 119 | return $lines; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/CsFixer/Phpdoc/MissingReturnAnnotationFixer.php: -------------------------------------------------------------------------------- 1 | functionsAnalyzer->getFunctionReturnType($tokens, $index); 36 | $type = $this->phpToDocTypeTransformer->transform($tokens, $returnTypeAnalysis); 37 | 38 | if ($this->shouldSkip($type, $docToken)) { 39 | return; 40 | } 41 | 42 | $indent = $this->resolveIndent($tokens, $index); 43 | $newLine = new Line(sprintf( 44 | '%s * @return %s%s', 45 | $indent, 46 | $type, 47 | $this->whitespacesFixerConfig->getLineEnding(), 48 | )); 49 | 50 | if ($docToken !== null) { 51 | $this->updateDocWithLines($tokens, $index, $docToken, [$newLine]); 52 | 53 | return; 54 | } 55 | 56 | $this->addDocWithLines($tokens, $index, [$newLine], $indent); 57 | } 58 | 59 | /** 60 | * @param string $type 61 | * @param \PhpCsFixer\Tokenizer\Token|null $docToken 62 | * @return bool 63 | */ 64 | private function shouldSkip(string $type, ?Token $docToken): bool 65 | { 66 | if (in_array($type, ['', 'void', 'mixed', 'never'], true)) { 67 | return true; 68 | } 69 | 70 | return $docToken && Strings::contains($docToken->getContent(), '@return'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Exception/NamespaceNotFoundException.php: -------------------------------------------------------------------------------- 1 | getPath()] = new SymfonySplFileInfo( 28 | $singleSource, 29 | $fileInfo->getPath(), 30 | $fileInfo->getPathname(), 31 | ); 32 | } else { 33 | $directories[] = $singleSource; 34 | } 35 | } 36 | 37 | $finder = Finder::create()->files() 38 | ->name('#\.(twig|html(\.twig)?|php|md)$#') 39 | ->in($directories); 40 | 41 | // ArrayIterator will be fixed in new release 42 | $finder->append(new ArrayIterator($files)); 43 | 44 | return $finder; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Helper/CyclomaticComplexitySniffSetting.php: -------------------------------------------------------------------------------- 1 | matchUseImports($tokens, $className); 36 | 37 | if ($matchedClassName !== null) { 38 | return $matchedClassName; 39 | } 40 | 41 | if ($this->hasNamespace($tokens)) { 42 | return $this->getNamespaceAsString($tokens) . '\\' . $className; 43 | } 44 | 45 | // no namespace, return the class 46 | return $className; 47 | } 48 | 49 | /** 50 | * Tries to match names against use imports, e.g. "SomeClass" returns "SomeNamespace\SomeClass" for: 51 | * 52 | * use SomeNamespace\AnotherClass; 53 | * use SomeNamespace\SomeClass; 54 | * 55 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 56 | * @param string $className 57 | * @return string|null 58 | */ 59 | private function matchUseImports(Tokens $tokens, string $className): ?string 60 | { 61 | $namespaceUseAnalyses = $this->namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens); 62 | 63 | foreach ($namespaceUseAnalyses as $namespaceUseAnalysis) { 64 | if ($className === $namespaceUseAnalysis->getShortName()) { 65 | return $namespaceUseAnalysis->getFullName(); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | 72 | /** 73 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 74 | * @return bool 75 | */ 76 | private function hasNamespace(Tokens $tokens): bool 77 | { 78 | return (bool)$tokens->findGivenKind([T_NAMESPACE], 0); 79 | } 80 | 81 | /** 82 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 83 | * @return string 84 | */ 85 | private function getNamespaceAsString(Tokens $tokens): string 86 | { 87 | $namespaceTokens = $tokens->findGivenKind([T_NAMESPACE], 0); 88 | $namespaceToken = array_pop($namespaceTokens); 89 | reset($namespaceToken); 90 | 91 | $namespacePosition = (int)key($namespaceToken); 92 | $namespaceName = ''; 93 | $position = $namespacePosition + 2; 94 | 95 | while ($tokens[$position]->isGivenKind([T_NS_SEPARATOR, T_STRING])) { 96 | $namespaceName .= $tokens[$position]->getContent(); 97 | ++$position; 98 | } 99 | 100 | return $namespaceName; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Helper/Naming.php: -------------------------------------------------------------------------------- 1 | getTokens(); 36 | 37 | $firstNamePart = $tokens[$classNameStartPosition]['content']; 38 | 39 | // is class 40 | if ($this->isClassName($file, $classNameStartPosition)) { 41 | $namespace = NamespaceHelper::findCurrentNamespaceName($file, $classNameStartPosition); 42 | 43 | if ($namespace) { 44 | return $namespace . self::NAMESPACE_SEPARATOR . $firstNamePart; 45 | } 46 | 47 | return $firstNamePart; 48 | } 49 | 50 | $classNameParts = []; 51 | $classNameParts[] = $firstNamePart; 52 | 53 | $nextTokenPointer = $classNameStartPosition + 1; 54 | 55 | while ($tokens[$nextTokenPointer]['code'] === T_NS_SEPARATOR) { 56 | ++$nextTokenPointer; 57 | $classNameParts[] = $tokens[$nextTokenPointer]['content']; 58 | ++$nextTokenPointer; 59 | } 60 | 61 | $completeClassName = implode(self::NAMESPACE_SEPARATOR, $classNameParts); 62 | 63 | $fqnClassName = $this->getFqnClassName($file, $completeClassName, $classNameStartPosition); 64 | 65 | if ($fqnClassName !== '') { 66 | return ltrim($fqnClassName, self::NAMESPACE_SEPARATOR); 67 | } 68 | 69 | return $completeClassName; 70 | } 71 | 72 | /** 73 | * @param \PHP_CodeSniffer\Files\File $file 74 | * @param string $className 75 | * @param int $classTokenPosition 76 | * @return string 77 | */ 78 | private function getFqnClassName(File $file, string $className, int $classTokenPosition): string 79 | { 80 | $referencedNames = $this->getReferencedNames($file); 81 | 82 | foreach ($referencedNames as $referencedName) { 83 | if (isset($this->fqnClassNameByFilePathAndClassName[$file->path][$className])) { 84 | return $this->fqnClassNameByFilePathAndClassName[$file->path][$className]; 85 | } 86 | 87 | $resolvedName = NamespaceHelper::resolveClassName( 88 | $file, 89 | $referencedName->getNameAsReferencedInFile(), 90 | $classTokenPosition, 91 | ); 92 | 93 | if ($referencedName->getNameAsReferencedInFile() === $className) { 94 | $this->fqnClassNameByFilePathAndClassName[$file->path][$className] = $resolvedName; 95 | 96 | return $resolvedName; 97 | } 98 | } 99 | 100 | return ''; 101 | } 102 | 103 | /** 104 | * @param \PHP_CodeSniffer\Files\File $file 105 | * @param int $position 106 | * @return bool 107 | */ 108 | private function isClassName(File $file, int $position): bool 109 | { 110 | return (bool)$file->findPrevious(T_CLASS, $position, max(1, $position - 3)); 111 | } 112 | 113 | /** 114 | * @param \PHP_CodeSniffer\Files\File $file 115 | * @return array 116 | */ 117 | private function getReferencedNames(File $file): array 118 | { 119 | if (isset($this->referencedNamesByFilePath[$file->path])) { 120 | return $this->referencedNamesByFilePath[$file->path]; 121 | } 122 | 123 | $referencedNames = ReferencedNameHelper::getAllReferencedNames($file, 0); 124 | 125 | $this->referencedNamesByFilePath[$file->path] = $referencedNames; 126 | 127 | return $referencedNames; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Helper/PhpToDocTypeTransformer.php: -------------------------------------------------------------------------------- 1 | getName(); 33 | 34 | if (method_exists($typeAnalysis, 'isNullable')) { 35 | $isNullable = $typeAnalysis->isNullable(); 36 | } else { 37 | // backward-compatibility with friendsofphp/php-cs-fixer in <2.14.3 38 | $isNullable = $type[0] === '?'; 39 | } 40 | } 41 | 42 | // nullable parameter or with a default value of null 43 | if ($isNullable || (is_string($default) && strtolower($default) === 'null')) { 44 | return $this->createFromNullable($tokens, $type); 45 | } 46 | 47 | $type = $this->fqnNameResolver->resolve($tokens, $type); 48 | 49 | return $this->preSlashType($type); 50 | } 51 | 52 | /** 53 | * @param string $type 54 | * @return string 55 | */ 56 | private function preSlashType(string $type): string 57 | { 58 | if (Strings::startsWith($type, '\\')) { 59 | return $type; 60 | } 61 | 62 | if (class_exists($type) || interface_exists($type)) { 63 | return '\\' . $type; 64 | } 65 | 66 | return $type; 67 | } 68 | 69 | /** 70 | * @param \PhpCsFixer\Tokenizer\Tokens $tokens 71 | * @param string $type 72 | * @return string 73 | */ 74 | private function createFromNullable(Tokens $tokens, string $type): string 75 | { 76 | // cleanup from "?" 77 | $type = $type[0] === '?' ? substr($type, 1) : $type; 78 | $type = $this->fqnNameResolver->resolve($tokens, $type); 79 | 80 | return $this->preSlashType($type) . '|null'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Helper/PhpdocRegex.php: -------------------------------------------------------------------------------- 1 | getClassReflection()?->getName(), self::CHECKED_NAMESPACE)) { 39 | return []; 40 | } 41 | 42 | if (!$this->isDataObjectWithAssociatedEntity($scope)) { 43 | return []; 44 | } 45 | 46 | if ($node->getNativeType() === null) { 47 | return []; 48 | } 49 | 50 | return [ 51 | RuleErrorBuilder::message(sprintf( 52 | 'Property %s::%s on data object with associated entity, should not have typehint.', 53 | $scope->getClassReflection()?->getDisplayName(), 54 | $node->getName(), 55 | ))->build(), 56 | ]; 57 | } 58 | 59 | /** 60 | * @param \PHPStan\Analyser\Scope $scope 61 | * @return bool 62 | */ 63 | private function isDataObjectWithAssociatedEntity(Scope $scope): bool 64 | { 65 | $className = $scope->getClassReflection()?->getName(); 66 | 67 | if (!str_ends_with($className, 'Data')) { 68 | return false; 69 | } 70 | 71 | try { 72 | $reflectionClass = new ReflectionClass(substr($className, 0, -4)); 73 | $docComment = $reflectionClass->getDocComment(); 74 | 75 | if ($docComment === false) { 76 | return false; 77 | } 78 | 79 | return str_contains($docComment, '@ORM\Entity'); 80 | } catch (ReflectionException) { 81 | return false; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Phpstan/EntityShouldHaveFactoryRule.php: -------------------------------------------------------------------------------- 1 | getClassReflection()?->getName() ?? ''; 43 | $factoryClassName = $entityClassName . 'Factory'; 44 | 45 | if (!str_starts_with($entityClassName, self::CHECKED_NAMESPACE)) { 46 | return []; 47 | } 48 | 49 | if (!$this->isCheckedEntity($entityClassName, $node)) { 50 | return []; 51 | } 52 | 53 | if (class_exists($factoryClassName)) { 54 | if ($this->factoryUsesEntityNameResolver($factoryClassName)) { 55 | return []; 56 | } 57 | 58 | return [ 59 | RuleErrorBuilder::message(sprintf( 60 | 'Factory %s do not use entity name resolver', 61 | $factoryClassName, 62 | ))->build(), 63 | ]; 64 | } 65 | 66 | return [ 67 | RuleErrorBuilder::message(sprintf( 68 | 'Entity %s is missing a factory (don\'t forget to use entity name resolver)', 69 | $scope->getClassReflection()?->getDisplayName(), 70 | ))->build(), 71 | ]; 72 | } 73 | 74 | /** 75 | * @param string $className 76 | * @param \PHPStan\Node\InClassNode $node 77 | * @return bool 78 | */ 79 | private function isCheckedEntity(string $className, InClassNode $node): bool 80 | { 81 | foreach (self::IGNORED_SUFFIXES as $ignoredSuffix) { 82 | if (str_ends_with($className, $ignoredSuffix)) { 83 | return false; 84 | } 85 | } 86 | 87 | return str_contains($node->getDocComment()?->getText() ?? '', '@ORM\Entity'); 88 | } 89 | 90 | /** 91 | * @param string $className 92 | * @return bool 93 | */ 94 | private function factoryUsesEntityNameResolver(string $className): bool 95 | { 96 | $reflectionClass = new ReflectionClass($className); 97 | $constructorParameters = $reflectionClass->getConstructor()?->getParameters(); 98 | 99 | if ($constructorParameters === null || count($constructorParameters) === 0) { 100 | return false; 101 | } 102 | 103 | foreach ($constructorParameters as $constructorParameter) { 104 | $type = $constructorParameter->getType()?->getName() ?? ''; 105 | 106 | if ($type === 'Shopsys\FrameworkBundle\Component\EntityExtension\EntityNameResolver') { 107 | return true; 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Phpstan/InjectedPropertiesInTestsExtension.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass(); 32 | 33 | if (!$declaringClass->implementsInterface(ServiceContainerTestCase::class)) { 34 | return false; 35 | } 36 | 37 | return $this->isInitialized($property, $propertyName); 38 | } 39 | 40 | /** 41 | * @param \PHPStan\Reflection\PropertyReflection $property 42 | * @param string $propertyName 43 | * @return bool 44 | */ 45 | public function isInitialized(PropertyReflection $property, string $propertyName): bool 46 | { 47 | return str_contains($property->getDocComment() ?? '', '@inject'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Phpstan/OrmPropertyGetterAndSetterHasNoTypehintRule.php: -------------------------------------------------------------------------------- 1 | getClassReflection()?->getName(), self::CHECKED_NAMESPACE)) { 39 | return []; 40 | } 41 | 42 | $methodName = $node->name->toString(); 43 | $expectedPropertyName = lcfirst(substr($methodName, 3)); 44 | 45 | try { 46 | $propertyReflection = $scope->getClassReflection()?->getProperty($expectedPropertyName, $scope); 47 | } catch (MissingPropertyFromReflectionException) { 48 | return []; 49 | } 50 | 51 | if (str_starts_with($methodName, 'get')) { 52 | return $this->checkGetter($node, $methodName, $scope, $propertyReflection); 53 | } 54 | 55 | if (str_starts_with($methodName, 'set')) { 56 | return $this->checkSetter($node, $methodName, $scope, $propertyReflection); 57 | } 58 | 59 | return []; 60 | } 61 | 62 | /** 63 | * @param \PhpParser\Node\Stmt\ClassMethod $node 64 | * @param string $methodName 65 | * @param \PHPStan\Analyser\Scope $scope 66 | * @param \PHPStan\Reflection\PropertyReflection $propertyReflection 67 | * @return array 68 | */ 69 | private function checkGetter( 70 | ClassMethod $node, 71 | string $methodName, 72 | Scope $scope, 73 | PropertyReflection $propertyReflection, 74 | ): array { 75 | if ($node->getReturnType() === null || $node->getReturnType()->name === 'bool') { 76 | return []; 77 | } 78 | 79 | if ($this->hasOrmAnnotation($propertyReflection) && !$this->isMethodInAncestor($methodName, $scope)) { 80 | return [ 81 | RuleErrorBuilder::message(sprintf( 82 | 'Method "%s::%s()" has a return typehint, but its associated property has an ORM annotation.', 83 | $scope->getClassReflection()?->getName(), 84 | $methodName, 85 | ))->line($node->getLine())->build(), 86 | ]; 87 | } 88 | 89 | return []; 90 | } 91 | 92 | /** 93 | * @param \PhpParser\Node\Stmt\ClassMethod $node 94 | * @param string $methodName 95 | * @param \PHPStan\Analyser\Scope $scope 96 | * @param \PHPStan\Reflection\PropertyReflection $propertyReflection 97 | * @return array 98 | */ 99 | private function checkSetter( 100 | ClassMethod $node, 101 | string $methodName, 102 | Scope $scope, 103 | PropertyReflection $propertyReflection, 104 | ): array { 105 | if (count($node->getParams()) !== 1) { 106 | return []; 107 | } 108 | 109 | $firstParameterType = $node->getParams()[0]->type; 110 | 111 | if ( 112 | !($firstParameterType instanceof Node\ComplexType) && 113 | ( 114 | $firstParameterType === null || 115 | $firstParameterType->name === 'bool' || 116 | $this->isParameterDataObjectOfCurrentEntity($firstParameterType->toString(), $scope) 117 | ) 118 | ) { 119 | return []; 120 | } 121 | 122 | if ($this->hasOrmAnnotation($propertyReflection) && !$this->isMethodInAncestor($methodName, $scope)) { 123 | return [ 124 | RuleErrorBuilder::message(sprintf( 125 | 'Method "%s::%s()" has a typehint on first parameter, but its associated property has an ORM annotation.', 126 | $scope->getClassReflection()?->getName(), 127 | $methodName, 128 | ))->line($node->getLine())->build(), 129 | ]; 130 | } 131 | 132 | return []; 133 | } 134 | 135 | /** 136 | * @param \PHPStan\Reflection\Php\PhpPropertyReflection $property 137 | * @return bool 138 | */ 139 | private function hasOrmAnnotation(PropertyReflection $property): bool 140 | { 141 | return $property->getDocComment() !== null && str_contains($property->getDocComment(), '@ORM\\'); 142 | } 143 | 144 | /** 145 | * @param string $methodName 146 | * @param \PHPStan\Analyser\Scope $scope 147 | * @return bool 148 | */ 149 | private function isMethodInAncestor(string $methodName, Scope $scope): bool 150 | { 151 | $classReflection = $scope->getClassReflection(); 152 | 153 | if ($classReflection === null) { 154 | return false; 155 | } 156 | 157 | $ancestors = $classReflection->getAncestors(); 158 | 159 | if (count($ancestors) === 0) { 160 | return false; 161 | } 162 | 163 | foreach ($ancestors as $ancestor) { 164 | if ($ancestor->is($classReflection->getName())) { 165 | continue; 166 | } 167 | 168 | if ($ancestor->hasMethod($methodName)) { 169 | return true; 170 | } 171 | } 172 | 173 | return false; 174 | } 175 | 176 | /** 177 | * @param string $parameterType 178 | * @param \PHPStan\Analyser\Scope $scope 179 | * @return bool 180 | */ 181 | private function isParameterDataObjectOfCurrentEntity(string $parameterType, Scope $scope): bool 182 | { 183 | $currentClassName = $scope->getClassReflection()?->getName(); 184 | 185 | if ($currentClassName === null) { 186 | return false; 187 | } 188 | 189 | return str_ends_with($parameterType, 'Data') && ($currentClassName . 'Data' === $parameterType); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Phpstan/OrmPropertyHasNoTypehintRule.php: -------------------------------------------------------------------------------- 1 | getClassReflection()?->getName(), self::CHECKED_NAMESPACE)) { 38 | return []; 39 | } 40 | 41 | if ($node->getNativeType() === null) { 42 | return []; 43 | } 44 | 45 | $propertyReflection = $scope->getClassReflection()?->getProperty($node->getName(), $scope); 46 | 47 | if ($this->hasOrmAnnotation($propertyReflection)) { 48 | return [ 49 | RuleErrorBuilder::message(sprintf( 50 | 'Property %s::%s has ORM annotation, so it should not have typehint.', 51 | $scope->getClassReflection()?->getDisplayName(), 52 | $node->getName(), 53 | ))->build(), 54 | ]; 55 | } 56 | 57 | return []; 58 | } 59 | 60 | /** 61 | * @param \PHPStan\Reflection\PropertyReflection $property 62 | * @return bool 63 | */ 64 | private function hasOrmAnnotation(PropertyReflection $property): bool 65 | { 66 | return $property->getDocComment() !== null && str_contains($property->getDocComment(), '@ORM\\'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/SetList/SetList.php: -------------------------------------------------------------------------------- 1 | isConstInsideClass($file, $constPosition)) { 36 | return; 37 | } 38 | 39 | if ($this->isConstWithAccessModifier($file, $constPosition)) { 40 | return; 41 | } 42 | 43 | if ($this->isConstWithAccessAnnotation($file, $constPosition)) { 44 | return; 45 | } 46 | 47 | $file->addError('Constant must have access modifier', $constPosition, self::class); 48 | } 49 | 50 | /** 51 | * @param \PHP_CodeSniffer\Files\File $file 52 | * @param int $constPosition 53 | * @return bool 54 | */ 55 | private function isConstInsideClass(File $file, int $constPosition): bool 56 | { 57 | $classStartPosition = $file->findPrevious(T_CLASS, $constPosition); 58 | 59 | if ($classStartPosition === false) { 60 | return false; 61 | } 62 | 63 | $tokens = $file->getTokens(); 64 | $classEndPosition = $tokens[$classStartPosition]['scope_closer']; 65 | 66 | return $constPosition > $classStartPosition && $constPosition < $classEndPosition; 67 | } 68 | 69 | /** 70 | * @param \PHP_CodeSniffer\Files\File $file 71 | * @param int $constPosition 72 | * @return bool 73 | */ 74 | private function isConstWithAccessModifier(File $file, int $constPosition): bool 75 | { 76 | $previousTokenEndPosition = $this->findScopeSearchEndPosition($file, $constPosition); 77 | 78 | $accessModifierStartPosition = $file->findPrevious( 79 | Tokens::$scopeModifiers, 80 | $constPosition, 81 | $previousTokenEndPosition, 82 | ); 83 | 84 | return (bool)$accessModifierStartPosition; 85 | } 86 | 87 | /** 88 | * @param \PHP_CodeSniffer\Files\File $file 89 | * @param int $constPosition 90 | * @return bool 91 | */ 92 | private function isConstWithAccessAnnotation(File $file, int $constPosition): bool 93 | { 94 | $previousTokenEndPosition = $this->findScopeSearchEndPosition($file, $constPosition); 95 | 96 | $phpDocStartPosition = $file->findPrevious( 97 | T_DOC_COMMENT_OPEN_TAG, 98 | $constPosition, 99 | $previousTokenEndPosition ?: 0, 100 | ); 101 | 102 | if ($phpDocStartPosition === false) { 103 | return false; 104 | } 105 | 106 | return $this->phpDocContainsAccessTag($file, $phpDocStartPosition); 107 | } 108 | 109 | /** 110 | * @param \PHP_CodeSniffer\Files\File $file 111 | * @param int $phpDocStartPosition 112 | * @return bool 113 | */ 114 | private function phpDocContainsAccessTag(File $file, int $phpDocStartPosition): bool 115 | { 116 | $tokens = $file->getTokens(); 117 | 118 | $commentTagPositions = array_reverse($tokens[$phpDocStartPosition]['comment_tags']); 119 | 120 | $lastPosition = $tokens[$phpDocStartPosition]['comment_closer']; 121 | 122 | foreach ($commentTagPositions as $commentTagPosition) { 123 | if ($tokens[$commentTagPosition]['content'] === '@access') { 124 | $possibleAccessModifierPosition = $file->findNext( 125 | T_DOC_COMMENT_STRING, 126 | $commentTagPosition, 127 | $lastPosition, 128 | ); 129 | 130 | if (preg_match( 131 | '~(public|protected|private)~', 132 | $tokens[$possibleAccessModifierPosition]['content'], 133 | ) === 1) { 134 | return true; 135 | } 136 | } 137 | } 138 | 139 | return false; 140 | } 141 | 142 | /** 143 | * @param \PHP_CodeSniffer\Files\File $file 144 | * @param int $constPosition 145 | * @return int 146 | */ 147 | private function findScopeSearchEndPosition(File $file, int $constPosition): int 148 | { 149 | return $file->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $constPosition) ?: 0; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Sniffs/ForbiddenDoctrineDefaultValueSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 30 | $docBlockOpeningTagPositions = $this->getAllDocBlockOpeningTagPositions($file, $classPosition); 31 | 32 | foreach ($docBlockOpeningTagPositions as $docBlockOpenTagPosition) { 33 | $docBlockToken = $tokens[$docBlockOpenTagPosition]; 34 | 35 | $content = TokenHelper::getContent($file, $docBlockOpenTagPosition, $docBlockToken['comment_closer']); 36 | 37 | if ($this->annotationContainsDefaultValue($content)) { 38 | $file->addError( 39 | 'Default value of entity properties cannot be used.', 40 | $docBlockOpenTagPosition, 41 | self::class, 42 | ); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @param string $annotationString 49 | * @return bool 50 | */ 51 | protected function annotationContainsDefaultValue(string $annotationString): bool 52 | { 53 | return (bool)preg_match('~options\s*=\s*\{\s*.*"default"~', $annotationString); 54 | } 55 | 56 | /** 57 | * @param \PHP_CodeSniffer\Files\File $file 58 | * @param int $startPosition 59 | * @return int[] 60 | */ 61 | protected function getAllDocBlockOpeningTagPositions(File $file, int $startPosition): array 62 | { 63 | $tokens = $file->getTokens(); 64 | $classToken = $tokens[$startPosition]; 65 | 66 | return TokenHelper::findNextAll( 67 | $file, 68 | [T_DOC_COMMENT_OPEN_TAG], 69 | $classToken['scope_opener'], 70 | $classToken['scope_closer'], 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Sniffs/ForbiddenDoctrineInheritanceSniff.php: -------------------------------------------------------------------------------- 1 | findPrevious(T_DOC_COMMENT_OPEN_TAG, $classPosition); 27 | 28 | if ($phpDocStartPosition === false) { 29 | return; 30 | } 31 | 32 | $phpDocTags = $this->findPhpDocTags($file, $classPosition, $phpDocStartPosition); 33 | 34 | foreach ($phpDocTags as $position => $token) { 35 | if ($this->isDoctrineInheritanceAnnotation($token)) { 36 | $message = 'It is forbidden to use Doctrine inheritance mapping because it causes problems during entity extension. Such problem with `OrderItem` was resolved during making OrderItem extendable #715.'; 37 | $file->addError( 38 | $message, 39 | $position, 40 | self::class, 41 | ); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * @param \PHP_CodeSniffer\Files\File $file 48 | * @param int $classPosition 49 | * @param int $phpDocStartPosition 50 | * @return array 51 | */ 52 | private function findPhpDocTags(File $file, int $classPosition, int $phpDocStartPosition): array 53 | { 54 | $phpDocEndPosition = $file->findPrevious(T_DOC_COMMENT_CLOSE_TAG, $classPosition); 55 | 56 | $result = []; 57 | $tokens = $file->getTokens(); 58 | 59 | for ($i = $phpDocStartPosition; $i < $phpDocEndPosition; $i++) { 60 | if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { 61 | $result[$i] = $tokens[$i]; 62 | } 63 | } 64 | 65 | return $result; 66 | } 67 | 68 | /** 69 | * @param array $token 70 | */ 71 | private function isDoctrineInheritanceAnnotation(array $token) 72 | { 73 | $content = $token['content']; 74 | 75 | return preg_match('~^.*ORM.*InheritanceType~', $content) === 1; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Sniffs/ForbiddenDumpSniff.php: -------------------------------------------------------------------------------- 1 | null, 22 | 'dump' => null, 23 | 'print_r' => null, 24 | 'var_dump' => null, 25 | 'var_export' => null, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /src/Sniffs/ForbiddenExitSniff.php: -------------------------------------------------------------------------------- 1 | null, 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /src/Sniffs/ForbiddenSuperGlobalSniff.php: -------------------------------------------------------------------------------- 1 | getTokens()[$position]; 39 | 40 | if (!in_array($currentToken['content'], $this->superglobalVariables, true)) { 41 | return; 42 | } 43 | 44 | $file->addError( 45 | sprintf('Super global "%s" is forbidden', $currentToken['content']), 46 | $position, 47 | self::class, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Sniffs/ForceLateStaticBindingForProtectedConstantsSniff.php: -------------------------------------------------------------------------------- 1 | getAllProtectedConstantsInClass($file); 33 | 34 | $selfPositions = TokenHelper::findNextAll($file, T_SELF, $classPosition); 35 | 36 | foreach ($selfPositions as $selfPosition) { 37 | $constantName = $this->findConstantNameFromSelfCall($file, $selfPosition); 38 | 39 | if ($constantName === null) { 40 | continue; 41 | } 42 | 43 | if (!in_array($constantName, $protectedConstants, true)) { 44 | continue; 45 | } 46 | 47 | $file->addFixableError( 48 | 'For better extensibility use late static binding.', 49 | $selfPosition, 50 | self::class, 51 | ); 52 | 53 | $file->fixer->beginChangeset(); 54 | $file->fixer->replaceToken($selfPosition, 'static'); 55 | $file->fixer->endChangeset(); 56 | } 57 | } 58 | 59 | /** 60 | * @param \PHP_CodeSniffer\Files\File $file 61 | * @param int $selfPosition 62 | * @return string|null 63 | */ 64 | private function findConstantNameFromSelfCall(File $file, int $selfPosition): ?string 65 | { 66 | $tokens = $file->getTokens(); 67 | 68 | $doubleColonPosition = TokenHelper::findNextEffective($file, $selfPosition + 1); 69 | 70 | if ($tokens[$doubleColonPosition]['code'] !== T_DOUBLE_COLON) { 71 | return null; 72 | } 73 | 74 | $stringPosition = TokenHelper::findNextEffective($file, $doubleColonPosition + 1); 75 | 76 | if ($tokens[$stringPosition]['code'] !== T_STRING) { 77 | return null; 78 | } 79 | 80 | if (strtolower($tokens[$stringPosition]['content']) === 'class') { 81 | return null; 82 | } 83 | 84 | $positionAfterString = TokenHelper::findNextEffective($file, $stringPosition + 1); 85 | 86 | if ($tokens[$positionAfterString]['code'] === T_OPEN_PARENTHESIS) { 87 | return null; 88 | } 89 | 90 | return $tokens[$stringPosition]['content']; 91 | } 92 | 93 | /** 94 | * @param \PHP_CodeSniffer\Files\File $file 95 | * @return string[] 96 | */ 97 | private function getAllProtectedConstantsInClass(File $file): array 98 | { 99 | $constPositions = TokenHelper::findNextAll($file, T_CONST, 0); 100 | 101 | $protectedConstants = []; 102 | 103 | foreach ($constPositions as $constPosition) { 104 | if ($this->isProtectedVisibility($file, $constPosition)) { 105 | $protectedConstants[] = ConstantHelper::getName($file, $constPosition); 106 | 107 | continue; 108 | } 109 | } 110 | 111 | return $protectedConstants; 112 | } 113 | 114 | /** 115 | * @param \PHP_CodeSniffer\Files\File $file 116 | * @param int $constPosition 117 | * @return bool 118 | */ 119 | private function isProtectedVisibility(File $file, int $constPosition): bool 120 | { 121 | $protectedModifierPosition = TokenHelper::findPreviousLocal($file, T_PROTECTED, $constPosition); 122 | 123 | return $protectedModifierPosition !== null; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Sniffs/ObjectIsCreatedByFactorySniff.php: -------------------------------------------------------------------------------- 1 | findEndOfStatement($position); 29 | $instantiatedClassNamePosition = $file->findNext(T_STRING, $position, $endPosition); 30 | 31 | if ($instantiatedClassNamePosition === false) { 32 | // eg. new $className; cannot be resolved 33 | return; 34 | } 35 | 36 | $naming = new Naming(); 37 | 38 | $instantiatedClassName = $naming->getClassName($file, $instantiatedClassNamePosition); 39 | $factoryClassName = $instantiatedClassName . 'Factory'; 40 | $currentClassName = $this->getFirstClassNameInFile($file); 41 | 42 | if (!class_exists($factoryClassName) || is_a($currentClassName, $factoryClassName, true)) { 43 | return; 44 | } 45 | 46 | $file->addError( 47 | sprintf('For creation of "%s" class use its factory "%s"', $instantiatedClassName, $factoryClassName), 48 | $position, 49 | self::class, 50 | ); 51 | } 52 | 53 | /** 54 | * We can not use Symplify\CodingStandard\TokenRunner\Analyzer\SnifferAnalyzer\Naming::getClassName() 55 | * as it does not include namespace of declared class. 56 | * 57 | * @param \PHP_CodeSniffer\Files\File $file 58 | * @return string|null 59 | */ 60 | private function getFirstClassNameInFile(File $file): ?string 61 | { 62 | $position = $file->findNext(T_CLASS, 0); 63 | 64 | if ($position === false) { 65 | return null; 66 | } 67 | 68 | $fileClassName = ClassHelper::getFullyQualifiedName($file, $position); 69 | 70 | return ltrim($fileClassName, '\\'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Sniffs/RequireOverrideAttributeSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 29 | 30 | $classPtr = $phpcsFile->findPrevious([T_CLASS, T_TRAIT], $stackPtr); 31 | 32 | if ($classPtr === false) { 33 | return; 34 | } 35 | 36 | $classNamePtr = $phpcsFile->findNext(T_STRING, $classPtr); 37 | 38 | if ($classNamePtr === false) { 39 | return; 40 | } 41 | $className = $tokens[$classNamePtr]['content']; 42 | 43 | $methodNamePtr = $phpcsFile->findNext(T_STRING, $stackPtr); 44 | 45 | if ($methodNamePtr === false) { 46 | return; 47 | } 48 | $methodName = $tokens[$methodNamePtr]['content']; 49 | 50 | if ($methodName === '__construct') { 51 | return; 52 | } 53 | 54 | $parentClassPtr = $phpcsFile->findNext(T_EXTENDS, $classNamePtr); 55 | 56 | if ($parentClassPtr === false) { 57 | return; 58 | } 59 | 60 | $parentClassNamePtr = $phpcsFile->findNext(T_STRING, $parentClassPtr); 61 | 62 | if ($parentClassNamePtr === false) { 63 | return; 64 | } 65 | 66 | $parentClassName = $tokens[$parentClassNamePtr]['content']; 67 | $parentClassFqn = $this->getFqnOfClass($phpcsFile, $tokens, $parentClassName); 68 | 69 | if ($parentClassFqn === '') { 70 | return; 71 | } 72 | 73 | 74 | try { 75 | $parentClassReflection = new ReflectionClass($parentClassFqn); 76 | 77 | $hasMethod = $this->hasParentClassMethod($parentClassReflection, $methodName); 78 | } catch (ReflectionException $e) { 79 | return; 80 | } 81 | 82 | if ($hasMethod === false) { 83 | return; 84 | } 85 | 86 | $hasOverride = $this->checkMethodHasOverrideAttribute($phpcsFile, $methodNamePtr, $tokens, $hasOverride); 87 | 88 | if ($hasOverride === true) { 89 | return; 90 | } 91 | 92 | $error = "Method {$className}::{$methodName} overrides {$parentClassName}::{$methodName} but is missing #[Override] attribute."; 93 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingOverride'); 94 | 95 | if (!$fix) { 96 | return; 97 | } 98 | 99 | $phpcsFile->fixer->beginChangeset(); 100 | 101 | $previousLine = $phpcsFile->findPrevious(T_WHITESPACE, $methodNamePtr, value: PHP_EOL); 102 | $indent = $tokens[$previousLine + 1]['content']; 103 | $phpcsFile->fixer->addContentBefore($previousLine + 1, "{$indent}#[Override]" . PHP_EOL); 104 | 105 | $overrideFqn = $this->getFqnOfClass($phpcsFile, $tokens, 'Override'); 106 | 107 | if ($overrideFqn === '') { 108 | $usePtr = $phpcsFile->findNext(T_USE, 0); 109 | $previousLine = $phpcsFile->findPrevious(T_WHITESPACE, $usePtr, value: PHP_EOL); 110 | $phpcsFile->fixer->addContentBefore($previousLine + 1, 'use Override;' . PHP_EOL); 111 | } 112 | 113 | $phpcsFile->fixer->endChangeset(); 114 | } 115 | 116 | /** 117 | * @param \ReflectionClass $parentClass 118 | * @param string $methodName 119 | * @return bool 120 | */ 121 | protected function hasParentClassMethod(ReflectionClass $parentClass, string $methodName): bool 122 | { 123 | if ($parentClass->hasMethod($methodName)) { 124 | return true; 125 | } 126 | 127 | $parentClass = $parentClass->getParentClass(); 128 | 129 | if ($parentClass === false) { 130 | return false; 131 | } 132 | 133 | return $parentClass->hasMethod($methodName); 134 | } 135 | 136 | /** 137 | * @param \PHP_CodeSniffer\Files\File $phpcsFile 138 | * @param array $tokens 139 | * @param string $parentClassName 140 | * @return string 141 | */ 142 | private function getFqnOfClass(File $phpcsFile, array $tokens, mixed $parentClassName): string 143 | { 144 | $usePtr = $phpcsFile->findNext(T_USE, 0); 145 | 146 | $parentClassFqn = ''; 147 | 148 | while ($usePtr !== false) { 149 | $semicolonPtr = $phpcsFile->findNext(T_SEMICOLON, $usePtr); 150 | $fqnClassNamePtr = $phpcsFile->findPrevious(T_STRING, $semicolonPtr); 151 | 152 | $fqnClassName = $tokens[$fqnClassNamePtr]['content']; 153 | 154 | if ($fqnClassName !== $parentClassName) { 155 | $usePtr = $phpcsFile->findNext(T_USE, $usePtr + 1); 156 | 157 | continue; 158 | } 159 | 160 | $fqnCurrentPtr = $usePtr + 2; 161 | 162 | while ($tokens[$fqnCurrentPtr]['type'] !== 'T_WHITESPACE' && $tokens[$fqnCurrentPtr]['type'] !== 'T_SEMICOLON') { 163 | $parentClassFqn .= $tokens[$fqnCurrentPtr]['content']; 164 | $fqnCurrentPtr++; 165 | } 166 | 167 | break; 168 | } 169 | 170 | return $parentClassFqn; 171 | } 172 | 173 | /** 174 | * @param \PHP_CodeSniffer\Files\File $phpcsFile 175 | * @param int $methodNamePtr 176 | * @param array $tokens 177 | * @return bool 178 | */ 179 | private function checkMethodHasOverrideAttribute(File $phpcsFile, int $methodNamePtr, array $tokens): bool 180 | { 181 | $hasOverride = false; 182 | $previousLinePtr = $phpcsFile->findPrevious(T_WHITESPACE, $methodNamePtr, value: PHP_EOL); 183 | 184 | for ($i = $previousLinePtr - 1; $i >= 0; $i--) { 185 | if ($tokens[$i]['code'] === T_ATTRIBUTE) { 186 | $attributeNamePtr = $phpcsFile->findNext(T_STRING, $i); 187 | 188 | if ($attributeNamePtr !== false && $tokens[$attributeNamePtr]['content'] === 'Override') { 189 | $hasOverride = true; 190 | 191 | break; 192 | } 193 | } 194 | 195 | if ($tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG || $tokens[$i]['code'] === T_FUNCTION) { 196 | break; 197 | } 198 | } 199 | 200 | return $hasOverride; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Sniffs/ValidVariableNameSniff.php: -------------------------------------------------------------------------------- 1 | checkCamelCaseFormatViolation($file, $position, $errorMessageFormat); 32 | } 33 | 34 | /** 35 | * @param \PHP_CodeSniffer\Files\File $file 36 | * @param int $position 37 | */ 38 | #[Override] 39 | protected function processVariableInString(File $file, $position): void 40 | { 41 | } 42 | 43 | /** 44 | * @param \PHP_CodeSniffer\Files\File $file 45 | * @param int $position 46 | */ 47 | #[Override] 48 | protected function processMemberVar(File $file, $position): void 49 | { 50 | $errorMessageFormat = 'Class member variable "$%s" should be camel case'; 51 | $this->checkCamelCaseFormatViolation($file, $position, $errorMessageFormat); 52 | } 53 | 54 | /** 55 | * @param \PHP_CodeSniffer\Files\File $file 56 | * @param int $position 57 | * @param string $errorMessageFormat 58 | */ 59 | private function checkCamelCaseFormatViolation(File $file, int $position, string $errorMessageFormat): void 60 | { 61 | $currentToken = $file->getTokens()[$position]; 62 | 63 | $variableName = ltrim($currentToken['content'], '$'); 64 | 65 | if (Common::isCamelCaps($variableName)) { 66 | return; 67 | } 68 | 69 | $file->addError(sprintf( 70 | $errorMessageFormat, 71 | $variableName, 72 | ), $position, self::class); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Unit/CsFixer/AbstractFixerTestCase.php: -------------------------------------------------------------------------------- 1 | createFixerService(); 40 | 41 | if ($inputFilePath !== null) { 42 | $input = file_get_contents($inputFilePath); 43 | Tokens::clearCache(); 44 | $tokens = Tokens::fromCode($input); 45 | 46 | if (!$fixer->isCandidate($tokens)) { 47 | return; 48 | } 49 | 50 | $fixer->fix($file, $tokens); 51 | 52 | static::assertThat( 53 | $tokens->generateCode(), 54 | new IsIdenticalString($expected), 55 | ); 56 | static::assertTrue( 57 | $tokens->isChanged(), 58 | 'Tokens collection built on input code must be marked as changed after fixing.', 59 | ); 60 | 61 | $tokens->clearEmptyTokens(); 62 | 63 | Tokens::clearCache(); 64 | $expectedTokens = Tokens::fromCode($expected); 65 | 66 | static::assertTokens($expectedTokens, $tokens); 67 | } 68 | 69 | Tokens::clearCache(); 70 | $tokens = Tokens::fromCode($expected); 71 | 72 | if (!$fixer->isCandidate($tokens)) { 73 | return; 74 | } 75 | 76 | $fixer->fix($file, $tokens); 77 | 78 | static::assertThat( 79 | $tokens->generateCode(), 80 | new IsIdenticalString($expected), 81 | ); 82 | static::assertFalse( 83 | $tokens->isChanged(), 84 | 'Tokens collection built on expected code must not be marked as changed after fixing.', 85 | ); 86 | } 87 | 88 | /** 89 | * @param \PhpCsFixer\Tokenizer\Tokens $expectedTokens 90 | * @param \PhpCsFixer\Tokenizer\Tokens $inputTokens 91 | */ 92 | private static function assertTokens(Tokens $expectedTokens, Tokens $inputTokens): void 93 | { 94 | foreach ($expectedTokens as $index => $expectedToken) { 95 | if (!isset($inputTokens[$index])) { 96 | static::fail( 97 | sprintf( 98 | "The token at index %d must be:\n%s, but is not set in the input collection.", 99 | $index, 100 | $expectedToken->toJson(), 101 | ), 102 | ); 103 | } 104 | 105 | $inputToken = $inputTokens[$index]; 106 | 107 | static::assertTrue( 108 | $expectedToken->equals($inputToken), 109 | sprintf( 110 | "The token at index %d must be:\n%s,\ngot:\n%s.", 111 | $index, 112 | $expectedToken->toJson(), 113 | $inputToken->toJson(), 114 | ), 115 | ); 116 | 117 | $expectedTokenKind = $expectedToken->isArray() ? $expectedToken->getId() : $expectedToken->getContent(); 118 | static::assertTrue( 119 | $inputTokens->isTokenKindFound($expectedTokenKind), 120 | sprintf( 121 | 'The token kind %s (%s) must be found in tokens collection.', 122 | $expectedTokenKind, 123 | is_string($expectedTokenKind) ? $expectedTokenKind : Token::getNameForId($expectedTokenKind), 124 | ), 125 | ); 126 | } 127 | 128 | static::assertSame( 129 | $expectedTokens->count(), 130 | $inputTokens->count(), 131 | 'Both collections must have the same length.', 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Unit/CsFixer/ChainedFixer.php: -------------------------------------------------------------------------------- 1 | fixers[] = $fixer; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function isCandidate(Tokens $tokens): bool 32 | { 33 | foreach ($this->fixers as $fixer) { 34 | if ($fixer->isCandidate($tokens)) { 35 | return true; 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function isRisky(): bool 46 | { 47 | foreach ($this->fixers as $fixer) { 48 | if ($fixer->isRisky()) { 49 | return true; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function fix(SplFileInfo $file, Tokens $tokens): void 60 | { 61 | foreach ($this->fixers as $fixer) { 62 | $fixer->fix($file, $tokens); 63 | } 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getName(): string 70 | { 71 | return 'chained'; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getPriority(): int 78 | { 79 | return 0; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function supports(SplFileInfo $file): bool 86 | { 87 | foreach ($this->fixers as $fixer) { 88 | if ($fixer->supports($file)) { 89 | return true; 90 | } 91 | } 92 | 93 | return false; 94 | } 95 | 96 | /** 97 | * @return \PhpCsFixer\FixerDefinition\FixerDefinitionInterface 98 | */ 99 | public function getDefinition(): FixerDefinitionInterface 100 | { 101 | return new FixerDefinition('Chained fixer', []); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Unit/CsFixer/Constraint/IsIdenticalString.php: -------------------------------------------------------------------------------- 1 | isIdentical = new IsIdentical($this->value); 26 | } 27 | 28 | /** 29 | * @param mixed $other 30 | * @param string $description 31 | * @param bool $returnResult 32 | * @return bool|null 33 | */ 34 | #[Override] 35 | public function evaluate($other, string $description = '', bool $returnResult = false): ?bool 36 | { 37 | try { 38 | return $this->isIdentical->evaluate($other, $description, $returnResult); 39 | } catch (ExpectationFailedException $exception) { 40 | $message = $exception->getMessage(); 41 | 42 | $additionalFailureDescription = $this->additionalFailureDescription($other); 43 | 44 | if ($additionalFailureDescription) { 45 | $message .= "\n" . $additionalFailureDescription; 46 | } 47 | 48 | throw new ExpectationFailedException( 49 | $message, 50 | $exception->getComparisonFailure(), 51 | $exception, 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | #[Override] 60 | public function toString(): string 61 | { 62 | return $this->isIdentical->toString(); 63 | } 64 | 65 | /** 66 | * @param mixed $other 67 | * @return string 68 | */ 69 | #[Override] 70 | protected function additionalFailureDescription($other): string 71 | { 72 | $pattern = '/(\r\n|\n\r|\r)/'; 73 | 74 | if ( 75 | $other === $this->value || 76 | preg_replace($pattern, "\n", $other) !== preg_replace($pattern, "\n", $this->value) 77 | ) { 78 | return ''; 79 | } 80 | 81 | return ' #Warning: Strings contain different line endings! Debug using remapping ["\r" => "R", "\n" => "N", "\t" => "T"]:' 82 | . "\n" 83 | . ' -' . str_replace(["\r", "\n", "\t"], ['R', 'N', 'T'], $other) 84 | . "\n" 85 | . ' +' . str_replace(["\r", "\n", "\t"], ['R', 'N', 'T'], $this->value); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Unit/CsFixer/ForbiddenDumpFixer/ForbiddenDumpFixerTest.php: -------------------------------------------------------------------------------- 1 | configure([ 21 | 'analyzed_namespaces' => ['TestNamespace'], 22 | ]); 23 | 24 | return $fixer; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | #[Override] 31 | public static function getTestingFiles(): iterable 32 | { 33 | yield [__DIR__ . '/fixed/constructor_property_promotion.php', __DIR__ . '/wrong/constructor_property_promotion.php']; 34 | 35 | yield [__DIR__ . '/fixed/fixed.php', __DIR__ . '/wrong/wrong.php']; 36 | 37 | yield [__DIR__ . '/correct/correct.php']; 38 | 39 | yield [__DIR__ . '/correct/correct-final.php']; 40 | 41 | yield [__DIR__ . '/correct/ignored-namespace.php']; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/CsFixer/ForbiddenPrivateVisibilityFixer/correct/correct-final.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 14 | 15 | 16 | 17 | 18 | 19 | 14 | 15 | 16 | 17 | 18 | 19 |