├── LICENSE ├── README.md ├── composer.json ├── lib ├── AbstractFixer.php ├── Config.php ├── FinalAbstractPublicFixer.php ├── FinalInternalClassFixer.php ├── FunctionReferenceSpaceFixer.php ├── InlineCommentSpacerFixer.php ├── PhpFileOnlyProxyFixer.php └── Utf8Fixer.php └── renovate.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Filippo Tessarotto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slam PHP-CS-Fixer extensions 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/slam/php-cs-fixer-extensions.svg)](https://packagist.org/packages/slam/php-cs-fixer-extensions) 4 | [![Downloads](https://img.shields.io/packagist/dt/slam/php-cs-fixer-extensions.svg)](https://packagist.org/packages/slam/php-cs-fixer-extensions) 5 | [![Integrate](https://github.com/Slamdunk/php-cs-fixer-extensions/workflows/CI/badge.svg?branch=master)](https://github.com/Slamdunk/php-cs-fixer-extensions/actions) 6 | 7 | [PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) extensions and configurations 8 | 9 | ## Installation 10 | 11 | Execute: 12 | 13 | `composer require --dev slam/php-cs-fixer-extensions` 14 | 15 | ## Usage 16 | 17 | In your `.php_cs` file: 18 | 19 | ```php 20 | setRiskyAllowed(true); 25 | 26 | $config->registerCustomFixers([ 27 | new SlamCsFixer\FinalAbstractPublicFixer(), 28 | new SlamCsFixer\FinalInternalClassFixer(), 29 | new SlamCsFixer\FunctionReferenceSpaceFixer(), 30 | new SlamCsFixer\InlineCommentSpacerFixer(), 31 | new SlamCsFixer\PhpFileOnlyProxyFixer(new PhpCsFixer\Fixer\Basic\BracesFixer()), 32 | new SlamCsFixer\Utf8Fixer(), 33 | ]); 34 | 35 | $this->setRules([ 36 | 'Slam/final_abstract_public' => true, 37 | 'Slam/final_internal_class' => true, 38 | 'Slam/function_reference_space' => true, 39 | 'Slam/inline_comment_spacer' => true, 40 | 'Slam/php_only_braces' => true, 41 | 'Slam/utf8' => true, 42 | ]); 43 | 44 | $config->getFinder() 45 | ->in(__DIR__ . '/app') 46 | ->in(__DIR__ . '/tests') 47 | ->name('*.phtml') 48 | ; 49 | 50 | return $config; 51 | ``` 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slam/php-cs-fixer-extensions", 3 | "description": "Slam extension of friendsofphp/php-cs-fixer", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Filippo Tessarotto", 8 | "email": "zoeslam@gmail.com", 9 | "role": "Developer" 10 | } 11 | ], 12 | "require": { 13 | "php": "~8.3.0 || ~8.4.0", 14 | "ext-mbstring": "*", 15 | "ext-tokenizer": "*", 16 | "friendsofphp/php-cs-fixer": "^3.75.0" 17 | }, 18 | "require-dev": { 19 | "phpstan/phpstan": "^2.1.17", 20 | "phpstan/phpstan-phpunit": "^2.0.6", 21 | "phpunit/phpunit": "^12.2.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "SlamCsFixer\\": "lib/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "SlamCsFixer\\Tests\\": "tests/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/AbstractFixer.php: -------------------------------------------------------------------------------- 1 | true, 13 | '@PhpCsFixer' => true, 14 | '@PhpCsFixer:risky' => true, 15 | '@PHP80Migration:risky' => true, 16 | '@PHP81Migration' => true, 17 | '@PHPUnit84Migration:risky' => true, 18 | 'Slam/final_abstract_public' => true, 19 | 'Slam/final_internal_class' => true, 20 | 'Slam/function_reference_space' => true, 21 | 'Slam/php_only_slam_inline_comment_spacer' => true, 22 | 'Slam/utf8' => true, 23 | 'align_multiline_comment' => ['comment_type' => 'all_multiline'], 24 | 'binary_operator_spaces' => ['default' => 'align_single_space'], 25 | 'combine_consecutive_issets' => false, 26 | 'combine_consecutive_unsets' => false, 27 | 'comment_to_phpdoc' => false, 28 | 'concat_space' => ['spacing' => 'one'], 29 | 'date_time_create_from_format_call' => true, 30 | 'date_time_immutable' => false, 31 | 'error_suppression' => false, 32 | 'final_class' => false, 33 | 'final_internal_class' => false, 34 | 'final_public_method_for_abstract_class' => false, 35 | 'general_phpdoc_annotation_remove' => false, 36 | 'global_namespace_import' => true, 37 | 'group_import' => false, 38 | 'header_comment' => false, 39 | 'heredoc_indentation' => false, 40 | 'mb_str_functions' => false, 41 | 'method_argument_space' => ['keep_multiple_spaces_after_comma' => true], 42 | 'native_constant_invocation' => true, 43 | 'native_function_invocation' => ['include' => ['@internal']], 44 | 'no_multiline_whitespace_around_double_arrow' => false, 45 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 46 | 'not_operator_with_space' => false, 47 | 'not_operator_with_successor_space' => true, 48 | 'ordered_class_elements' => ['order' => ['use_trait', 'constant_public', 'constant_protected', 'constant_private', 'property', 'construct', 'destruct', 'magic', 'phpunit', 'method']], 49 | 'ordered_interfaces' => true, 50 | 'php_unit_data_provider_static' => true, 51 | 'php_unit_internal_class' => false, 52 | 'php_unit_size_class' => false, 53 | 'php_unit_strict' => false, 54 | 'php_unit_test_class_requires_covers' => false, 55 | 'phpdoc_add_missing_param_annotation' => false, 56 | 'phpdoc_line_span' => ['const' => 'single', 'property' => 'single', 'method' => 'single'], 57 | 'phpdoc_tag_casing' => true, 58 | 'phpdoc_to_param_type' => false, 59 | 'phpdoc_to_property_type' => true, 60 | 'phpdoc_to_return_type' => false, 61 | 'pow_to_exponentiation' => false, 62 | // 'psr0' => true, 63 | 'random_api_migration' => true, 64 | 'regular_callable_call' => true, 65 | 'simple_to_complex_string_variable' => false, 66 | 'simplified_if_return' => true, 67 | 'simplified_null_return' => false, 68 | 'single_line_throw' => false, 69 | 'space_after_semicolon' => true, 70 | 'static_lambda' => false, 71 | 'unary_operator_spaces' => false, 72 | 'use_arrow_functions' => false, 73 | ]; 74 | 75 | /** @param array $overriddenRules */ 76 | public function __construct(array $overriddenRules = []) 77 | { 78 | parent::__construct(__NAMESPACE__); 79 | \putenv('PHP_CS_FIXER_FUTURE_MODE=1'); 80 | 81 | $this->setRiskyAllowed(true); 82 | $this->registerCustomFixers([ 83 | new FinalAbstractPublicFixer(), 84 | new FinalInternalClassFixer(), 85 | new FunctionReferenceSpaceFixer(), 86 | new PhpFileOnlyProxyFixer(new InlineCommentSpacerFixer()), 87 | new Utf8Fixer(), 88 | ]); 89 | 90 | $rules = self::RULES; 91 | if (! empty($overriddenRules)) { 92 | $rules = \array_merge($rules, $overriddenRules); 93 | } 94 | 95 | $this->setRules($rules); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/FinalAbstractPublicFixer.php: -------------------------------------------------------------------------------- 1 | isTokenKindFound(\T_CLASS); 42 | } 43 | 44 | public function isRisky(): bool 45 | { 46 | return true; 47 | } 48 | 49 | protected function applyFix(SplFileInfo $file, Tokens $tokens): void 50 | { 51 | $classes = \array_keys($tokens->findGivenKind(\T_CLASS)); 52 | 53 | while ($classIndex = \array_pop($classes)) { 54 | $prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)]; 55 | if (! $prevToken->isGivenKind([\T_ABSTRACT])) { 56 | continue; 57 | } 58 | 59 | $classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']); 60 | $classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen); 61 | 62 | $this->fixClass($tokens, $classOpen, $classClose); 63 | } 64 | } 65 | 66 | private function fixClass(Tokens $tokens, int $classOpenIndex, int $classCloseIndex): void 67 | { 68 | for ($index = $classCloseIndex - 1; $index > $classOpenIndex; --$index) { 69 | if ($tokens[$index]->equals('}')) { 70 | $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); 71 | 72 | continue; 73 | } 74 | if (! $tokens[$index]->isGivenKind(\T_PUBLIC)) { 75 | continue; 76 | } 77 | $nextIndex = $tokens->getNextMeaningfulToken($index); 78 | $nextToken = $tokens[$nextIndex]; 79 | if ($nextToken->isGivenKind(\T_STATIC)) { 80 | $nextIndex = $tokens->getNextMeaningfulToken($nextIndex); 81 | $nextToken = $tokens[$nextIndex]; 82 | } 83 | if (! $nextToken->isGivenKind(\T_FUNCTION)) { 84 | continue; 85 | } 86 | $nextIndex = $tokens->getNextMeaningfulToken($nextIndex); 87 | $nextToken = $tokens[$nextIndex]; 88 | if (! $nextToken->isGivenKind(\T_STRING) || 0 === \mb_strpos($nextToken->getContent(), '__')) { 89 | continue; 90 | } 91 | $prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)]; 92 | if ($prevToken->isGivenKind([\T_FINAL, \T_ABSTRACT])) { 93 | continue; 94 | } 95 | 96 | $tokens->insertAt( 97 | $index, 98 | [ 99 | new Token([\T_FINAL, 'final']), 100 | new Token([\T_WHITESPACE, ' ']), 101 | ] 102 | ); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/FinalInternalClassFixer.php: -------------------------------------------------------------------------------- 1 | isTokenKindFound(\T_CLASS); 34 | } 35 | 36 | public function isRisky(): bool 37 | { 38 | return true; 39 | } 40 | 41 | protected function applyFix(SplFileInfo $file, Tokens $tokens): void 42 | { 43 | $classes = \array_keys($tokens->findGivenKind(\T_CLASS)); 44 | 45 | while ($classIndex = \array_pop($classes)) { 46 | $prevTokenIndex = $tokens->getPrevMeaningfulToken($classIndex); 47 | if (\defined('T_READONLY') && $tokens[$prevTokenIndex]->isGivenKind([\T_READONLY])) { 48 | $classIndex = $prevTokenIndex; 49 | } 50 | 51 | // ignore class if it is abstract or already final 52 | $prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)]; 53 | if ($prevToken->isGivenKind([\T_ABSTRACT, \T_FINAL, \T_NEW])) { 54 | continue; 55 | } 56 | 57 | // ignore class if it's a Doctrine Entity 58 | if (self::isDoctrineEntity($tokens, $classIndex)) { 59 | continue; 60 | } 61 | 62 | $tokens->insertAt( 63 | $classIndex, 64 | [ 65 | new Token([\T_FINAL, 'final']), 66 | new Token([\T_WHITESPACE, ' ']), 67 | ] 68 | ); 69 | } 70 | } 71 | 72 | private static function isDoctrineEntity(Tokens $tokens, int $classIndex): bool 73 | { 74 | $docToken = $tokens[$tokens->getPrevNonWhitespace($classIndex)]; 75 | if ($docToken->isGivenKind(\T_DOC_COMMENT) && 1 === \preg_match(\sprintf('/@%s/', self::REGEX), $docToken->getContent())) { 76 | return true; 77 | } 78 | 79 | while ($classIndex > 0 && $tokens[$tokens->getPrevNonWhitespace($classIndex)]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) { 80 | $attributeOpenIndex = $tokens->getPrevTokenOfKind($classIndex, [[\T_ATTRIBUTE]]); 81 | \assert(null !== $attributeOpenIndex); 82 | $content = ''; 83 | for ($index = $attributeOpenIndex; $index < $classIndex; ++$index) { 84 | $content .= $tokens[$index]->getContent(); 85 | } 86 | if (1 === \preg_match(\sprintf('/^#\[%s/', self::REGEX), $content)) { 87 | return true; 88 | } 89 | 90 | $classIndex = $attributeOpenIndex - 1; 91 | } 92 | 93 | return false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/FunctionReferenceSpaceFixer.php: -------------------------------------------------------------------------------- 1 | isTokenKindFound(\T_FUNCTION); 30 | } 31 | 32 | protected function applyFix(SplFileInfo $file, Tokens $tokens): void 33 | { 34 | for ($index = $tokens->count() - 1; $index > 0; --$index) { 35 | if (! $tokens[$index]->isGivenKind(\T_FUNCTION)) { 36 | continue; 37 | } 38 | 39 | $startParenthesisIndex = $tokens->getNextTokenOfKind($index, ['(']); 40 | $endParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startParenthesisIndex); 41 | $useIndex = $tokens->getNextNonWhitespace($endParenthesisIndex); 42 | if ($tokens[$useIndex]->isGivenKind(CT::T_USE_LAMBDA)) { 43 | $startUseIndex = $tokens->getNextTokenOfKind($useIndex, ['(']); 44 | $endParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startUseIndex); 45 | } 46 | 47 | for ($iter = $endParenthesisIndex; $iter > $startParenthesisIndex; --$iter) { 48 | $token = $tokens[$iter]; 49 | 50 | if (! $token->equals('&')) { 51 | continue; 52 | } 53 | 54 | $nextTokenIndex = $iter + 1; 55 | $nextToken = $tokens[$nextTokenIndex]; 56 | 57 | if ($nextToken->isWhitespace()) { 58 | $tokens[$nextTokenIndex] = new Token([$nextToken->getId(), ' ']); 59 | } else { 60 | $tokens->insertAt($nextTokenIndex, new Token([\T_WHITESPACE, ' '])); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/InlineCommentSpacerFixer.php: -------------------------------------------------------------------------------- 1 | isTokenKindFound(\T_COMMENT); 34 | } 35 | 36 | protected function applyFix(SplFileInfo $file, Tokens $tokens): void 37 | { 38 | foreach ($tokens as $index => $token) { 39 | $content = $token->getContent(); 40 | if (! $token->isComment() || '//' !== \mb_substr($content, 0, 2) || '// ' === \mb_substr($content, 0, 3)) { 41 | continue; 42 | } 43 | 44 | $content = \substr_replace($content, ' ', 2, 0); 45 | $tokens[$index] = new Token([$token->getId(), $content]); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/PhpFileOnlyProxyFixer.php: -------------------------------------------------------------------------------- 1 | fixer = $fixer; 24 | } 25 | 26 | public function configure(?array $configuration = null): void 27 | { 28 | if (! $this->fixer instanceof ConfigurableFixerInterface) { 29 | return; 30 | } 31 | 32 | $this->fixer->configure($configuration); 33 | } 34 | 35 | public function getConfigurationDefinition(): FixerConfigurationResolverInterface 36 | { 37 | if (! $this->fixer instanceof ConfigurableFixerInterface) { 38 | return new class implements FixerConfigurationResolverInterface { 39 | public function getOptions(): array 40 | { 41 | return []; 42 | } 43 | 44 | public function resolve(array $configuration): array 45 | { 46 | return []; 47 | } 48 | }; 49 | } 50 | 51 | return $this->fixer->getConfigurationDefinition(); 52 | } 53 | 54 | public function setWhitespacesConfig(WhitespacesFixerConfig $config): void 55 | { 56 | if (! $this->fixer instanceof WhitespacesAwareFixerInterface) { 57 | return; 58 | } 59 | 60 | $this->fixer->setWhitespacesConfig($config); 61 | } 62 | 63 | public function getDefinition(): FixerDefinitionInterface 64 | { 65 | $originalDefinition = $this->fixer->getDefinition(); 66 | 67 | return new FixerDefinition( 68 | \sprintf('PHP-FILE-ONLY: %s', $originalDefinition->getSummary()), 69 | $originalDefinition->getCodeSamples(), 70 | $originalDefinition->getDescription(), 71 | $originalDefinition->getRiskyDescription() 72 | ); 73 | } 74 | 75 | public function isCandidate(Tokens $tokens): bool 76 | { 77 | return $this->fixer->isCandidate($tokens); 78 | } 79 | 80 | public function isRisky(): bool 81 | { 82 | return $this->fixer->isRisky(); 83 | } 84 | 85 | public function fix(SplFileInfo $file, Tokens $tokens): void 86 | { 87 | $this->fixer->fix($file, $tokens); 88 | } 89 | 90 | public function getName(): string 91 | { 92 | return \sprintf('Slam/php_only_%s', \str_replace('/', '_', \mb_strtolower($this->fixer->getName()))); 93 | } 94 | 95 | public function getPriority(): int 96 | { 97 | return $this->fixer->getPriority(); 98 | } 99 | 100 | public function supports(SplFileInfo $file): bool 101 | { 102 | return 'php' === \pathinfo($file->getFilename(), \PATHINFO_EXTENSION); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/Utf8Fixer.php: -------------------------------------------------------------------------------- 1 | generateCode(); 40 | if (false === \mb_check_encoding($content, 'UTF-8')) { 41 | $tokens->setCode(\mb_convert_encoding($content, 'UTF-8', 'Windows-1252')); 42 | } 43 | } 44 | 45 | public function getPriority(): int 46 | { 47 | // Should run after encoding 48 | return 99; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>Slamdunk/.github:renovate-config" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------