├── .qlty ├── .gitignore ├── configs │ └── .yamllint.yaml └── qlty.toml ├── MO4 ├── Tests │ ├── Formatting │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.3.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.4.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.5.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.2.inc │ │ ├── AlphabeticalUseStatementsUnitTest.pass.1.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.1.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.1.inc.fixed │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.2.inc.fixed │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.2.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.6.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.6.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.4.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.4.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.4.inc.fixed │ │ ├── AlphabeticalUseStatementsUnitTest.fail.3.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.3.inc.fixed │ │ ├── AlphabeticalUseStatementsUnitTest.fail.2.inc │ │ ├── AlphabeticalUseStatementsUnitTest.fail.2.inc.fixed │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.3.inc.fixed │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.3.inc │ │ ├── AlphabeticalUseStatementsUnitTest.pass.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.1.inc.fixed │ │ ├── UnnecessaryNamespaceUsageUnitTest.pass.1.inc │ │ ├── UnnecessaryNamespaceUsageUnitTest.fail.1.inc │ │ ├── AlphabeticalUseStatementsUnitTest.php │ │ └── UnnecessaryNamespaceUsageUnitTest.php │ ├── Strings │ │ ├── VariableInDoubleQuotedStringUnitTest.fail.inc │ │ ├── VariableInDoubleQuotedStringUnitTest.fail.inc.fixed │ │ ├── VariableInDoubleQuotedStringUnitTest.pass.inc │ │ └── VariableInDoubleQuotedStringUnitTest.php │ ├── Arrays │ │ ├── MultiLineArrayUnitTest.pass.inc │ │ ├── MultiLineArrayUnitTest.fail.inc │ │ ├── MultiLineArrayUnitTest.fail.inc.fixed │ │ ├── MultiLineArrayUnitTest.php │ │ ├── ArrayDoubleArrowAlignmentUnitTest.php │ │ ├── ArrayDoubleArrowAlignmentUnitTest.pass.inc │ │ ├── ArrayDoubleArrowAlignmentUnitTest.fail.inc │ │ └── ArrayDoubleArrowAlignmentUnitTest.fail.inc.fixed │ ├── WhiteSpace │ │ ├── MultipleEmptyLinesUnitTest.pass.inc │ │ ├── MultipleEmptyLinesUnitTest.fail.inc.fixed │ │ ├── MultipleEmptyLinesUnitTest.fail.inc │ │ ├── ConstantSpacingUnitTest.pass.inc │ │ ├── ConstantSpacingUnitTest.fail.inc.fixed │ │ ├── ConstantSpacingUnitTest.fail.inc │ │ ├── MultipleEmptyLinesUnitTest.php │ │ └── ConstantSpacingUnitTest.php │ ├── Commenting │ │ ├── PropertyCommentUnitTest.fail.inc │ │ ├── PropertyCommentUnitTest.fail.inc.fixed │ │ ├── PropertyCommentUnitTest.pass.inc │ │ └── PropertyCommentUnitTest.php │ └── AbstractMo4SniffUnitTest.php ├── Library │ └── PregLibrary.php ├── Sniffs │ ├── WhiteSpace │ │ ├── ConstantSpacingSniff.php │ │ └── MultipleEmptyLinesSniff.php │ ├── Arrays │ │ ├── MultiLineArraySniff.php │ │ └── ArrayDoubleArrowAlignmentSniff.php │ ├── Strings │ │ └── VariableInDoubleQuotedStringSniff.php │ ├── Commenting │ │ └── PropertyCommentSniff.php │ └── Formatting │ │ ├── AlphabeticalUseStatementsSniff.php │ │ └── UnnecessaryNamespaceUsageSniff.php └── ruleset.xml ├── phpstan.neon ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md └── workflows │ └── ci.yml ├── .pinact.yaml ├── phpcs.mo4.xml ├── tests ├── static_analysis_bootstrap.php └── bootstrap.php ├── phpcs.xml.dist ├── psalm.xml ├── .gitignore ├── phpunit.xml.dist ├── LICENSE ├── .phan └── config.php ├── composer.json ├── integrationtests └── testfile.php ├── CHANGELOG.md └── README.md /.qlty/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !configs 3 | !configs/** 4 | !hooks 5 | !hooks/** 6 | !qlty.toml 7 | !.gitignore 8 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/UnnecessaryNamespaceUsageUnitTest.pass.3.inc: -------------------------------------------------------------------------------- 1 | b(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MO4/Tests/Strings/VariableInDoubleQuotedStringUnitTest.fail.inc: -------------------------------------------------------------------------------- 1 | bar(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MO4/Tests/Strings/VariableInDoubleQuotedStringUnitTest.fail.inc.fixed: -------------------------------------------------------------------------------- 1 | 12 | 13 | ### Description 14 | 15 | Please explain what you want to change and why. 16 | Additional information about the code and the commits 17 | may be helpful as well. 18 | -------------------------------------------------------------------------------- /MO4/Tests/WhiteSpace/MultipleEmptyLinesUnitTest.pass.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for PHP_CodeSniffer itself. 4 | 5 | 6 | MO4 7 | tests 8 | 9 | */Tests/*\.(inc|css|js) 10 | vendor/* 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /MO4/Tests/WhiteSpace/MultipleEmptyLinesUnitTest.fail.inc: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @license http://spdx.org/licenses/MIT MIT License 11 | * 12 | * @link https://github.com/mayflower/mo4-coding-standard 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | require_once __DIR__.'/../vendor/squizlabs/php_codesniffer/autoload.php'; 18 | require_once __DIR__.'/../vendor/squizlabs/php_codesniffer/src/Util/Tokens.php'; 19 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.fail.2.inc: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for PHP_CodeSniffer itself, applied to MO4. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /vendor/ 3 | /composer.phar 4 | composer.lock 5 | 6 | # Build & Tests 7 | /build/ 8 | .atoum.php 9 | phpunit.xml 10 | .phpunit.result.cache 11 | behat.yml 12 | 13 | # OSX 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | Icon 18 | .Spotlight-V100 19 | .Trashes 20 | 21 | # Windows image file caches 22 | Thumbs.db 23 | ehthumbs.db 24 | Desktop.ini 25 | $RECYCLE.BIN/ 26 | 27 | # Netbeans 28 | nbproject/private/ 29 | build/ 30 | nbbuild/ 31 | dist/ 32 | nbdist/ 33 | nbactions.xml 34 | nb-configuration.xml 35 | 36 | # Phpstorm 37 | .idea/ 38 | 39 | # SublimeText 40 | *.sublime-workspace 41 | 42 | # TextMate 43 | *.tmproj 44 | *.tmproject 45 | tmtags 46 | 47 | #vi & emacs 48 | *~ 49 | -------------------------------------------------------------------------------- /MO4/Tests/Strings/VariableInDoubleQuotedStringUnitTest.pass.inc: -------------------------------------------------------------------------------- 1 | getComputedPath($inputFile->getKey())}\n"; 17 | $k = "{$a[$b]}"; 18 | $l = "{$a[$b->foo()]}"; 19 | $m = "{$a[$b->foo($c)]}"; 20 | $n = "{$a[$b[0]][$b[0]]}"; 21 | $o = "{$a['foo'][$b[0]]}"; 22 | $p = "{$a[$b->foo($c['foo'][$d[0]])]}"; 23 | $p = "{$good[$fail]}"; 24 | $q = "{$prefix}/{$this->resolveName($id)}"; 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | vendor/squizlabs/php_codesniffer/tests/Standards/AllSniffs.php 12 | 13 | 14 | 15 | 16 | ./MO4/Sniffs 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/MultiLineArrayUnitTest.fail.inc: -------------------------------------------------------------------------------- 1 | [ 8 | 'object_id' => 1, 9 | 'object_class' => 'Test', 10 | 'object_class_name' => 'TestClass', 11 | 'object_name' => 'TestObject', 12 | 'id' => 'Test:1',], 13 | 'Test:2' => [ 14 | 'object_id' => 2, 15 | 'object_class' => 'Test', 16 | 'object_class_name' => 'TestClass', 17 | 'object_name' => 'TestObject', 18 | 'id' => 'Test:2',],]; 19 | 20 | 21 | $fail = array( 22 | 1); 23 | 24 | $fail = array(1, 25 | 2, 26 | ); 27 | 28 | $fail = [ 1, 29 | 2, 30 | ]; 31 | 32 | $fail = array( // fail 33 | ); 34 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/UnnecessaryNamespaceUsageUnitTest.fail.3.inc.fixed: -------------------------------------------------------------------------------- 1 | [ 9 | 'object_id' => 1, 10 | 'object_class' => 'Test', 11 | 'object_class_name' => 'TestClass', 12 | 'object_name' => 'TestObject', 13 | 'id' => 'Test:1', 14 | ], 15 | 'Test:2' => [ 16 | 'object_id' => 2, 17 | 'object_class' => 'Test', 18 | 'object_class_name' => 'TestClass', 19 | 'object_name' => 'TestObject', 20 | 'id' => 'Test:2', 21 | ], 22 | ]; 23 | 24 | 25 | $fail = array( 26 | 1 27 | ); 28 | 29 | $fail = array( 30 | 1, 31 | 2, 32 | ); 33 | 34 | $fail = [ 35 | 1, 36 | 2, 37 | ]; 38 | 39 | $fail = array( 40 | // fail 41 | ); 42 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/UnnecessaryNamespaceUsageUnitTest.fail.3.inc: -------------------------------------------------------------------------------- 1 | bar arrays 14 | */ 15 | private $_privateProperty = [['foo' => 'bar']]; 16 | 17 | /** 18 | * @var 19 | * 20 | * foo 21 | * 22 | * @var array 23 | */ 24 | private $_tmi; 25 | 26 | /** stuff */ 27 | private $_moar; 28 | 29 | /* 30 | * no doc block 31 | */ 32 | private $_priv; 33 | 34 | // no doc block 35 | private $_otherPriv; 36 | 37 | private $z = null; /** invalid 38 | multiline 39 | comment */ 40 | 41 | private $y = null; /** invalid doc block after declaration */ 42 | private $q = null; /* valid single line comment */ 43 | private $x = null; 44 | private $u = null; /* invalid 45 | multiline 46 | comment */ 47 | } 48 | -------------------------------------------------------------------------------- /MO4/Tests/Commenting/PropertyCommentUnitTest.fail.inc.fixed: -------------------------------------------------------------------------------- 1 | bar arrays 16 | */ 17 | private $_privateProperty = [['foo' => 'bar']]; 18 | 19 | /** 20 | * @var 21 | * 22 | * foo 23 | * 24 | * @var array 25 | */ 26 | private $_tmi; 27 | 28 | /** stuff */ 29 | private $_moar; 30 | 31 | /* 32 | * no doc block 33 | */ 34 | private $_priv; 35 | 36 | // no doc block 37 | private $_otherPriv; 38 | 39 | private $z = null; /** invalid 40 | multiline 41 | comment */ 42 | 43 | private $y = null; /** invalid doc block after declaration */ 44 | private $q = null; /* valid single line comment */ 45 | private $x = null; 46 | private $u = null; /* invalid 47 | multiline 48 | comment */ 49 | } 50 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.pass.inc: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @license http://spdx.org/licenses/MIT MIT License 11 | * 12 | * @link https://github.com/mayflower/mo4-coding-standard 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | use PHP_CodeSniffer\Config; 18 | use PHP_CodeSniffer\Util\Standards; 19 | 20 | $myStandardName = 'MO4'; 21 | 22 | require_once __DIR__.'/../vendor/squizlabs/php_codesniffer/tests/bootstrap.php'; 23 | 24 | // Add this Standard. 25 | Config::setConfigData( 26 | 'installed_paths', 27 | __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.$myStandardName, 28 | true 29 | ); 30 | 31 | // Ignore all other Standards in tests. 32 | $standards = Standards::getInstalledStandards(); 33 | $standards[] = 'Generic'; 34 | 35 | $ignoredStandardsStr = implode( 36 | ',', 37 | array_filter( 38 | $standards, 39 | static function ($v) use ($myStandardName): bool { 40 | return $v !== $myStandardName; 41 | } 42 | ) 43 | ); 44 | 45 | putenv("PHPCS_IGNORE_TESTS={$ignoredStandardsStr}"); 46 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/UnnecessaryNamespaceUsageUnitTest.fail.1.inc: -------------------------------------------------------------------------------- 1 | bar arrays 23 | * 24 | * @var array 25 | */ 26 | private $_privateProperty = [['foo' => 'bar']]; 27 | 28 | /** 29 | * @var int 30 | * 31 | * @ORM\Column(name="SECT_ID", type="integer", nullable=false) 32 | * @ORM\Id 33 | */ 34 | protected $id; 35 | 36 | /** 37 | * foo 38 | */ 39 | public function foo() 40 | { 41 | $foo = 2; 42 | /** @var int $bla */ 43 | $bla = 1; 44 | /** @var int $bla */ 45 | 46 | return ($bla === $foo); /* valid single line comment */ 47 | } 48 | 49 | private $_endThing; 50 | 51 | /** 52 | * stuff 53 | */ 54 | const STUFF = 1; 55 | 56 | public $stuff = [self::STUFF]; 57 | } 58 | -------------------------------------------------------------------------------- /MO4/Tests/WhiteSpace/MultipleEmptyLinesUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\WhiteSpace; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the MultipleEmptyLines sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class MultipleEmptyLinesUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'MultipleEmptyLinesUnitTest.pass.inc' => [], 37 | 'MultipleEmptyLinesUnitTest.fail.inc' => [ 38 | 2 => 1, 39 | 14 => 1, 40 | 21 => 1, 41 | 24 => 1, 42 | 29 => 1, 43 | ], 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/MultiLineArrayUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Arrays; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for @see MultiLineArraySniff 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class MultiLineArrayUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'MultiLineArrayUnitTest.pass.inc' => [], 37 | 'MultiLineArrayUnitTest.fail.inc' => [ 38 | 4 => 1, 39 | 12 => 1, 40 | 18 => 2, 41 | 22 => 1, 42 | 24 => 1, 43 | 28 => 1, 44 | 32 => 1, 45 | ], 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /MO4/Tests/Commenting/PropertyCommentUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Commenting; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the AlphabeticalUseStatements sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2014-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class PropertyCommentUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'PropertyCommentUnitTest.pass.inc' => [], 37 | 'PropertyCommentUnitTest.fail.inc' => [ 38 | 7 => 1, 39 | 10 => 1, 40 | 17 => 1, 41 | 26 => 2, 42 | 29 => 1, 43 | 34 => 1, 44 | 37 => 2, 45 | 41 => 1, 46 | 44 => 1, 47 | ], 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /MO4/Tests/Strings/VariableInDoubleQuotedStringUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Strings; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the VariableInDoubleQuotedString sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class VariableInDoubleQuotedStringUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'VariableInDoubleQuotedStringUnitTest.pass.inc' => [], 37 | 'VariableInDoubleQuotedStringUnitTest.fail.inc' => [ 38 | 3 => 1, 39 | 4 => 1, 40 | 5 => 2, 41 | 6 => 2, 42 | 7 => 1, 43 | 8 => 1, 44 | 9 => 1, 45 | 10 => 1, 46 | 11 => 1, 47 | ], 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /MO4/Tests/WhiteSpace/ConstantSpacingUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\WhiteSpace; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the VariableInDoubleQuotedString sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class ConstantSpacingUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'ConstantSpacingUnitTest.pass.inc' => [], 37 | 'ConstantSpacingUnitTest.fail.inc' => [ 38 | 4 => 1, 39 | 5 => 1, 40 | 6 => 1, 41 | 10 => 1, 42 | 12 => 1, 43 | 13 => 1, 44 | 14 => 1, 45 | 15 => 1, 46 | 18 => 1, 47 | 22 => 1, 48 | 23 => 1, 49 | 24 => 1, 50 | ], 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /.phan/config.php: -------------------------------------------------------------------------------- 1 | null, 14 | 15 | // A list of directories that should be parsed for class and 16 | // method information. After excluding the directories 17 | // defined in exclude_analysis_directory_list, the remaining 18 | // files will be statically analyzed for errors. 19 | // 20 | // Thus, both first-party and third-party code being used by 21 | // your application should be included in this list. 22 | 'directory_list' => [ 23 | 'MO4', 24 | 'tests', 25 | 'vendor/squizlabs/php_codesniffer', 26 | 'vendor/symfony/polyfill-php83', 27 | ], 28 | 29 | // A directory list that defines files that will be excluded 30 | // from static analysis, but whose class and method 31 | // information should be included. 32 | // 33 | // Generally, you'll want to include the directories for 34 | // third-party code (such as "vendor/") in this list. 35 | // 36 | // n.b.: If you'd like to parse but not analyze 3rd 37 | // party code, directories containing that code 38 | // should be added to both the `directory_list` 39 | // and `exclude_analysis_directory_list` arrays. 40 | "exclude_analysis_directory_list" => [ 41 | 'vendor/' 42 | ], 43 | ]; 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mayflower/mo4-coding-standard", 3 | "description": "PHP CodeSniffer ruleset implementing the MO4 coding standards extending the Symfony coding standards.", 4 | "license": "MIT", 5 | "type": "phpcodesniffer-standard", 6 | "keywords": [ 7 | "phpcs", 8 | "static analysis", 9 | "Symfony", 10 | "mo4", 11 | "standards", 12 | "PSR" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Xaver Loppenstedt", 17 | "email": "xaver@loppenstedt.de" 18 | } 19 | ], 20 | "homepage": "https://github.com/mayflower/mo4-coding-standard", 21 | "support": { 22 | "issues": "https://github.com/mayflower/mo4-coding-standard/issues", 23 | "source": "https://github.com/mayflower/mo4-coding-standard" 24 | }, 25 | "require": { 26 | "php": "^8.1", 27 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", 28 | "escapestudios/symfony2-coding-standard": "^3.16.0", 29 | "slevomat/coding-standard": "^8.14", 30 | "squizlabs/php_codesniffer": "^3.8.0" 31 | }, 32 | "require-dev": { 33 | "ergebnis/composer-normalize": "^2.45", 34 | "nikic/php-parser": "< 5.0.1", 35 | "phan/phan": "^5.4.5", 36 | "phpstan/phpstan": "^2.0", 37 | "phpstan/phpstan-strict-rules": "^2.0", 38 | "phpunit/phpunit": "^9.6.15", 39 | "psalm/plugin-phpunit": "^0.19", 40 | "sabre/event": ">= 5.1.6", 41 | "symfony/filesystem": ">= 5.4.45", 42 | "symfony/polyfill-php83": "^1.32", 43 | "vimeo/psalm": "^6.0.0" 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "dealerdirect/phpcodesniffer-composer-installer": true, 48 | "ergebnis/composer-normalize": true 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.qlty/qlty.toml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by `qlty init`. 2 | # You can modify it to suit your needs. 3 | # We recommend you to commit this file to your repository. 4 | # 5 | # This configuration is used by both Qlty CLI and Qlty Cloud. 6 | # 7 | # Qlty CLI -- Code quality toolkit for developers 8 | # Qlty Cloud -- Fully automated Code Health Platform 9 | # 10 | # Try Qlty Cloud: https://qlty.sh 11 | # 12 | # For a guide to configuration, visit https://qlty.sh/d/config 13 | # Or for a full reference, visit https://qlty.sh/d/qlty-toml 14 | config_version = "0" 15 | 16 | exclude_patterns = [ 17 | "*_min.*", 18 | "*-min.*", 19 | "*.min.*", 20 | "**/.yarn/**", 21 | "**/*.d.ts", 22 | "**/assets/**", 23 | "**/bower_components/**", 24 | "**/build/**", 25 | "**/cache/**", 26 | "**/config/**", 27 | "**/db/**", 28 | "**/deps/**", 29 | "**/dist/**", 30 | "**/extern/**", 31 | "**/external/**", 32 | "**/generated/**", 33 | "**/Godeps/**", 34 | "**/gradlew/**", 35 | "**/mvnw/**", 36 | "**/node_modules/**", 37 | "**/protos/**", 38 | "**/seed/**", 39 | "**/target/**", 40 | "**/templates/**", 41 | "**/testdata/**", 42 | "**/vendor/**", "integrationtests/", 43 | ] 44 | 45 | test_patterns = [ 46 | "**/test/**", 47 | "**/spec/**", 48 | "**/*.test.*", 49 | "**/*.spec.*", 50 | "**/*_test.*", 51 | "**/*_spec.*", 52 | "**/test_*.*", 53 | "**/spec_*.*", 54 | ] 55 | 56 | [smells] 57 | mode = "comment" 58 | 59 | [smells.boolean_logic] 60 | threshold = 4 61 | 62 | [smells.file_complexity] 63 | threshold = 55 64 | 65 | [smells.return_statements] 66 | threshold = 4 67 | enabled = false 68 | 69 | [smells.nested_control_flow] 70 | threshold = 4 71 | 72 | [smells.function_parameters] 73 | threshold = 4 74 | 75 | [smells.function_complexity] 76 | threshold = 16 77 | 78 | [smells.duplication] 79 | enabled = true 80 | threshold = 20 81 | 82 | [[source]] 83 | name = "default" 84 | default = true 85 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/ArrayDoubleArrowAlignmentUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Arrays; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for @see ArrayDoubleArrowAlignmentSniff 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class ArrayDoubleArrowAlignmentUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'ArrayDoubleArrowAlignmentUnitTest.pass.inc' => [], 37 | 'ArrayDoubleArrowAlignmentUnitTest.fail.inc' => [ 38 | 5 => 1, 39 | 10 => 1, 40 | 17 => 2, 41 | 18 => 2, 42 | 22 => 1, 43 | 28 => 1, 44 | 38 => 1, 45 | 43 => 1, 46 | 45 => 1, 47 | 49 => 1, 48 | 51 => 1, 49 | 58 => 1, 50 | 59 => 1, 51 | 61 => 1, 52 | 67 => 1, 53 | 70 => 1, 54 | 71 => 1, 55 | 73 => 1, 56 | 82 => 1, 57 | 83 => 1, 58 | 85 => 1, 59 | 93 => 1, 60 | 94 => 1, 61 | 97 => 1, 62 | 105 => 1, 63 | 130 => 1, 64 | 132 => 1, 65 | 134 => 1, 66 | 136 => 2, 67 | 140 => 1, 68 | 141 => 1, 69 | 145 => 2, 70 | 149 => 1, 71 | ], 72 | ]; 73 | } 74 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/AlphabeticalUseStatementsUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Formatting; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the AlphabeticalUseStatements sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | */ 33 | final class AlphabeticalUseStatementsUnitTest extends AbstractMo4SniffUnitTest 34 | { 35 | protected $expectedErrorList = [ 36 | 'AlphabeticalUseStatementsUnitTest.pass.inc' => [], 37 | 'AlphabeticalUseStatementsUnitTest.pass.1.inc' => [], 38 | 'AlphabeticalUseStatementsUnitTest.fail.1.inc' => [ 39 | 4 => 1, 40 | 5 => 1, 41 | 8 => 1, 42 | 9 => 1, 43 | 12 => 1, 44 | ], 45 | // Take care, more than one fix will be applied. 46 | 'AlphabeticalUseStatementsUnitTest.fail.2.inc' => [ 47 | 6 => 1, 48 | 8 => 1, 49 | ], 50 | 'AlphabeticalUseStatementsUnitTest.fail.3.inc' => [ 51 | 7 => 1, 52 | 8 => 1, 53 | 10 => 1, 54 | 15 => 1, 55 | ], 56 | 'AlphabeticalUseStatementsUnitTest.fail.4.inc' => [ 57 | 4 => 1, 58 | 8 => 1, 59 | 13 => 1, 60 | 17 => 1, 61 | 20 => 1, 62 | 21 => 1, 63 | ], 64 | 'AlphabeticalUseStatementsUnitTest.fail.6.inc' => [5 => 1], 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you contribute code, please make sure it conforms to the 4 | MO4 coding standard 5 | and that the unit tests still pass. 6 | Whenever possible, add an auto fixer for coding standard violations. 7 | 8 | ## Setup 9 | 10 | We do recommend the following setup: 11 | 12 | * make sure that [Composer](https://getcomposer.org) is installed 13 | * clone this repository 14 | 15 | git clone https://github.com/mayflower/mo4-coding-standard.git 16 | 17 | * install all required dependencies 18 | 19 | composer install 20 | 21 | * be sure that [Xdebug](https://xdebug.org/) is installed, if you like to check code coverage. 22 | 23 | 24 | ## Coding Standard 25 | 26 | To check the coding standard, execute in the repository root: 27 | 28 | ./vendor/bin/phpcs 29 | 30 | `phpcs` might report that some coding standard issues can be fixed automatically. 31 | So give `phpcbf` a try and let it fix the issues for you: 32 | 33 | ./vendor/bin/phpcbf 34 | 35 | ## Tests 36 | 37 | To run the unit tests, execute in the repository root: 38 | 39 | ./vendor/bin/phpunit 40 | 41 | To run the integration tests, execute 42 | 43 | ./vendor/bin/phpcs --standard=MO4 integrationtests/*.php 44 | 45 | ## Static analysis 46 | 47 | We use [PHPStan](https://github.com/phpstan/phpstan) and [Phan](https://github.com/phan/phan), please refer to the 48 | respective documentation for installation instructions. 49 | 50 | ./vendor/bin/phpstan analyse 51 | ./vendor/bin/phan -i 52 | 53 | ## Code Coverage 54 | 55 | Make sure, that you write tests for your code. 56 | 57 | Testing code coverage with [PHPUnit](https://phpunit.de/) requires [Xdebug](https://xdebug.org/) to be enabled. 58 | 59 | You can generate a simple code coverage report by running in the repository root: 60 | 61 | ./vendor/bin/phpunit --coverage-text 62 | 63 | In the case that Xdebug is disabled by default 64 | 65 | php -d zend_extension=xdebug.so vendor/bin/phpunit --coverage-text 66 | 67 | will do the trick. 68 | 69 | Please refer to the [PHPUnit Manual](https://phpunit.de/documentation.html) for further information about code coverage. 70 | 71 | -------------------------------------------------------------------------------- /MO4/Library/PregLibrary.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Library; 16 | 17 | use PHP_CodeSniffer\Exceptions\RuntimeException; 18 | 19 | final class PregLibrary 20 | { 21 | /** 22 | * Split string by a regular expression 23 | * 24 | * @param string $pattern The pattern to search for, as a string. 25 | * @param string $subject The input string. 26 | * @param int $limit If specified, then only substrings up to limit are returned with the rest of the string 27 | * being placed in the last substring. A limit of -1, 0 or NULL means "no limit" and, as is 28 | * standard across PHP, you can use NULL to skip to the flags parameter. 29 | * @param int $flags Can be any combination of the following flags (combined with the | bitwise operator): 30 | * PREG_SPLIT_NO_EMPTY: If this flag is set, only non-empty pieces will be returned. 31 | * PREG_SPLIT_DELIM_CAPTURE: If this flag is set, parenthesized expression in the delimiter 32 | * pattern will be captured and returned as well. 33 | * PREG_SPLIT_OFFSET_CAPTURE: If this flag is set, for every occurring match the appendant 34 | * string offset will also be returned. 35 | * 36 | * @return array|array 37 | * 38 | * @throws RuntimeException 39 | * 40 | * @psalm-suppress ArgumentTypeCoercion 41 | */ 42 | public static function MO4PregSplit(string $pattern, string $subject, int $limit = -1, int $flags = 0): array 43 | { 44 | $pregSplitResult = \preg_split($pattern, $subject, $limit, $flags); 45 | 46 | // @phan-suppress-next-line PhanTypeComparisonToArray 47 | if (false === $pregSplitResult) { 48 | throw new RuntimeException('Unexpected Error in MO4 Coding Standard.'); 49 | } 50 | 51 | return $pregSplitResult; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MO4/Tests/Formatting/UnnecessaryNamespaceUsageUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests\Formatting; 16 | 17 | use MO4\Tests\AbstractMo4SniffUnitTest; 18 | 19 | /** 20 | * Unit test class for the UnnecessaryNamespaceUsageUnitTest sniff. 21 | * 22 | * A sniff unit test checks a .inc file for expected violations of a single 23 | * coding standard. Expected errors and warnings are stored in this class. 24 | * 25 | * @author Xaver Loppenstedt 26 | * @author Marco Jantke 27 | * @author Steffen Ritter 28 | * 29 | * @copyright 2013-21 Xaver Loppenstedt, some rights reserved. 30 | * 31 | * @license http://spdx.org/licenses/MIT MIT License 32 | * 33 | * @link https://github.com/mayflower/mo4-coding-standard 34 | */ 35 | final class UnnecessaryNamespaceUsageUnitTest extends AbstractMo4SniffUnitTest 36 | { 37 | protected $expectedWarningList = [ 38 | 'UnnecessaryNamespaceUsageUnitTest.pass.1.inc' => [], 39 | 'UnnecessaryNamespaceUsageUnitTest.pass.2.inc' => [], 40 | 'UnnecessaryNamespaceUsageUnitTest.pass.3.inc' => [], 41 | 'UnnecessaryNamespaceUsageUnitTest.pass.4.inc' => [], 42 | 'UnnecessaryNamespaceUsageUnitTest.pass.5.inc' => [], 43 | 'UnnecessaryNamespaceUsageUnitTest.pass.6.inc' => [], 44 | 'UnnecessaryNamespaceUsageUnitTest.fail.1.inc' => [ 45 | 17 => 1, 46 | 19 => 1, 47 | 24 => 1, 48 | 25 => 1, 49 | 26 => 2, 50 | 28 => 1, 51 | 30 => 2, 52 | 32 => 1, 53 | 33 => 1, 54 | 40 => 1, 55 | 44 => 1, 56 | 45 => 1, 57 | 46 => 1, 58 | 52 => 1, 59 | 56 => 1, 60 | ], 61 | 'UnnecessaryNamespaceUsageUnitTest.fail.2.inc' => [ 62 | 10 => 1, 63 | 11 => 1, 64 | ], 65 | 'UnnecessaryNamespaceUsageUnitTest.fail.3.inc' => [ 66 | 15 => 1, 67 | 16 => 1, 68 | 17 => 1, 69 | 18 => 1, 70 | 22 => 1, 71 | 23 => 1, 72 | 25 => 3, 73 | ], 74 | 'UnnecessaryNamespaceUsageUnitTest.fail.4.inc' => [], 75 | ]; 76 | } 77 | -------------------------------------------------------------------------------- /MO4/Sniffs/WhiteSpace/ConstantSpacingSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\WhiteSpace; 16 | 17 | use PHP_CodeSniffer\Files\File; 18 | use PHP_CodeSniffer\Sniffs\Sniff; 19 | 20 | /** 21 | * Multi Line Array sniff. 22 | * 23 | * @author Xaver Loppenstedt 24 | * 25 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 26 | * 27 | * @license http://spdx.org/licenses/MIT MIT License 28 | * 29 | * @link https://github.com/mayflower/mo4-coding-standard 30 | * 31 | * @psalm-api 32 | */ 33 | class ConstantSpacingSniff implements Sniff 34 | { 35 | /** 36 | * Define all types of arrays. 37 | * 38 | * @var array 39 | */ 40 | protected $arrayTokens = [ 41 | T_CONST, 42 | ]; 43 | 44 | /** 45 | * Registers the tokens that this sniff wants to listen for. 46 | * 47 | * @return array 48 | * 49 | * @see Tokens.php 50 | */ 51 | #[\Override] 52 | public function register(): array 53 | { 54 | return $this->arrayTokens; 55 | } 56 | 57 | /** 58 | * Processes this test, when one of its tokens is encountered. 59 | * 60 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 61 | * 62 | * @param File $phpcsFile The file being scanned. 63 | * @param int $stackPtr The position of the current token in 64 | * the stack passed in $tokens. 65 | * 66 | * @return void 67 | */ 68 | #[\Override] 69 | public function process(File $phpcsFile, $stackPtr): void 70 | { 71 | $tokens = $phpcsFile->getTokens(); 72 | $nextPtr = $stackPtr + 1; 73 | $next = $tokens[$nextPtr]['content']; 74 | 75 | if (T_WHITESPACE !== $tokens[$nextPtr]['code']) { 76 | return; 77 | } 78 | 79 | if (' ' === $next) { 80 | return; 81 | } 82 | 83 | $fix = $phpcsFile->addFixableError( 84 | 'Keyword const must be followed by a single space, but found %s', 85 | $stackPtr, 86 | 'Incorrect', 87 | [\strlen($next)] 88 | ); 89 | 90 | if (true !== $fix) { 91 | return; 92 | } 93 | 94 | $phpcsFile->fixer->replaceToken($nextPtr, ' '); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/ArrayDoubleArrowAlignmentUnitTest.pass.inc: -------------------------------------------------------------------------------- 1 | 1, 5 | 'four' => 4, 6 | ]; 7 | 8 | $a = [ 9 | 'one' => 1, 10 | 2, 11 | 'four' => 4, 12 | ]; 13 | 14 | $a = [ 15 | 'one' => 1, 16 | 2, 17 | 'four' /* comment */ => 4, 18 | ]; 19 | 20 | $a = [ 21 | 'one' => 1, 22 | 2, 23 | // comment 24 | 'four' => 4, 25 | ]; 26 | 27 | $a = [ 28 | 2, 29 | 'four' => 4, 30 | // comment 31 | 'one' => /* comment */ 32 | 1, 33 | ]; 34 | 35 | $a = array( 36 | 'short' => 1, 37 | 'verylongerkey' => 2, 38 | 'x' => 3, 39 | ); 40 | 41 | $a = [ 42 | 'short' => 1, 43 | 'verylongerkey' => 2, 44 | 'x' => 3, 45 | ]; 46 | 47 | $a = array( 48 | 'short' => 1, 49 | 'verylongerkey' => 2, 50 | 'array' => array( 51 | 'val1' => 1, 52 | 'val112' => 1, 53 | 'val1123' => 1, 54 | 'v' => 1, 55 | ), 56 | 'x' => 3, 57 | ); 58 | 59 | $a = [ 60 | 'short' => 1, 61 | 'verylongerkey' => 2, 62 | 'array' => [ 63 | 'val1' => 1, 64 | 'val112' => 1, 65 | 'val1123' => 1, 66 | 'v' => 1, 67 | ], 68 | 'x' => 3, 69 | ]; 70 | 71 | $a = [ 72 | 'short' => 1, 73 | 'verylongerkey' => 2, 74 | 'array' => array( 75 | 'val1' => 1, 76 | 'val112' => 1, 77 | 'val1123' => 1, 78 | 'v' => 1, 79 | ), 80 | 'x' => 3, 81 | ]; 82 | 83 | $a = array( 84 | 'short' => 1, 85 | 'verylongerkey' => 2, 86 | 'array' => [ 87 | 'val1' => 1, 88 | 'val112' => 1, 89 | 'val1123' => 1, 90 | 'v' => 1, 91 | ], 92 | 'x' => 3, 93 | ); 94 | 95 | $a = [ 96 | 0 => [1 => 'O', 0 => 'O'], 97 | 1 => [1 => 'O', 0 => 'I'], 98 | 2 => [1 => 'I', 0 => 'O'], 99 | 3 => [1 => 'I', 0 => 'I'], 100 | ]; 101 | 102 | $a = [ 103 | 0 => [1 => 'O', 0 => 'O'], 104 | 1 => [1 => 'O', 0 => 'I'], 105 | 2 => [1 => 'I', 0 => 'O'], 106 | 3 => [1 => 'I', 0 => 'I'], 107 | ]; 108 | 109 | $a = [ 110 | function () { 111 | $b = [ 112 | 'one' => 1, 113 | 'four' => 4, 114 | ]; 115 | foreach ($b as $k => $v) { 116 | } 117 | $bar = [ 118 | 'one' => 1, 119 | 'four' => 4, 120 | ]; 121 | foreach ($bar as $key => $value) { 122 | } 123 | }, 124 | 'func' => function () { 125 | }, 126 | 'one' => 1, 127 | 2, 128 | 'four' => 2, 129 | ]; 130 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/ArrayDoubleArrowAlignmentUnitTest.fail.inc: -------------------------------------------------------------------------------- 1 | 1, 6 | 'four' => 4, 7 | ]; 8 | 9 | $a = [ 10 | 'one' => 1, 11 | 2, 12 | 'four' => 4, 13 | ]; 14 | 15 | // bad 16 | $a = [ 17 | 'one' => 1, 'two' => 2, 18 | 'three' => 1, 'four' => 4, 19 | ]; 20 | 21 | $a = [ 22 | 'one' => 1, 23 | 2, 24 | 'four' /* comment */ => 4, 25 | ]; 26 | 27 | $a = [ 28 | 'one' => 1, 29 | 2, 30 | // comment 31 | 'four' => 4, 32 | ]; 33 | 34 | $a = [ 35 | 2, 36 | 'four' => 4, 37 | // comment 38 | 'one' => /* comment */ 39 | 1, 40 | ]; 41 | 42 | $a = array( 43 | 'short' => 1, 44 | 'verylongerkey' => 2, 45 | 'x' => 3, 46 | ); 47 | 48 | $a = [ 49 | 'short' => 1, 50 | 'verylongerkey' => 2, 51 | 'x' => 3, 52 | ]; 53 | 54 | $a = array( 55 | 'short' => 1, 56 | 'verylongerkey' => 2, 57 | 'array' => array( 58 | 'val1' => 1, 59 | 'val112' => 1, 60 | 'val11234567' => 1, 61 | 'v' => 1, 62 | ), 63 | 'x' => 3, 64 | ); 65 | 66 | $a = [ 67 | 'short' => 1, 68 | 'verylongerkey' => 2, 69 | 'array' => [ 70 | 'val1' => 1, 71 | 'val112' => 1, 72 | 'val1123456' => 1, 73 | 'v' => 1, 74 | ], 75 | 'x' => 3, 76 | ]; 77 | 78 | $a = [ 79 | 'short' => 1, 80 | 'verylongerkey' => 2, 81 | 'array' => array( 82 | 'val1' => 1, 83 | 'val112' => 1, 84 | 'val11234567' => 1, 85 | 'v' => 1, 86 | ), 87 | 'x' => 3, 88 | ]; 89 | 90 | $a = array( 91 | 'short' => 1, 92 | 'verylongerkey' => 2, 93 | 'array' => [ 94 | 'val1' => 1, 95 | 'val112' => 1, 96 | 'val1123' => 1, 97 | 'v' => 1, 98 | ], 99 | 'x' => 3, 100 | ); 101 | 102 | $a = [ 103 | 0 => [ 1 => 'O', 0 => 'O'], 104 | 1 => [ 1 => 'O', 0 => 'I'], 105 | 2 => [ 1 => 'I', 0 => 'O'], 106 | 3 => [ 1 => 'I', 0 => 'I'], 107 | ]; 108 | 109 | foreach ($x as $k => $v) { 110 | 111 | } 112 | 113 | $a = [ 114 | function () { 115 | $b = [ 116 | 'one' => 1, 117 | 'four' => 4, 118 | ]; 119 | foreach ($b as $k => $v) { 120 | } 121 | $bar = [ 122 | 'one' => 1, 123 | 'four' => 4, 124 | ]; 125 | foreach ($bar as $key => $value) { 126 | } 127 | }, 128 | function () { 129 | }, 130 | 'one' => 1, 131 | 2, 132 | 'four' => 2, 133 | 123456789 134 | /* bla */ => 'fail', 135 | 98765432 136 | => 'fail', 137 | ]; 138 | 139 | $a=[ 140 | 'v'=>1, 141 | 'vv'=>2, 142 | ]; 143 | // deal with syntax errors 144 | $aa = [ 145 | 'y' => [1, 2, 3] 'x' => 0 'z' => -1 146 | ]; 147 | 148 | $a=[ 149 | 'v'=>1 'vv'=>2 150 | ]; 151 | -------------------------------------------------------------------------------- /MO4/Tests/Arrays/ArrayDoubleArrowAlignmentUnitTest.fail.inc.fixed: -------------------------------------------------------------------------------- 1 | 1, 6 | 'four' => 4, 7 | ]; 8 | 9 | $a = [ 10 | 'one' => 1, 11 | 2, 12 | 'four' => 4, 13 | ]; 14 | 15 | // bad 16 | $a = [ 17 | 'one' => 1, 18 | 'two' => 2, 19 | 'three' => 1, 20 | 'four' => 4, 21 | ]; 22 | 23 | $a = [ 24 | 'one' => 1, 25 | 2, 26 | 'four' /* comment */ => 4, 27 | ]; 28 | 29 | $a = [ 30 | 'one' => 1, 31 | 2, 32 | // comment 33 | 'four' => 4, 34 | ]; 35 | 36 | $a = [ 37 | 2, 38 | 'four' => 4, 39 | // comment 40 | 'one' => /* comment */ 41 | 1, 42 | ]; 43 | 44 | $a = array( 45 | 'short' => 1, 46 | 'verylongerkey' => 2, 47 | 'x' => 3, 48 | ); 49 | 50 | $a = [ 51 | 'short' => 1, 52 | 'verylongerkey' => 2, 53 | 'x' => 3, 54 | ]; 55 | 56 | $a = array( 57 | 'short' => 1, 58 | 'verylongerkey' => 2, 59 | 'array' => array( 60 | 'val1' => 1, 61 | 'val112' => 1, 62 | 'val11234567' => 1, 63 | 'v' => 1, 64 | ), 65 | 'x' => 3, 66 | ); 67 | 68 | $a = [ 69 | 'short' => 1, 70 | 'verylongerkey' => 2, 71 | 'array' => [ 72 | 'val1' => 1, 73 | 'val112' => 1, 74 | 'val1123456' => 1, 75 | 'v' => 1, 76 | ], 77 | 'x' => 3, 78 | ]; 79 | 80 | $a = [ 81 | 'short' => 1, 82 | 'verylongerkey' => 2, 83 | 'array' => array( 84 | 'val1' => 1, 85 | 'val112' => 1, 86 | 'val11234567' => 1, 87 | 'v' => 1, 88 | ), 89 | 'x' => 3, 90 | ]; 91 | 92 | $a = array( 93 | 'short' => 1, 94 | 'verylongerkey' => 2, 95 | 'array' => [ 96 | 'val1' => 1, 97 | 'val112' => 1, 98 | 'val1123' => 1, 99 | 'v' => 1, 100 | ], 101 | 'x' => 3, 102 | ); 103 | 104 | $a = [ 105 | 0 => [ 1 => 'O', 0 => 'O'], 106 | 1 => [ 1 => 'O', 0 => 'I'], 107 | 2 => [ 1 => 'I', 0 => 'O'], 108 | 3 => [ 1 => 'I', 0 => 'I'], 109 | ]; 110 | 111 | foreach ($x as $k => $v) { 112 | 113 | } 114 | 115 | $a = [ 116 | function () { 117 | $b = [ 118 | 'one' => 1, 119 | 'four' => 4, 120 | ]; 121 | foreach ($b as $k => $v) { 122 | } 123 | $bar = [ 124 | 'one' => 1, 125 | 'four' => 4, 126 | ]; 127 | foreach ($bar as $key => $value) { 128 | } 129 | }, 130 | function () { 131 | }, 132 | 'one' => 1, 133 | 2, 134 | 'four' => 2, 135 | 123456789 /* bla */ => 'fail', 136 | 98765432 => 'fail', 137 | ]; 138 | 139 | $a=[ 140 | 'v' =>1, 141 | 'vv' =>2, 142 | ]; 143 | // deal with syntax errors 144 | $aa = [ 145 | 'y' => [1, 2, 3] 'x' => 0 'z' => -1 146 | ]; 147 | 148 | $a=[ 149 | 'v'=>1 'vv'=>2 150 | ]; 151 | -------------------------------------------------------------------------------- /MO4/Sniffs/WhiteSpace/MultipleEmptyLinesSniff.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | * @see Tokens.php 27 | */ 28 | #[\Override] 29 | public function register(): array 30 | { 31 | return [ 32 | // Assume most comments end with a newline 33 | T_COMMENT, 34 | // Assume all getTokens(); 52 | 53 | // This sniff intentionally doesn't care about whitespace at the end of the file 54 | if (!isset($tokens[$stackPtr + 3]) 55 | || $tokens[$stackPtr + 2]['line'] === $tokens[$stackPtr + 3]['line'] 56 | ) { 57 | return $stackPtr + 3; 58 | } 59 | 60 | if ($tokens[$stackPtr + 1]['line'] === $tokens[$stackPtr + 2]['line']) { 61 | return $stackPtr + 2; 62 | } 63 | 64 | // Finally, check the assumption the current token is or ends with a newline 65 | if ($tokens[$stackPtr]['line'] === $tokens[$stackPtr + 1]['line']) { 66 | return; 67 | } 68 | 69 | // Search for the next non-newline token 70 | $next = $stackPtr + 1; 71 | 72 | while (isset($tokens[$next + 1]) && 73 | $tokens[$next]['code'] === T_WHITESPACE && 74 | $tokens[$next]['line'] !== $tokens[$next + 1]['line'] 75 | ) { 76 | $next++; 77 | } 78 | 79 | $count = $next - $stackPtr - 1; 80 | 81 | if ($count > 1 82 | && $phpcsFile->addFixableError( 83 | 'Multiple empty lines should not exist in a row; found %s consecutive empty lines', 84 | $stackPtr + 1, 85 | 'MultipleEmptyLines', 86 | [$count] 87 | ) 88 | ) { 89 | $phpcsFile->fixer->beginChangeset(); 90 | 91 | // Remove all newlines except the first two, i.e. keep one empty line 92 | for ($i = $stackPtr + 2; $i < $next; $i++) { 93 | $phpcsFile->fixer->replaceToken($i, ''); 94 | } 95 | 96 | $phpcsFile->fixer->endChangeset(); 97 | } 98 | 99 | // Don't check the current sequence a second time 100 | return $next; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /integrationtests/testfile.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Acme; 15 | 16 | const BAM = 1; 17 | 18 | /** 19 | * Coding standards demonstration. 20 | */ 21 | class FooBar 22 | { 23 | public const SOME_CONST = 42; 24 | public const STR_CONST = '43'; 25 | protected const PROTECT = 0; 26 | public const LALA = 'lala'; 27 | 28 | /** 29 | * @var string 30 | */ 31 | private $fooBar; 32 | 33 | /** 34 | * @param string $dummy Some argument description 35 | */ 36 | public function __construct(string $dummy) 37 | { 38 | $this->fooBar = $this->transformText($dummy); 39 | } 40 | 41 | /** 42 | * @return string 43 | * 44 | * @deprecated 45 | */ 46 | public function someDeprecatedMethod(): string 47 | { 48 | @\trigger_error(\sprintf('The %s() method is deprecated since version 2.8 and will be removed in 3.0. Use Acme\Baz::someMethod() instead.', __METHOD__), E_USER_DEPRECATED); 49 | 50 | return Baz::someMethod(); 51 | } 52 | 53 | /** 54 | * Transforms the input given as first argument. 55 | * 56 | * @param bool|string $dummy Some argument description 57 | * @param array $options An options collection to be used within the transformation 58 | * 59 | * @return string|null The transformed input 60 | * 61 | * @throws \RuntimeException When an invalid option is provided 62 | */ 63 | private function transformText($dummy, array $options = []): ?string 64 | { 65 | /** @var array $defaultOptions */ 66 | $defaultOptions = [ 67 | 'some_default' => 'values', 68 | 'another_default' => 'more values', 69 | ]; 70 | 71 | foreach ($options as $option) { 72 | if (!\in_array($option, $defaultOptions)) { 73 | throw new \RuntimeException(\sprintf('Unrecognized option "%s"', $option)); 74 | } 75 | } 76 | 77 | $destructuredStuff = \array_flip(...$defaultOptions); 78 | 79 | $mergedOptions = \array_merge( 80 | $defaultOptions, 81 | $options 82 | ); 83 | 84 | if (true === $dummy) { 85 | return null; 86 | } 87 | 88 | if ('string' === $dummy) { 89 | if ('values' === $mergedOptions['some_default']) { 90 | return \substr($dummy, 0, 5); 91 | } 92 | 93 | return \ucwords($dummy); 94 | } 95 | 96 | return $destructuredStuff; 97 | } 98 | 99 | /** 100 | * Performs some basic check for a given value. 101 | * 102 | * @param mixed $value Some value to check against 103 | * @param bool $theSwitch Some switch to control the method's flow 104 | * 105 | * @return bool|void The resultant check if $theSwitch isn't false, void otherwise 106 | */ 107 | private function reverseBoolean($value = null, bool $theSwitch = false) 108 | { 109 | if (!$theSwitch) { 110 | return; 111 | } 112 | 113 | return !$value; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MO4/Sniffs/Arrays/MultiLineArraySniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Arrays; 16 | 17 | use PHP_CodeSniffer\Files\File; 18 | use PHP_CodeSniffer\Sniffs\Sniff; 19 | 20 | /** 21 | * Multi Line Array sniff. 22 | * 23 | * @author Xaver Loppenstedt 24 | * 25 | * @copyright 2013-2017 Xaver Loppenstedt, some rights reserved. 26 | * 27 | * @license http://spdx.org/licenses/MIT MIT License 28 | * 29 | * @link https://github.com/mayflower/mo4-coding-standard 30 | * 31 | * @psalm-api 32 | */ 33 | class MultiLineArraySniff implements Sniff 34 | { 35 | /** 36 | * Define all types of arrays. 37 | * 38 | * @var array 39 | */ 40 | protected $arrayTokens = [ 41 | // @phan-suppress-next-line PhanUndeclaredConstant 42 | T_OPEN_SHORT_ARRAY, 43 | T_ARRAY, 44 | ]; 45 | 46 | /** 47 | * Registers the tokens that this sniff wants to listen for. 48 | * 49 | * @return array 50 | * 51 | * @see Tokens.php 52 | */ 53 | #[\Override] 54 | public function register(): array 55 | { 56 | return $this->arrayTokens; 57 | } 58 | 59 | /** 60 | * Processes this test, when one of its tokens is encountered. 61 | * 62 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 63 | * 64 | * @param File $phpcsFile The file being scanned. 65 | * @param int $stackPtr The position of the current token in 66 | * the stack passed in $tokens. 67 | * 68 | * @return void 69 | */ 70 | #[\Override] 71 | public function process(File $phpcsFile, $stackPtr): void 72 | { 73 | $tokens = $phpcsFile->getTokens(); 74 | $current = $tokens[$stackPtr]; 75 | 76 | if (T_ARRAY === $current['code']) { 77 | $arrayType = 'parenthesis'; 78 | $start = $current['parenthesis_opener']; 79 | $end = $current['parenthesis_closer']; 80 | } else { 81 | $arrayType = 'bracket'; 82 | $start = $current['bracket_opener']; 83 | $end = $current['bracket_closer']; 84 | } 85 | 86 | if ($tokens[$start]['line'] === $tokens[$end]['line']) { 87 | return; 88 | } 89 | 90 | if ($tokens[($start + 2)]['line'] === $tokens[$start]['line']) { 91 | $fixable = $phpcsFile->addFixableError( 92 | \sprintf( 93 | 'opening %s of multi line array must be followed by newline', 94 | $arrayType 95 | ), 96 | $start, 97 | 'OpeningMustBeFollowedByNewline' 98 | ); 99 | 100 | if (true === $fixable) { 101 | $phpcsFile->fixer->beginChangeset(); 102 | $phpcsFile->fixer->addNewline($start); 103 | $phpcsFile->fixer->endChangeset(); 104 | } 105 | } 106 | 107 | if ($tokens[($end - 2)]['line'] !== $tokens[$end]['line']) { 108 | return; 109 | } 110 | 111 | $fixable = $phpcsFile->addFixableError( 112 | \sprintf( 113 | 'closing %s of multi line array must in own line', 114 | $arrayType 115 | ), 116 | $end, 117 | 'ClosingMustBeInOwnLine' 118 | ); 119 | 120 | if (true !== $fixable) { 121 | return; 122 | } 123 | 124 | $phpcsFile->fixer->beginChangeset(); 125 | $phpcsFile->fixer->addNewlineBefore($end); 126 | $phpcsFile->fixer->endChangeset(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /MO4/Tests/AbstractMo4SniffUnitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Tests; 16 | 17 | use PHP_CodeSniffer\Exceptions\RuntimeException; 18 | use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; 19 | 20 | /** 21 | * Abstract class to make the writing of tests more convenient. 22 | * 23 | * A sniff unit test checks a .inc file for expected violations of a single 24 | * coding standard. 25 | * 26 | * Expected errors and warnings are stored in the class fields $expectedErrorList 27 | * and $expectedWarningList 28 | * 29 | * @author Xaver Loppenstedt 30 | * 31 | * @copyright 2013-2021 Xaver Loppenstedt, some rights reserved. 32 | * 33 | * @license http://spdx.org/licenses/MIT MIT License 34 | * 35 | * @link https://github.com/mayflower/mo4-coding-standard 36 | */ 37 | abstract class AbstractMo4SniffUnitTest extends AbstractSniffUnitTest 38 | { 39 | /** 40 | * Array or Array containing the test file as key and as value the key-value pairs with line number and number of# 41 | * errors as describe in @see AbstractSniffUnitTest::getErrorList 42 | * 43 | * When the array is empty, the test will pass. 44 | * 45 | * @var array 46 | */ 47 | protected $expectedErrorList = []; 48 | 49 | /** 50 | * Array or Array containing the test file as key and as value the key-value pairs with line number and number of# 51 | * errors as describe in @see AbstractSniffUnitTest::getWarningList 52 | * 53 | * When the array is empty, the test will pass. 54 | * 55 | * @var array 56 | */ 57 | protected $expectedWarningList = []; 58 | 59 | /** 60 | * Returns the lines where errors should occur. 61 | * 62 | * The key of the array should represent the line number and the value 63 | * should represent the number of errors that should occur on that line. 64 | * 65 | * @param string $testFile test file 66 | * 67 | * @return array 68 | * 69 | * @throws RuntimeException 70 | */ 71 | #[\Override] 72 | protected function getErrorList(string $testFile = ''): array 73 | { 74 | return $this->getRecordForTestFile($testFile, $this->expectedErrorList); 75 | } 76 | 77 | /** 78 | * Returns the lines where warnings should occur. 79 | * 80 | * The key of the array should represent the line number and the value 81 | * should represent the number of warnings that should occur on that line. 82 | * 83 | * @param string $testFile test file 84 | * 85 | * @return array 86 | * 87 | * @throws RuntimeException 88 | */ 89 | #[\Override] 90 | protected function getWarningList(string $testFile = ''): array 91 | { 92 | return $this->getRecordForTestFile($testFile, $this->expectedWarningList); 93 | } 94 | 95 | /** 96 | * Returns the lines where warnings should occur for the error or warning list. 97 | * 98 | * The key of the array should represent the line number and the value 99 | * should represent the number of warnings that should occur on that line. 100 | * 101 | * @param string $testFile test file 102 | * @param array $list record for test file 103 | * 104 | * @return array 105 | * 106 | * @throws RuntimeException 107 | */ 108 | private function getRecordForTestFile(string $testFile, array $list): array 109 | { 110 | if ([] === $list) { 111 | return []; 112 | } 113 | 114 | if (!\array_key_exists($testFile, $list)) { 115 | throw new RuntimeException( 116 | \sprintf('%s%s is not handled by %s', \sprintf('Testfile %s in ', $testFile), __DIR__, self::class) 117 | ); 118 | } 119 | 120 | return $list[$testFile]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /MO4/Sniffs/Strings/VariableInDoubleQuotedStringSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Strings; 16 | 17 | use PHP_CodeSniffer\Files\File; 18 | use PHP_CodeSniffer\Sniffs\Sniff; 19 | 20 | /** 21 | * Variable in Double Quoted String sniff. 22 | * 23 | * Variables in double quoted strings must be surrounded by { } 24 | * 25 | * @author Xaver Loppenstedt 26 | * 27 | * @copyright 2013 Xaver Loppenstedt, some rights reserved. 28 | * 29 | * @license http://spdx.org/licenses/MIT MIT License 30 | * 31 | * @link https://github.com/mayflower/mo4-coding-standard 32 | * 33 | * @psalm-api 34 | */ 35 | class VariableInDoubleQuotedStringSniff implements Sniff 36 | { 37 | /** 38 | * Registers the tokens that this sniff wants to listen for. 39 | * 40 | * @return array 41 | * 42 | * @see Tokens.php 43 | */ 44 | #[\Override] 45 | public function register(): array 46 | { 47 | return [T_DOUBLE_QUOTED_STRING]; 48 | } 49 | 50 | /** 51 | * Called when one of the token types that this sniff is listening for 52 | * is found. 53 | * 54 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 55 | * 56 | * @param File $phpcsFile The PHP_CodeSniffer file where the 57 | * token was found. 58 | * @param int $stackPtr The position in the PHP_CodeSniffer 59 | * file's token stack where the token 60 | * was found. 61 | * 62 | * @return void 63 | */ 64 | #[\Override] 65 | public function process(File $phpcsFile, $stackPtr): void 66 | { 67 | $varRegExp = '/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/'; 68 | 69 | $tokens = $phpcsFile->getTokens(); 70 | $content = $tokens[$stackPtr]['content']; 71 | 72 | $matches = []; 73 | 74 | \preg_match_all($varRegExp, $content, $matches, PREG_OFFSET_CAPTURE); 75 | 76 | foreach ($matches as $match) { 77 | foreach ($match as [$var, $pos]) { 78 | if (1 !== $pos && '{' === $content[($pos - 1)]) { 79 | continue; 80 | } 81 | 82 | if (\strpos(\substr($content, 0, $pos), '{') > 0 83 | && !\str_contains(\substr($content, 0, $pos), '}') 84 | ) { 85 | continue; 86 | } 87 | 88 | $lastOpeningBrace = \strrpos(\substr($content, 0, $pos), '{'); 89 | 90 | if (false !== $lastOpeningBrace 91 | && '$' === $content[($lastOpeningBrace + 1)] 92 | ) { 93 | $lastClosingBrace = \strrpos(\substr($content, 0, $pos), '}'); 94 | 95 | if (false !== $lastClosingBrace 96 | && $lastClosingBrace < $lastOpeningBrace 97 | ) { 98 | continue; 99 | } 100 | } 101 | 102 | $fix = $phpcsFile->addFixableError( 103 | \sprintf( 104 | 'must surround variable %s with { }', 105 | $var 106 | ), 107 | $stackPtr, 108 | 'NotSurroundedWithBraces' 109 | ); 110 | 111 | if (true !== $fix) { 112 | continue; 113 | } 114 | 115 | $correctVariable = $this->surroundVariableWithBraces( 116 | $content, 117 | $pos, 118 | $var 119 | ); 120 | 121 | $this->fixPhpCsFile($stackPtr, $correctVariable, $phpcsFile); 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Surrounds a variable with curly brackets 128 | * 129 | * @param string $content content 130 | * @param int $pos position 131 | * @param string $var variable 132 | * 133 | * @return string 134 | */ 135 | private function surroundVariableWithBraces(string $content, int $pos, string $var): string 136 | { 137 | $before = \substr($content, 0, $pos); 138 | $after = \substr($content, ($pos + \strlen($var))); 139 | 140 | return $before.'{'.$var.'}'.$after; 141 | } 142 | 143 | /** 144 | * Fixes the file 145 | * 146 | * @param int $stackPtr stack pointer 147 | * @param string $correctVariable correct variable 148 | * @param File $phpCsFile PHP_CodeSniffer File object 149 | * 150 | * @return void 151 | */ 152 | private function fixPhpCsFile(int $stackPtr, string $correctVariable, File $phpCsFile): void 153 | { 154 | $phpCsFile->fixer->beginChangeset(); 155 | $phpCsFile->fixer->replaceToken($stackPtr, $correctVariable); 156 | $phpCsFile->fixer->endChangeset(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: MO4 Coding Standard CI 2 | on: 3 | pull_request: 4 | push: 5 | schedule: 6 | - cron: '30 5 1 * *' 7 | 8 | jobs: 9 | style-checks: 10 | env: 11 | XMLLINT_INDENT: " " 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: install dependencies 15 | run: sudo apt update -qq && sudo apt -y install libxml2-utils 16 | - name: Check out repository code 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | - name: Install PHP 19 | uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # master 20 | with: 21 | php-version: '8.1' 22 | coverage: 'none' 23 | - name: Cache dependencies 24 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 25 | with: 26 | path: '~/.cache/composer' 27 | key: "cache-composer-${{ hashFiles('composer.json') }}" 28 | restore-keys: 'cache-composer-' 29 | - name: run composer 30 | run: composer update --prefer-lowest --prefer-dist --no-interaction --no-progress 31 | - run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd MO4/ruleset.xml 32 | - run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd phpcs.mo4.xml 33 | - run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd phpcs.xml.dist 34 | - run: diff -B MO4/ruleset.xml <(xmllint --format MO4/ruleset.xml) 35 | - run: diff -B phpcs.mo4.xml <(xmllint --format phpcs.mo4.xml) 36 | - run: diff -B phpcs.xml.dist <(xmllint --format phpcs.xml.dist) 37 | - name: Stylecheck against MO4 itself 38 | run: vendor/bin/phpcs 39 | run-tests: 40 | strategy: 41 | matrix: 42 | os: [ubuntu-latest] 43 | php_version: 44 | - 8.1 45 | - 8.2 46 | - 8.3 47 | - 8.4 48 | dependencies_level: 49 | - --prefer-lowest 50 | - "" 51 | include: 52 | - os: windows-latest 53 | php_version: 8.1 54 | dependencies_level: --prefer-lowest 55 | - os: windows-latest 56 | php_version: 8.1 57 | dependencies_level: '' 58 | runs-on: ${{ matrix.os }} 59 | steps: 60 | - name: Set git to use LF on windows 61 | if: ${{ matrix.os == 'windows-latest' }} 62 | run: | 63 | git config --global core.autocrlf false 64 | git config --global core.eol lf 65 | - name: Check out repository code 66 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 67 | - name: Install PHP 68 | uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # master 69 | with: 70 | coverage: 'xdebug' 71 | php-version: ${{ matrix.php_version }} 72 | extensions: ast-1.1.1 73 | - name: Cache dependencies 74 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 75 | with: 76 | path: '~/.cache/composer' 77 | key: "cache-composer-${{ hashFiles('composer.json') }}" 78 | restore-keys: 'cache-composer-' 79 | - name: Run composer 80 | run: composer update ${{ matrix.dependencies_level }} --prefer-dist --no-interaction --no-progress 81 | - name: Check composer.json 82 | run: composer normalize --dry-run 83 | - name: Run tests 84 | run: vendor/bin/phpunit 85 | - name: Run integration tests 86 | run: vendor/bin/phpcs -s --standard=MO4 integrationtests/testfile.php 87 | - name: Run PHPStan 88 | run: vendor/bin/phpstan analyse --no-progress 89 | - name: Run psalm 90 | if: ${{ matrix.os != 'windows-latest' }} 91 | run: vendor/bin/psalm 92 | - name: Run phan 93 | if: ${{ matrix.os != 'windows-latest' }} 94 | run: vendor/bin/phan 95 | # AST 1.1 binary for Windows seems to be missing on PECL 96 | - name: Run phan with polyfill 97 | if: ${{ matrix.os == 'windows-latest' }} 98 | run: vendor/bin/phan --allow-polyfill-parser 99 | - name: Run tests with coverage 100 | if: ${{ matrix.os != 'windows-latest' && matrix.php_version == '8.1' && matrix.dependencies_level != '--prefer-lowest' }} 101 | run: php vendor/bin/phpunit --coverage-clover=coverage.xml --coverage-cobertura=cobertura.xml --log-junit=junit.xml 102 | - name: Upload coverage to Codecov 103 | if: ${{ matrix.os != 'windows-latest' && matrix.php_version == '8.1' && matrix.dependencies_level != '--prefer-lowest' }} 104 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 105 | with: 106 | token: ${{ secrets.CODECOV_TOKEN }} 107 | - name: Upload test results to Codecov 108 | if: ${{ matrix.os != 'windows-latest' && matrix.php_version == '8.1' && matrix.dependencies_level != '--prefer-lowest' }} 109 | uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 110 | with: 111 | token: ${{ secrets.CODECOV_TOKEN }} 112 | - name: Upload coverage to Qlty 113 | if: ${{ matrix.os != 'windows-latest' && matrix.php_version == '8.1' && matrix.dependencies_level != '--prefer-lowest' }} 114 | uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 115 | with: 116 | token: ${{ secrets.QLTY_COVERAGE_TOKEN }} 117 | files: cobertura.xml 118 | strip-prefix: '/home/runner/work/mo4-coding-standard' 119 | add-prefix: 'MO4/Sniffs/' 120 | env: 121 | QLTY_COVERAGE_TOKEN: ${{ secrets.QLTY_COVERAGE_TOKEN }} 122 | 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [10.0.2] - 2025-07-28 7 | ### Changed 8 | - Raised minimum Symfony CS release, fixes #218 9 | 10 | ## [10.0.1] - 2024-12-16 11 | ### Changed 12 | - Fix uninitialized string offset in UnnecessaryNamespaceUsage, fixes #212 13 | 14 | ## [10.0.0] - 2024-03-13 15 | ### Changed 16 | Allow dealerdirect/phpcodesniffer-composer-installer 1.x 17 | ### Added 18 | - Add `SlevomatCodingStandard.Arrays.ArrayAccess` 19 | - Add `SlevomatCodingStandard.Attributes.AttributesOrder` 20 | - Add `SlevomatCodingStandard.Attributes.DisallowAttributesJoining` 21 | - Add `SlevomatCodingStandard.Attributes.DisallowMultipleAttributesPerLine` 22 | - Add `SlevomatCodingStandard.Classes.EnumCaseSpacing` 23 | - Add `SlevomatCodingStandard.Functions.NamedArgumentSpacing` 24 | 25 | ## [9.0.1] - 2023-12-12 26 | ### Changed 27 | - Fix invalid property case (fixes a runtime error when using PHPCS 3.8.0+) 28 | 29 | ## [9.0.0] - 2022-08-23 30 | ### Added 31 | - Add PHP 8.1 support 32 | - Add `SlevomatCodingStandard.Classes.BackedEnumTypeSpacing` 33 | - Add `SlevomatCodingStandard.Classes.ForbiddenPublicProperty` 34 | 35 | ## [8.0.0] - 2021-08-31 36 | ### Added 37 | - Add `MO4.WhiteSpace.ConstantSpacing` 38 | - Add `MO4.WhiteSpace.MultipleEmptyLinesSniff` 39 | ### Changed 40 | - refactored tests 41 | 42 | ## [7.0.0] - 2021-04-22 43 | ### Added 44 | - Add PHP 8.0 support 45 | - Add `Generic.WhiteSpace.ArbitraryParenthesesSpacing` 46 | - Add `Squiz.PHP.DisallowMultipleAssignments` 47 | - Add `SlevomatCodingStandard.Arrays.SingleLineArrayWhitespace` 48 | - Add `SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly` 49 | - Add `SlevomatCodingStandard.Functions.UnusedParameter` 50 | - Add `SlevomatCodingStandard.Functions.UselessParameterDefaultValue` 51 | ### Changed 52 | - Raised minimum PHPCS release 53 | - Raised minimum Slevomat CS release 54 | 55 | ## [6.0.0] - 2020-10-29 56 | ### Added 57 | - Add `SlevomatCodingStandard.Classes.DisallowMultiConstantDefinition` 58 | - Add `SlevomatCodingStandard.Commenting.UselessInheritDocComment` 59 | - Add `SlevomatCodingStandard.Functions.StaticClosure` 60 | - Add `SlevomatCodingStandard.Namespaces.UseSpacing` 61 | - Add `PSR12` (excluding conflicting rules) 62 | ### Changed 63 | - Raised PHP requirement to 7.2 64 | 65 | ## [5.0.0] - 2019-12-16 66 | ### Added 67 | - Add `Squiz.WhiteSpace.FunctionSpacing` 68 | - Add `Squiz.WhiteSpace.MemberVarSpacing` 69 | - Add `PSR12.Traits.UseDeclaration` 70 | - Add `SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions` 71 | ### Changed 72 | - Raised minimum Slevomat CS release and adapt MO4 ruleset 73 | - Raised minimum Symfony CS release 74 | - Raised minimum PHPCS release 75 | - Support PHP 7.4 76 | 77 | ## [4.0.0] - 2019-09-25 78 | ### Added 79 | - New sniffs from Slevomat, see comments in [the ruleset](https://github.com/mayflower/mo4-coding-standard/blob/master/MO4/ruleset.xml) 80 | ### Changed 81 | - Raised minimum Symfony CS release 82 | 83 | ## [3.2.2] - 2019-08-29 84 | ### Changed 85 | - Add `@group` to default `ignoredAnnotationNames` 86 | - Add `ignoreMultiline` for if vs. ternary 87 | 88 | ## [3.2.1] - 2019-07-01 89 | ### Changed 90 | - Raised minimum Symfony CS release 91 | - Use strict typing in MO4 sniff code 92 | 93 | ## [3.2.0] - 2019-04-23 94 | ### Changed 95 | - Raised minimum Symfony CS release 96 | 97 | ## [3.1.0] - 2019-04-14 98 | ### Changed 99 | - Raised minimum Symfony CS release 100 | - Raised minimum Slevomat CS release 101 | 102 | ## [3.0.1] - 2018-11-23 103 | ### Changed 104 | - Updated dealerdirect/phpcodesniffer-composer-installer dependency 105 | 106 | ## [3.0.0] - 2018-10-24 107 | ### Added 108 | - PHP 7.3 support 109 | - Windows support 110 | - Integration tests 111 | - New sniffs from upstream rulesets, see comments in [the ruleset](https://github.com/mayflower/mo4-coding-standard/blob/master/MO4/ruleset.xml) 112 | - Autofixer for `MO4.Arrays.ArrayDoubleArrowAlignment`, fixes [#91](https://github.com/mayflower/mo4-coding-standard/issues/91) 113 | 114 | ### Changed 115 | - Raised PHP requirement to 7.1 116 | - Detection of more comments after declarations, fixes [#96](https://github.com/mayflower/mo4-coding-standard/issues/96) 117 | - Documentation about upstream sniffs moved from README to comments per rule in [the ruleset](https://github.com/mayflower/mo4-coding-standard/blob/master/MO4/ruleset.xml) 118 | 119 | ### Removed 120 | - Support for PHP 5.6 and PHP 7.0 121 | - PEAR as installation method 122 | 123 | ## [2.1.2] - 2018-09-06 124 | ### Changed 125 | - prefer builtin-functions for performance 126 | 127 | ## [2.1.1] - 2018-05-17 128 | ### Changed 129 | - Check for empty needle when calling strpos, fixes [#78](https://github.com/mayflower/mo4-coding-standard/issues/78) 130 | 131 | ## [2.1.0] - 2018-04-06 132 | ### Changed 133 | - PHPCS version 3.2.3 or later is required. 134 | 135 | ## [2.0.0] - 2017-12-18 136 | ### Added 137 | - `MO4.Arrays.ArrayDoubleArrowAlignment` 138 | - `MO4.Arrays.MultiLineArray` 139 | - `Generic.Arrays.ArrayIndent` 140 | - `Squiz.WhiteSpace.OperatorSpacing` 141 | - The behaviour of `MO4.Formatting.AlphabeticalUseStatements` can be configured with the property `order`. 142 | Possible values are: `dictionary`, `string`, `string-locale` or `string-case-insensitive`. 143 | - Static code analysis with PHPStan. 144 | - Various cloud based code quality tools like: Scrutinizer CI, codecov.io, ... 145 | 146 | ### Changed 147 | - PHPCS version 3.2.0 or later is required. 148 | - Code complies to PHPCS coding standard version 3.2.0. 149 | - Default ordering of `MO4.Formatting.AlphabeticalUseStatements` is now `dictionary` instead of `string`. 150 | - Updates and fixes for class documentation. 151 | - Improve testing and code coverage. 152 | - Unknown test files will not trigger wrong type exceptions anymore, but report decent error messages with `RuntimeException`. 153 | - Many fixes and stability improvements. 154 | 155 | ### Removed 156 | - `MO4.Formatting.ArrayAlignmentUnit` 157 | - replaced by `MO4.Arrays.ArrayDoubleArrowAlignment` and `MO4.Arrays.MultiLineArray`. 158 | - `MO4.Formatting.UseArrayShortTag` 159 | - replaced by `Generic.Arrays.DisallowLongArray`. 160 | - Dead code from `MO4.Strings.VariableInDoubleQuotedString`. 161 | 162 | ## [1.0.0] - 2017-11-20 163 | ### Changed 164 | - MO4 coding standard can be installed as composer package and is released on packagist.org. 165 | - Replaced underlying Symfony coding standard. 166 | - PHPCS 3.0 or later is required. 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MO4 CodeSniffer ruleset 2 | 3 | Provides a PHP CodeSniffer ruleset for the MO4 coding standard 4 | 5 | [![Build Status](https://github.com/mayflower/mo4-coding-standard/actions/workflows/ci.yml/badge.svg)](https://github.com/mayflower/mo4-coding-standard/actions) 6 | [![Code Coverage](https://codecov.io/gh/mayflower/mo4-coding-standard/branch/master/graph/badge.svg)](https://codecov.io/gh/mayflower/mo4-coding-standard/branch/master/) 7 | [![Qlty Maintainability](https://qlty.sh/gh/mayflower/projects/mo4-coding-standard/maintainability.svg)](https://qlty.sh/gh/mayflower/projects/mo4-coding-standard) 8 | [![SonarQube Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mayflower_mo4-coding-standard&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mayflower_mo4-coding-standard) 9 | 10 | [![Latest Stable Version](https://poser.pugx.org/mayflower/mo4-coding-standard/v/stable)](https://packagist.org/packages/mayflower/mo4-coding-standard) 11 | [![Total Downloads](https://poser.pugx.org/mayflower/mo4-coding-standard/downloads)](https://packagist.org/packages/mayflower/mo4-coding-standard) 12 | [![Latest Unstable Version](https://poser.pugx.org/mayflower/mo4-coding-standard/v/unstable)](https://packagist.org/packages/mayflower/mo4-coding-standard) 13 | [![License](https://poser.pugx.org/mayflower/mo4-coding-standard/license)](https://packagist.org/packages/mayflower/mo4-coding-standard) 14 | [![composer.lock](https://poser.pugx.org/mayflower/mo4-coding-standard/composerlock)](https://packagist.org/packages/mayflower/mo4-coding-standard) 15 | 16 | ## MO4 Coding Standard 17 | 18 | The MO4 Coding Standard is an extension of the [Symfony Coding Standard](http://symfony.com/doc/current/contributing/code/standards.html) and adds following rules: 19 | 20 | ### MO4.Arrays.ArrayDoubleArrowAlignment 21 | * In associative arrays, the `=>` operators must be aligned. 22 | * In arrays, the key and `=>` operator must be on the same line. 23 | 24 | ### MO4.Arrays.MultiLineArray 25 | * In multi line arrays, the opening bracket must be followed by newline. 26 | * In multi line arrays, the closing bracket must be in own line. 27 | * In multi line arrays, the elements must be indented. 28 | 29 | ### MO4.Commenting.PropertyComment 30 | * doc blocks of class properties must be multiline and have exactly one `@var` annotation 31 | 32 | ### MO4.Formatting.AlphabeticalUseStatements 33 | * `use` statements must be sorted lexicographically. The order function can be configured. 34 | 35 | #### Configuration 36 | The `order` property of the `MO4.Formatting.AlphabeticalUseStatements` sniff defines 37 | which function is used for ordering. 38 | 39 | Possible values for order: 40 | * `dictionary` (default): based on [strcmp](http://php.net/strcmp), the namespace separator 41 | precedes any other character 42 | ```php 43 | use Doctrine\ORM\Query; 44 | use Doctrine\ORM\Query\Expr; 45 | use Doctrine\ORM\QueryBuilder; 46 | ``` 47 | * `string`: binary safe string comparison using [strcmp](http://php.net/strcmp) 48 | ```php 49 | use Doctrine\ORM\Query; 50 | use Doctrine\ORM\QueryBuilder; 51 | use Doctrine\ORM\Query\Expr; 52 | 53 | use ExampleSub; 54 | use Examples; 55 | ``` 56 | * `string-locale`: locale based string comparison using [strcoll](http://php.net/strcoll) 57 | * `string-case-insensitive`: binary safe case-insensitive string comparison [strcasecmp](http://php.net/strcasecmp) 58 | ```php 59 | use Examples; 60 | use ExampleSub; 61 | ``` 62 | 63 | To change the sorting order for your project, add this snippet to your custom `ruleset.xml`: 64 | 65 | ```xml 66 | 67 | 68 | 69 | 70 | 71 | ``` 72 | 73 | ### MO4.Formatting.UnnecessaryNamespaceUsage 74 | * The imported class name must be used, when it was imported with a `use` statement. 75 | 76 | ### MO4.Strings.VariableInDoubleQuotedString 77 | * Interpolated variables in double quoted strings must be surrounded by `{ }`, e.g. `{$VAR}` instead of `$VAR`. 78 | 79 | ### MO4.WhiteSpace.ConstantSpacing 80 | * const must be followed by a single space. 81 | 82 | ### MO4.WhiteSpace.MultipleEmptyLines 83 | * No more than one empty consecutive line is allowed. Taken from [mediawiki/mediawiki-codesniffer](https://github.com/wikimedia/mediawiki-tools-codesniffer). 84 | 85 | ### Further rules (imported from other standards) 86 | * See `MO4/ruleset.xml`, which has each imported rule commented. 87 | 88 | Note that with this ruleset, the following [Symfony Coding Standard](http://symfony.com/doc/current/contributing/code/standards.html) rules are not enforced: 89 | * "`add doc blocks for all classes`": the doc block for classes can be omitted, if they add no value 90 | * "`the license block has to be present at the top of every PHP file, before the namespace`": the license block can be omitted 91 | 92 | Most of the issues can be auto-fixed with `phpcbf`. 93 | 94 | ## Requires 95 | 96 | * [PHP](http://php.net) 97 | * [Composer](https://getcomposer.org/) is optional, but strongly recommended 98 | 99 | ## Installation 100 | 101 | ### Composer 102 | 103 | Using [Composer](https://getcomposer.org/) is the preferred way. 104 | 105 | 1. Add the MO4 coding standard to `composer.json` 106 | 107 | composer require --dev mayflower/mo4-coding-standard 108 | 109 | 2. Profit 110 | 111 | ./vendor/bin/phpcs --standard=MO4 path/to/my/file.php 112 | 113 | 3. Optionally, you might set MO4 as default coding standard 114 | 115 | ./vendor/bin/phpcs --config-set default_standard MO4 116 | 117 | ### Source 118 | 119 | 1. Checkout this repository 120 | 121 | git clone https://github.com/mayflower/mo4-coding-standard.git 122 | 123 | 2. Install dependencies 124 | 125 | composer install 126 | 127 | 3. Check, that Symfony and MO4 are listed as coding standards 128 | 129 | ./vendor/bin/phpcs -i 130 | 131 | 4. Profit 132 | 133 | ./vendor/bin/phpcs --standard=MO4 path/to/my/file.php 134 | 135 | 5. Optionally, you might set MO4 as default coding standard 136 | 137 | ./vendor/bin/phpcs --config-set default_standard MO4 138 | 139 | ## Troubleshooting 140 | 141 | If `phpcs` complains that MO4 is not installed, please check the installed coding standards with 142 | `phpcs -i` and that `installed_paths` is set correctly with `phpcs --config-show` 143 | 144 | ## Dependencies 145 | 146 | * [PHP CodeSniffer](https://github.com/phpcsstandards/PHP_CodeSniffer) 147 | * [David Joos's Symfony Coding Standard](https://github.com/djoos/Symfony-coding-standard) 148 | * [Composer installer for PHP_CodeSniffer coding standards](https://github.com/DealerDirect/phpcodesniffer-composer-installer) 149 | * [Slevomat Coding Standard](https://github.com/slevomat/coding-standard) 150 | 151 | ## Contributing 152 | 153 | See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for information. 154 | 155 | ## License 156 | 157 | This project is licensed under the MIT license. 158 | See the [LICENSE](LICENSE) file for details. 159 | -------------------------------------------------------------------------------- /MO4/Sniffs/Arrays/ArrayDoubleArrowAlignmentSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Arrays; 16 | 17 | use PHP_CodeSniffer\Files\File; 18 | use PHP_CodeSniffer\Sniffs\Sniff; 19 | use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens; 20 | 21 | /** 22 | * Array Double Arrow Alignment sniff. 23 | * 24 | * '=>' must be aligned in arrays, and the key and the '=>' must be in the same line 25 | * 26 | * @author Xaver Loppenstedt 27 | * 28 | * @copyright 2013 Xaver Loppenstedt, some rights reserved. 29 | * 30 | * @license http://spdx.org/licenses/MIT MIT License 31 | * 32 | * @link https://github.com/mayflower/mo4-coding-standard 33 | * 34 | * @psalm-api 35 | */ 36 | class ArrayDoubleArrowAlignmentSniff implements Sniff 37 | { 38 | /** 39 | * Define all types of arrays. 40 | * 41 | * @var array 42 | */ 43 | protected $arrayTokens = [ 44 | // @phan-suppress-next-line PhanUndeclaredConstant 45 | T_OPEN_SHORT_ARRAY, 46 | T_ARRAY, 47 | ]; 48 | 49 | /** 50 | * Registers the tokens that this sniff wants to listen for. 51 | * 52 | * @return array 53 | * 54 | * @see Tokens.php 55 | */ 56 | #[\Override] 57 | public function register(): array 58 | { 59 | return $this->arrayTokens; 60 | } 61 | 62 | /** 63 | * Processes this test, when one of its tokens is encountered. 64 | * 65 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 66 | * 67 | * @param File $phpcsFile The file being scanned. 68 | * @param int $stackPtr The position of the current token in 69 | * the stack passed in $tokens. 70 | * 71 | * @return void 72 | */ 73 | #[\Override] 74 | public function process(File $phpcsFile, $stackPtr): void 75 | { 76 | $tokens = $phpcsFile->getTokens(); 77 | $current = $tokens[$stackPtr]; 78 | 79 | if (T_ARRAY === $current['code']) { 80 | $start = $current['parenthesis_opener']; 81 | $end = $current['parenthesis_closer']; 82 | } else { 83 | $start = $current['bracket_opener']; 84 | $end = $current['bracket_closer']; 85 | } 86 | 87 | if ($tokens[$start]['line'] === $tokens[$end]['line']) { 88 | return; 89 | } 90 | 91 | // phpcs:disable 92 | /** @var array $assignments */ 93 | $assignments = []; 94 | // phpcs:enable 95 | $keyEndColumn = -1; 96 | $lastLine = -1; 97 | 98 | for ($i = ($start + 1); $i < $end; $i++) { 99 | $current = $tokens[$i]; 100 | $previous = $tokens[($i - 1)]; 101 | 102 | // Skip nested arrays. 103 | if (\in_array($current['code'], $this->arrayTokens, true)) { 104 | $i = T_ARRAY === $current['code'] ? ($current['parenthesis_closer'] + 1) : ($current['bracket_closer'] + 1); 105 | 106 | continue; 107 | } 108 | 109 | // Skip closures in array. 110 | if (T_CLOSURE === $current['code']) { 111 | $i = ($current['scope_closer'] + 1); 112 | 113 | continue; 114 | } 115 | 116 | $i = (int) $i; 117 | 118 | if (T_DOUBLE_ARROW !== $current['code']) { 119 | continue; 120 | } 121 | 122 | $assignments[] = $i; 123 | $column = $previous['column']; 124 | $line = $current['line']; 125 | 126 | if ($lastLine === $line) { 127 | $previousComma = $this->getPreviousComma($phpcsFile, $i, $start); 128 | 129 | $msg = 'only one "=>" assignments per line is allowed in a multi line array'; 130 | 131 | if (false !== $previousComma) { 132 | $fixable = $phpcsFile->addFixableError($msg, $i, 'OneAssignmentPerLine'); 133 | 134 | if (true === $fixable) { 135 | $phpcsFile->fixer->beginChangeset(); 136 | $phpcsFile->fixer->addNewline((int) $previousComma); 137 | $phpcsFile->fixer->endChangeset(); 138 | } 139 | } else { 140 | // Remove current and previous '=>' from array for further processing. 141 | \array_pop($assignments); 142 | \array_pop($assignments); 143 | $phpcsFile->addError($msg, $i, 'OneAssignmentPerLine'); 144 | } 145 | } 146 | 147 | $hasKeyInLine = false; 148 | 149 | $j = ($i - 1); 150 | 151 | while (($j >= 0) && ($tokens[$j]['line'] === $current['line'])) { 152 | if (!\in_array($tokens[$j]['code'], PHP_CodeSniffer_Tokens::$emptyTokens, true)) { 153 | $hasKeyInLine = true; 154 | } 155 | 156 | $j--; 157 | } 158 | 159 | if (false === $hasKeyInLine) { 160 | $fixable = $phpcsFile->addFixableError( 161 | 'in arrays, keys and "=>" must be on the same line', 162 | $i, 163 | 'KeyAndValueNotOnSameLine' 164 | ); 165 | 166 | if (true === $fixable) { 167 | $phpcsFile->fixer->beginChangeset(); 168 | $phpcsFile->fixer->replaceToken($j, ''); 169 | $phpcsFile->fixer->endChangeset(); 170 | } 171 | } 172 | 173 | if ($column > $keyEndColumn) { 174 | $keyEndColumn = $column; 175 | } 176 | 177 | $lastLine = $line; 178 | } 179 | 180 | $doubleArrowStartColumn = ($keyEndColumn + 1); 181 | 182 | foreach ($assignments as $ptr) { 183 | $current = $tokens[$ptr]; 184 | $column = $current['column']; 185 | 186 | $beforeArrowPtr = ($ptr - 1); 187 | $currentIndent = \strlen($tokens[$beforeArrowPtr]['content']); 188 | $correctIndent = ($currentIndent - $column + $doubleArrowStartColumn); 189 | 190 | if ($column === $doubleArrowStartColumn) { 191 | continue; 192 | } 193 | 194 | $fixable = $phpcsFile->addFixableError("each \"=>\" assignments must be aligned; current indentation before \"=>\" are {$currentIndent} space(s), must be {$correctIndent} space(s)", $ptr, 'AssignmentsNotAligned'); 195 | 196 | if (false === $fixable) { 197 | continue; 198 | } 199 | 200 | $phpcsFile->fixer->beginChangeset(); 201 | 202 | if (T_WHITESPACE === $tokens[$beforeArrowPtr]['code']) { 203 | $phpcsFile->fixer->replaceToken($beforeArrowPtr, \str_repeat(' ', $correctIndent)); 204 | } else { 205 | $phpcsFile->fixer->addContent($beforeArrowPtr, \str_repeat(' ', $correctIndent)); 206 | } 207 | 208 | $phpcsFile->fixer->endChangeset(); 209 | } 210 | } 211 | 212 | /** 213 | * Find previous comma in array. 214 | * 215 | * @param File $phpcsFile The file being scanned. 216 | * @param int $stackPtr The position of the current token in 217 | * the stack passed in $tokens. 218 | * @param int $start Start of the array 219 | * 220 | * @return bool|int 221 | */ 222 | private function getPreviousComma(File $phpcsFile, int $stackPtr, int $start) 223 | { 224 | $previousComma = false; 225 | $tokens = $phpcsFile->getTokens(); 226 | 227 | $ptr = $phpcsFile->findPrevious([T_COMMA, T_CLOSE_SHORT_ARRAY], $stackPtr, $start); 228 | 229 | while (false !== $ptr) { 230 | if (T_COMMA === $tokens[$ptr]['code']) { 231 | $previousComma = $ptr; 232 | 233 | break; 234 | } 235 | 236 | if (T_CLOSE_SHORT_ARRAY === $tokens[$ptr]['code']) { 237 | $ptr = $tokens[$ptr]['bracket_opener']; 238 | } 239 | 240 | $ptr = $phpcsFile->findPrevious([T_COMMA, T_CLOSE_SHORT_ARRAY], ($ptr - 1), $start); 241 | } 242 | 243 | return $previousComma; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /MO4/Sniffs/Commenting/PropertyCommentSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Commenting; 16 | 17 | use MO4\Library\PregLibrary; 18 | use PHP_CodeSniffer\Exceptions\RuntimeException; 19 | use PHP_CodeSniffer\Files\File; 20 | use PHP_CodeSniffer\Sniffs\AbstractScopeSniff; 21 | 22 | /** 23 | * Property Comment Sniff sniff. 24 | * 25 | * Doc blocks of class properties must be multiline and have exactly one var 26 | * annotation. 27 | * 28 | * @author Xaver Loppenstedt 29 | * 30 | * @copyright 2013-2014 Xaver Loppenstedt, some rights reserved. 31 | * 32 | * @license http://spdx.org/licenses/MIT MIT License 33 | * 34 | * @link https://github.com/mayflower/mo4-coding-standard 35 | * 36 | * @psalm-api 37 | */ 38 | class PropertyCommentSniff extends AbstractScopeSniff 39 | { 40 | /** 41 | * List of token types this sniff analyzes 42 | * 43 | * @var array 44 | */ 45 | private $myTokenTypes = [ 46 | T_VARIABLE, 47 | T_CONST, 48 | ]; 49 | 50 | /** 51 | * Construct PropertyCommentSniff 52 | * 53 | * @throws RuntimeException 54 | */ 55 | public function __construct() 56 | { 57 | $scopes = [T_CLASS]; 58 | 59 | parent::__construct($scopes, $this->myTokenTypes, true); 60 | } 61 | 62 | /** 63 | * Processes a token that is found within the scope that this test is 64 | * listening to. 65 | * 66 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 67 | * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter 68 | * 69 | * @param File $phpcsFile The file where this token was found. 70 | * @param int $stackPtr The position in the stack where this 71 | * token was found. 72 | * @param int $currScope The position in the tokens array that 73 | * opened the scope that this test is 74 | * listening for. 75 | * 76 | * @return void 77 | * 78 | * @throws RuntimeException 79 | */ 80 | #[\Override] 81 | protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope): void 82 | { 83 | $find = [ 84 | T_COMMENT, 85 | T_DOC_COMMENT_CLOSE_TAG, 86 | T_CLASS, 87 | T_CONST, 88 | T_FUNCTION, 89 | T_VARIABLE, 90 | T_OPEN_TAG, 91 | ]; 92 | $tokens = $phpcsFile->getTokens(); 93 | 94 | // Before even checking the doc blocks above the current var/const, 95 | // check if we have a single line comment after it on the same line, 96 | // and if that one is OK. 97 | $postComment = $phpcsFile->findNext( 98 | [ 99 | T_DOC_COMMENT_OPEN_TAG, 100 | T_COMMENT, 101 | ], 102 | $stackPtr 103 | ); 104 | 105 | if (false !== $postComment 106 | && $tokens[$postComment]['line'] === $tokens[$stackPtr]['line'] 107 | ) { 108 | if ('/**' === $tokens[$postComment]['content']) { 109 | // That's an error already. 110 | $phpcsFile->addError( 111 | 'no doc blocks are allowed directly after declaration', 112 | $stackPtr, 113 | 'NoDocBlockAllowed' 114 | ); 115 | } elseif (!\str_starts_with($tokens[$postComment]['content'], '//') 116 | && !\str_ends_with($tokens[$postComment]['content'], '*/') 117 | ) { 118 | $phpcsFile->addError( 119 | 'no multiline comments after declarations allowed', 120 | $stackPtr, 121 | 'MustBeOneLine' 122 | ); 123 | } 124 | } 125 | 126 | // Don't do constants for now. 127 | if (T_CONST === $tokens[$stackPtr]['code']) { 128 | return; 129 | } 130 | 131 | $commentEnd = (int) $phpcsFile->findPrevious($find, ($stackPtr - 1)); 132 | 133 | $conditions = $tokens[$commentEnd]['conditions']; 134 | $lastCondition = \array_pop($conditions); 135 | 136 | if (T_CLASS !== $lastCondition) { 137 | return; 138 | } 139 | 140 | $code = $tokens[$commentEnd]['code']; 141 | 142 | if (T_DOC_COMMENT_CLOSE_TAG === $code) { 143 | $commentStart = $tokens[$commentEnd]['comment_opener']; 144 | 145 | // Check if this comment is completely in one line, 146 | // above the current line, 147 | // and has a variable preceding it in the same line. 148 | // If yes, it doesn't count. 149 | $firstTokenOnLine = $phpcsFile->findFirstOnLine( 150 | $this->myTokenTypes, 151 | $commentEnd 152 | ); 153 | 154 | if (false !== $firstTokenOnLine 155 | && $tokens[$commentStart]['line'] === $tokens[$commentEnd]['line'] 156 | && $tokens[$stackPtr]['line'] > $tokens[$commentEnd]['line'] 157 | ) { 158 | return; 159 | } 160 | 161 | $isCommentOneLiner 162 | = $tokens[$commentStart]['line'] === $tokens[$commentEnd]['line']; 163 | 164 | $length = ($commentEnd - $commentStart + 1); 165 | $tokensAsString = $phpcsFile->getTokensAsString( 166 | $commentStart, 167 | $length 168 | ); 169 | 170 | $vars = PregLibrary::MO4PregSplit('/\s+@var\s+/', $tokensAsString); 171 | 172 | $varCount = (\count($vars) - 1); 173 | 174 | if ((0 === $varCount) || ($varCount > 1)) { 175 | $phpcsFile->addError( 176 | 'property doc comment must have exactly one @var annotation', 177 | $commentStart, 178 | 'MustHaveOneVarAnnotationDefined' 179 | ); 180 | } 181 | 182 | if (1 === $varCount) { 183 | if (true === $isCommentOneLiner) { 184 | $fix = $phpcsFile->addFixableError( 185 | 'property doc comment must be multi line', 186 | $commentEnd, 187 | 'NotMultiLineDocBlock' 188 | ); 189 | 190 | if (true === $fix) { 191 | $phpcsFile->fixer->beginChangeset(); 192 | $phpcsFile->fixer->addContent($commentStart, "\n *"); 193 | $phpcsFile->fixer->replaceToken( 194 | ($commentEnd - 1), 195 | \rtrim($tokens[($commentEnd - 1)]['content']) 196 | ); 197 | $phpcsFile->fixer->addContentBefore($commentEnd, "\n "); 198 | $phpcsFile->fixer->endChangeset(); 199 | } 200 | } 201 | } elseif (true === $isCommentOneLiner) { 202 | $phpcsFile->addError( 203 | 'property doc comment must be multi line', 204 | $commentEnd, 205 | 'NotMultiLineDocBlock' 206 | ); 207 | } 208 | // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit 209 | } elseif (T_COMMENT === $code) { 210 | // It seems that when we are in here, 211 | // then we have a line comment at $commentEnd. 212 | // Now, check if the same comment has 213 | // a variable definition on the same line. 214 | // If yes, it doesn't count. 215 | $firstOnLine = $phpcsFile->findFirstOnLine( 216 | $this->myTokenTypes, 217 | $commentEnd 218 | ); 219 | 220 | // phpcs:enable SlevomatCodingStandard.ControlStructures.EarlyExit 221 | if (false === $firstOnLine) { 222 | $commentStart = $phpcsFile->findPrevious( 223 | T_COMMENT, 224 | $commentEnd, 225 | 0, 226 | true 227 | ); 228 | $phpcsFile->addError( 229 | 'property doc comment must begin with /**', 230 | ((int) $commentStart + 1), 231 | 'NotADocBlock' 232 | ); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Process tokens outside scope. 239 | * 240 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 241 | * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter 242 | * 243 | * @param File $phpcsFile The file where this token was found. 244 | * @param int $stackPtr The position in the stack where this 245 | * token was found. 246 | * 247 | * @return void 248 | */ 249 | #[\Override] 250 | protected function processTokenOutsideScope(File $phpcsFile, $stackPtr): void 251 | { 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /MO4/Sniffs/Formatting/AlphabeticalUseStatementsSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Formatting; 16 | 17 | use PHP_CodeSniffer\Files\File; 18 | use PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff; 19 | use PHP_CodeSniffer\Util\Common; 20 | use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens; 21 | 22 | /** 23 | * Alphabetical Use Statements sniff. 24 | * 25 | * Use statements must be in alphabetical order, grouped by empty lines. 26 | * 27 | * @author Xaver Loppenstedt 28 | * @author Steffen Ritter 29 | * @author Christian Albrecht 30 | * 31 | * @copyright 2013-2017 Xaver Loppenstedt, some rights reserved. 32 | * 33 | * @license http://spdx.org/licenses/MIT MIT License 34 | * 35 | * @link https://github.com/mayflower/mo4-coding-standard 36 | * 37 | * @psalm-api 38 | */ 39 | class AlphabeticalUseStatementsSniff extends UseDeclarationSniff 40 | { 41 | private const NAMESPACE_SEPARATOR_STRING = '\\'; 42 | 43 | private const SUPPORTED_ORDERING_METHODS = [ 44 | 'dictionary', 45 | 'string', 46 | 'string', 47 | 'string-locale', 48 | 'string-case-insensitive', 49 | ]; 50 | 51 | /** 52 | * Sorting order, see SUPPORTED_ORDERING_METHODS for possible settings 53 | * 54 | * Unknown types will be mapped to 'string'. 55 | * 56 | * @var string 57 | */ 58 | public $order = 'dictionary'; 59 | 60 | /** 61 | * Last import seen in group 62 | * 63 | * @var string 64 | */ 65 | private $lastImport = ''; 66 | 67 | /** 68 | * Line number of the last seen use statement 69 | * 70 | * @var int 71 | */ 72 | private $lastLine = -1; 73 | 74 | /** 75 | * Current file 76 | * 77 | * @var string 78 | */ 79 | private $currentFile = ''; 80 | 81 | /** 82 | * Processes this test, when one of its tokens is encountered. 83 | * 84 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 85 | * 86 | * @param File $phpcsFile The file being scanned. 87 | * @param int $stackPtr The position of the current token in 88 | * the stack passed in $tokens. 89 | * 90 | * @return void 91 | */ 92 | #[\Override] 93 | public function process(File $phpcsFile, $stackPtr): void 94 | { 95 | if (!\in_array($this->order, self::SUPPORTED_ORDERING_METHODS, true)) { 96 | $error = \sprintf( 97 | "'%s' is not a valid order function for %s! Pick one of: %s", 98 | $this->order, 99 | Common::getSniffCode(self::class), 100 | \implode(', ', self::SUPPORTED_ORDERING_METHODS) 101 | ); 102 | 103 | $phpcsFile->addError($error, $stackPtr, 'InvalidOrder'); 104 | 105 | return; 106 | } 107 | 108 | parent::process($phpcsFile, $stackPtr); 109 | 110 | if ($this->currentFile !== $phpcsFile->getFilename()) { 111 | $this->lastLine = -1; 112 | $this->lastImport = ''; 113 | $this->currentFile = $phpcsFile->getFilename(); 114 | } 115 | 116 | $tokens = $phpcsFile->getTokens(); 117 | $line = $tokens[$stackPtr]['line']; 118 | 119 | // Ignore function () use () {...}. 120 | $isNonImportUse = $this->checkIsNonImportUse($phpcsFile, $stackPtr); 121 | 122 | if (true === $isNonImportUse) { 123 | return; 124 | } 125 | 126 | $currentImportArr = $this->getUseImport($phpcsFile, $stackPtr); 127 | 128 | if (false === $currentImportArr) { 129 | return; 130 | } 131 | 132 | $currentPtr = $currentImportArr['startPtr']; 133 | $currentImport = $currentImportArr['content']; 134 | 135 | if (($this->lastLine + 1) < $line) { 136 | $this->lastLine = $line; 137 | $this->lastImport = $currentImport; 138 | 139 | return; 140 | } 141 | 142 | $fixable = false; 143 | 144 | if ('' !== $this->lastImport 145 | && $this->compareString($this->lastImport, $currentImport) > 0 146 | ) { 147 | $msg = 'USE statements must be sorted alphabetically, order %s'; 148 | $code = 'MustBeSortedAlphabetically'; 149 | $fixable = $phpcsFile->addFixableError($msg, $currentPtr, $code, [$this->order]); 150 | } 151 | 152 | if (true === $fixable) { 153 | // Find the correct position in current use block. 154 | $newDestinationPtr 155 | = $this->findNewDestination($phpcsFile, $stackPtr, $currentImport); 156 | 157 | $currentUseStr = $this->getUseStatementAsString($phpcsFile, $stackPtr); 158 | 159 | $phpcsFile->fixer->beginChangeset(); 160 | $phpcsFile->fixer->addContentBefore($newDestinationPtr, $currentUseStr); 161 | $this->fixerClearLine($phpcsFile, $stackPtr); 162 | $phpcsFile->fixer->endChangeset(); 163 | } 164 | 165 | $this->lastImport = $currentImport; 166 | $this->lastLine = $line; 167 | } 168 | 169 | /** 170 | * Get the import class name for use statement pointed by $stackPtr. 171 | * 172 | * @param File $phpcsFile PHP CS File 173 | * @param int $stackPtr pointer 174 | * 175 | * @return array|false 176 | */ 177 | private function getUseImport(File $phpcsFile, int $stackPtr) 178 | { 179 | $importTokens = [ 180 | T_NS_SEPARATOR, 181 | T_STRING, 182 | ]; 183 | 184 | $start = $phpcsFile->findNext( 185 | PHP_CodeSniffer_Tokens::$emptyTokens, 186 | ($stackPtr + 1), 187 | null, 188 | true 189 | ); 190 | 191 | // $start is false when "use" is the last token in file... 192 | if (false === $start) { 193 | return false; 194 | } 195 | 196 | $end = (int) $phpcsFile->findNext($importTokens, $start, null, true); 197 | $import = $phpcsFile->getTokensAsString($start, ($end - $start)); 198 | 199 | return [ 200 | 'startPtr' => $start, 201 | 'content' => $import, 202 | ]; 203 | } 204 | 205 | /** 206 | * Get the full use statement as string, including trailing white space. 207 | * 208 | * @param File $phpcsFile PHP CS File 209 | * @param int $stackPtr pointer 210 | * 211 | * @return string 212 | */ 213 | private function getUseStatementAsString(File $phpcsFile, int $stackPtr): string 214 | { 215 | $tokens = $phpcsFile->getTokens(); 216 | 217 | $useEndPtr = (int) $phpcsFile->findNext([T_SEMICOLON], ($stackPtr + 2)); 218 | $useLength = ($useEndPtr - $stackPtr + 1); 219 | 220 | if (T_WHITESPACE === $tokens[($useEndPtr + 1)]['code']) { 221 | $useLength++; 222 | } 223 | 224 | return $phpcsFile->getTokensAsString($stackPtr, $useLength); 225 | } 226 | 227 | /** 228 | * Check if "use" token is not used for import. 229 | * E.g. function () use () {...}. 230 | * 231 | * @param File $phpcsFile PHP CS File 232 | * @param int $stackPtr pointer 233 | * 234 | * @return bool 235 | */ 236 | private function checkIsNonImportUse(File $phpcsFile, int $stackPtr): bool 237 | { 238 | $tokens = $phpcsFile->getTokens(); 239 | 240 | $prev = $phpcsFile->findPrevious( 241 | PHP_CodeSniffer_Tokens::$emptyTokens, 242 | ($stackPtr - 1), 243 | 0, 244 | true, 245 | null, 246 | true 247 | ); 248 | 249 | if (false !== $prev) { 250 | $prevToken = $tokens[$prev]; 251 | 252 | if (T_CLOSE_PARENTHESIS === $prevToken['code']) { 253 | return true; 254 | } 255 | } 256 | 257 | return false; 258 | } 259 | 260 | /** 261 | * Replace all the token in same line as the element pointed to by $stackPtr 262 | * the by the empty string. 263 | * This will delete the line. 264 | * 265 | * @param File $phpcsFile PHP CS file 266 | * @param int $stackPtr pointer 267 | * 268 | * @return void 269 | */ 270 | private function fixerClearLine(File $phpcsFile, int $stackPtr): void 271 | { 272 | $tokens = $phpcsFile->getTokens(); 273 | $line = $tokens[$stackPtr]['line']; 274 | 275 | for ($i = ($stackPtr - 1); $tokens[$i]['line'] === $line; $i--) { 276 | $phpcsFile->fixer->replaceToken($i, ''); 277 | } 278 | 279 | for ($i = $stackPtr; $tokens[$i]['line'] === $line; $i++) { 280 | $phpcsFile->fixer->replaceToken($i, ''); 281 | } 282 | } 283 | 284 | /** 285 | * Find a new destination pointer for the given import string in current 286 | * use block. 287 | * 288 | * @param File $phpcsFile PHP CS File 289 | * @param int $stackPtr pointer 290 | * @param string $import import string requiring new position 291 | * 292 | * @return int 293 | */ 294 | private function findNewDestination(File $phpcsFile, int $stackPtr, string $import): int 295 | { 296 | $tokens = $phpcsFile->getTokens(); 297 | 298 | $line = $tokens[$stackPtr]['line']; 299 | /** @var int|bool $prevLine */ 300 | $prevLine = false; 301 | $prevPtr = $stackPtr; 302 | 303 | do { 304 | $ptr = $prevPtr; 305 | 306 | // Use $line for the first iteration. 307 | if (false !== $prevLine) { 308 | $line = $prevLine; 309 | } 310 | 311 | $prevPtr = $phpcsFile->findPrevious(T_USE, ($ptr - 1)); 312 | 313 | if (false === $prevPtr) { 314 | break; 315 | } 316 | 317 | $prevLine = $tokens[$prevPtr]['line']; 318 | // phpcs:disable 319 | /** @var array $prevImportArr */ 320 | $prevImportArr = $this->getUseImport($phpcsFile, $prevPtr); 321 | // phpcs:enable 322 | } while ($prevLine === ($line - 1) 323 | && ($this->compareString($prevImportArr['content'], $import) > 0) 324 | ); 325 | 326 | return $ptr; 327 | } 328 | 329 | /** 330 | * Compare namespace strings according defined order function. 331 | * 332 | * @param string $a first namespace string 333 | * @param string $b second namespace string 334 | * 335 | * @return int 336 | */ 337 | private function compareString(string $a, string $b): int 338 | { 339 | return match ($this->order) { 340 | 'string' => \strcmp($a, $b), 341 | 'string-locale' => \strcoll($a, $b), 342 | 'string-case-insensitive' => \strcasecmp($a, $b), 343 | // Default is 'dictionary'. 344 | default => $this->dictionaryCompare($a, $b), 345 | }; 346 | } 347 | 348 | /** 349 | * Lexicographical namespace string compare. 350 | * 351 | * Example: 352 | * 353 | * use Doctrine\ORM\Query; 354 | * use Doctrine\ORM\Query\Expr; 355 | * use Doctrine\ORM\QueryBuilder; 356 | * 357 | * @param string $a first namespace string 358 | * @param string $b second namespace string 359 | * 360 | * @return int 361 | */ 362 | private function dictionaryCompare(string $a, string $b): int 363 | { 364 | $min = \min(\strlen($a), \strlen($b)); 365 | 366 | for ($i = 0; $i < $min; $i++) { 367 | if ($a[$i] === $b[$i]) { 368 | continue; 369 | } 370 | 371 | if (self::NAMESPACE_SEPARATOR_STRING === $a[$i]) { 372 | return -1; 373 | } 374 | 375 | if (self::NAMESPACE_SEPARATOR_STRING === $b[$i]) { 376 | return 1; 377 | } 378 | 379 | if ($a[$i] < $b[$i]) { 380 | return -1; 381 | } 382 | 383 | if ($a[$i] > $b[$i]) { 384 | return 1; 385 | } 386 | } 387 | 388 | return \strcmp(\substr($a, $min), \substr($b, $min)); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /MO4/Sniffs/Formatting/UnnecessaryNamespaceUsageSniff.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * @license http://spdx.org/licenses/MIT MIT License 9 | * 10 | * @link https://github.com/mayflower/mo4-coding-standard 11 | */ 12 | 13 | declare(strict_types=1); 14 | 15 | namespace MO4\Sniffs\Formatting; 16 | 17 | use MO4\Library\PregLibrary; 18 | use PHP_CodeSniffer\Exceptions\RuntimeException; 19 | use PHP_CodeSniffer\Files\File; 20 | use PHP_CodeSniffer\Sniffs\Sniff; 21 | use PHP_CodeSniffer\Util\Tokens as PHP_CodeSniffer_Tokens; 22 | 23 | /** 24 | * Unnecessary Namespace Usage sniff. 25 | * 26 | * Full namespace declaration should be skipped in favour of the short declaration. 27 | * 28 | * @author Xaver Loppenstedt 29 | * @author Marco Jantke 30 | * @author Steffen Ritter 31 | * 32 | * @copyright 2013 Xaver Loppenstedt, some rights reserved. 33 | * 34 | * @license http://spdx.org/licenses/MIT MIT License 35 | * 36 | * @link https://github.com/mayflower/mo4-coding-standard 37 | * 38 | * @psalm-api 39 | */ 40 | class UnnecessaryNamespaceUsageSniff implements Sniff 41 | { 42 | /** 43 | * Tokens used in full class name. 44 | * 45 | * @var array 46 | */ 47 | private $classNameTokens = [ 48 | T_NS_SEPARATOR, 49 | T_STRING, 50 | ]; 51 | 52 | /** 53 | * Registers the tokens that this sniff wants to listen for. 54 | * 55 | * @return array 56 | * 57 | * @see Tokens.php 58 | */ 59 | #[\Override] 60 | public function register(): array 61 | { 62 | return [T_CLASS]; 63 | } 64 | 65 | /** 66 | * Called when one of the token types that this sniff is listening for 67 | * is found. 68 | * 69 | * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint 70 | * 71 | * @param File $phpcsFile The PHP_CodeSniffer file where the 72 | * token was found. 73 | * @param int $stackPtr The position in the PHP_CodeSniffer 74 | * file's token stack where the token 75 | * was found. 76 | * 77 | * @return void 78 | * 79 | * @throws RuntimeException 80 | */ 81 | #[\Override] 82 | public function process(File $phpcsFile, $stackPtr): void 83 | { 84 | $docCommentTags = [ 85 | '@param' => 1, 86 | '@return' => 1, 87 | '@throws' => 1, 88 | '@var' => 2, 89 | ]; 90 | $scanTokens = [ 91 | T_NS_SEPARATOR, 92 | T_DOC_COMMENT_OPEN_TAG, 93 | ]; 94 | 95 | $tokens = $phpcsFile->getTokens(); 96 | $useStatements = $this->getUseStatements($phpcsFile, 0, ($stackPtr - 1)); 97 | $namespace = $this->getNamespace($phpcsFile, 0, ($stackPtr - 1)); 98 | 99 | $nsSep = $phpcsFile->findNext($scanTokens, ($stackPtr + 1)); 100 | 101 | while (false !== $nsSep) { 102 | $classNameEnd = (int) $phpcsFile->findNext( 103 | $this->classNameTokens, 104 | $nsSep, 105 | null, 106 | true 107 | ); 108 | 109 | if (T_NS_SEPARATOR === $tokens[$nsSep]['code']) { 110 | if (T_STRING === $tokens[($nsSep - 1)]['code']) { 111 | --$nsSep; 112 | } 113 | 114 | $className = $phpcsFile->getTokensAsString( 115 | $nsSep, 116 | ($classNameEnd - $nsSep) 117 | ); 118 | 119 | $this->checkShorthandPossible( 120 | $phpcsFile, 121 | $useStatements, 122 | $className, 123 | $namespace, 124 | $nsSep, 125 | ($classNameEnd - 1) 126 | ); 127 | } else { 128 | // Doc comment block. 129 | foreach ($tokens[$nsSep]['comment_tags'] as $tag) { 130 | $content = $tokens[$tag]['content']; 131 | 132 | if (!\array_key_exists($content, $docCommentTags)) { 133 | continue; 134 | } 135 | 136 | $next = ($tag + 1); 137 | // PHP Code Sniffer will magically add T_DOC_COMMENT_CLOSE_TAG with empty string content. 138 | $lineEnd = $phpcsFile->findNext( 139 | [ 140 | T_DOC_COMMENT_CLOSE_TAG, 141 | T_DOC_COMMENT_STAR, 142 | ], 143 | $next 144 | ); 145 | 146 | $docCommentStringPtr = $phpcsFile->findNext( 147 | [T_DOC_COMMENT_STRING], 148 | $next, 149 | (int) $lineEnd 150 | ); 151 | 152 | if (false === $docCommentStringPtr) { 153 | continue; 154 | } 155 | 156 | $docLine = $tokens[$docCommentStringPtr]['content']; 157 | 158 | $docLineTokens = PregLibrary::MO4PregSplit( 159 | '/\s+/', 160 | $docLine, 161 | -1, 162 | PREG_SPLIT_NO_EMPTY 163 | ); 164 | 165 | // phpcs:disable 166 | /** @var array $docLineTokens */ 167 | $docLineTokens = \array_slice( 168 | $docLineTokens, 169 | 0, 170 | $docCommentTags[$content] 171 | ); 172 | // phpcs:enable 173 | 174 | foreach ($docLineTokens as $docLineToken) { 175 | // phpcs:disable 176 | /** @var array $typeTokens */ 177 | $typeTokens = PregLibrary::MO4PregSplit( 178 | '/\|/', 179 | $docLineToken, 180 | -1, 181 | PREG_SPLIT_NO_EMPTY 182 | ); 183 | // phpcs:enable 184 | 185 | foreach ($typeTokens as $typeToken) { 186 | if (\in_array($typeToken, $useStatements, true)) { 187 | continue; 188 | } 189 | 190 | $this->checkShorthandPossible( 191 | $phpcsFile, 192 | $useStatements, 193 | $typeToken, 194 | $namespace, 195 | $docCommentStringPtr, 196 | $docCommentStringPtr, 197 | true 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | 204 | $nsSep = $phpcsFile->findNext($scanTokens, ($classNameEnd + 1)); 205 | } 206 | } 207 | 208 | /** 209 | * Get all use statements in range 210 | * 211 | * @param File $phpcsFile PHP CS File 212 | * @param int $start start pointer 213 | * @param int $end end pointer 214 | * 215 | * @return array 216 | */ 217 | protected function getUseStatements(File $phpcsFile, int $start, int $end): array 218 | { 219 | $useStatements = []; 220 | $i = $start; 221 | $tokens = $phpcsFile->getTokens(); 222 | $useTokenPtr = $phpcsFile->findNext(T_USE, $i, $end); 223 | 224 | while (false !== $useTokenPtr) { 225 | $classNameStart = (int) $phpcsFile->findNext( 226 | PHP_CodeSniffer_Tokens::$emptyTokens, 227 | ($useTokenPtr + 1), 228 | $end, 229 | true 230 | ); 231 | $classNameEnd = $phpcsFile->findNext( 232 | $this->classNameTokens, 233 | ($classNameStart + 1), 234 | $end, 235 | true 236 | ); 237 | 238 | if (false === $classNameEnd) { 239 | break; 240 | } 241 | 242 | $useEnd = $phpcsFile->findNext( 243 | [ 244 | T_SEMICOLON, 245 | T_COMMA, 246 | ], 247 | $classNameEnd, 248 | $end 249 | ); 250 | 251 | // Prevent endless loop when 'use ;' is the last use statement. 252 | if (false === $useEnd) { 253 | break; 254 | } 255 | 256 | /** @var int $aliasNamePtr */ 257 | $aliasNamePtr = $phpcsFile->findPrevious( 258 | PHP_CodeSniffer_Tokens::$emptyTokens, 259 | ($useEnd - 1), 260 | 0, 261 | true 262 | ); 263 | 264 | $length = ($classNameEnd - $classNameStart); 265 | $className = $phpcsFile->getTokensAsString($classNameStart, $length); 266 | 267 | $className = $this->getFullyQualifiedClassName($className); 268 | $useStatements[$className] = $tokens[$aliasNamePtr]['content']; 269 | $i = ($useEnd + 1); 270 | 271 | $useTokenPtr = T_COMMA === $tokens[$useEnd]['code'] ? $i : $phpcsFile->findNext(T_USE, $i, $end); 272 | } 273 | 274 | return $useStatements; 275 | } 276 | 277 | /** 278 | * Get the namespace of the current class file 279 | * 280 | * @param File $phpcsFile PHP CS File 281 | * @param int $start start pointer 282 | * @param int $end end pointer 283 | * 284 | * @return string 285 | */ 286 | protected function getNamespace(File $phpcsFile, int $start, int $end): string 287 | { 288 | $namespace = (int) $phpcsFile->findNext(T_NAMESPACE, $start, $end); 289 | $namespaceStart = $phpcsFile->findNext( 290 | PHP_CodeSniffer_Tokens::$emptyTokens, 291 | ($namespace + 1), 292 | $end, 293 | true 294 | ); 295 | 296 | if (false === $namespaceStart) { 297 | return ''; 298 | } 299 | 300 | $namespaceEnd = (int) $phpcsFile->findNext( 301 | $this->classNameTokens, 302 | ($namespaceStart + 1), 303 | $end, 304 | true 305 | ); 306 | 307 | $nslen = ($namespaceEnd - $namespaceStart); 308 | $name = $phpcsFile->getTokensAsString($namespaceStart, $nslen); 309 | 310 | return "\\{$name}\\"; 311 | } 312 | 313 | /** 314 | * Return the fully qualified class name, e.g. '\Foo\Bar\Faz' 315 | * 316 | * @param string $className class name 317 | * 318 | * @return string 319 | */ 320 | private function getFullyQualifiedClassName(string $className): string 321 | { 322 | return '\\' !== $className[0] ? "\\{$className}" : $className; 323 | } 324 | 325 | /** 326 | * Check if short hand is possible. 327 | * 328 | * @param File $phpcsFile PHP CS File 329 | * @param array $useStatements array with class use statements 330 | * @param string $className class name 331 | * @param string $namespace name space 332 | * @param int $startPtr start token pointer 333 | * @param int $endPtr end token pointer 334 | * @param bool $isDocBlock true if fixing doc block 335 | * 336 | * @return void 337 | */ 338 | private function checkShorthandPossible(File $phpcsFile, array $useStatements, string $className, string $namespace, int $startPtr, int $endPtr, bool $isDocBlock = false): void 339 | { 340 | $msg = 'Shorthand possible. Replace "%s" with "%s"'; 341 | $code = 'UnnecessaryNamespaceUsage'; 342 | $fixable = false; 343 | $replaceClassName = false; 344 | $replacement = ''; 345 | 346 | $fullClassName = $this->getFullyQualifiedClassName($className); 347 | 348 | if (\array_key_exists($fullClassName, $useStatements)) { 349 | $replacement = $useStatements[$fullClassName]; 350 | 351 | $data = [ 352 | $className, 353 | $replacement, 354 | ]; 355 | 356 | $fixable = $phpcsFile->addFixableWarning( 357 | $msg, 358 | $startPtr, 359 | $code, 360 | $data 361 | ); 362 | 363 | $replaceClassName = true; 364 | } elseif ('' !== $namespace && \str_starts_with($fullClassName, $namespace)) { 365 | $replacement = \substr($fullClassName, \strlen($namespace)); 366 | 367 | $data = [ 368 | $className, 369 | $replacement, 370 | ]; 371 | $fixable = $phpcsFile->addFixableWarning( 372 | $msg, 373 | $startPtr, 374 | $code, 375 | $data 376 | ); 377 | } 378 | 379 | if (true !== $fixable) { 380 | return; 381 | } 382 | 383 | $phpcsFile->fixer->beginChangeset(); 384 | 385 | if (true === $isDocBlock) { 386 | $tokens = $phpcsFile->getTokens(); 387 | $oldContent = $tokens[$startPtr]['content']; 388 | /** @var string $newContent */ 389 | $newContent = \str_replace($className, $replacement, $oldContent); 390 | $phpcsFile->fixer->replaceToken($startPtr, $newContent); 391 | } else { 392 | for ($i = $startPtr; $i < $endPtr; $i++) { 393 | $phpcsFile->fixer->replaceToken($i, ''); 394 | } 395 | 396 | if (true === $replaceClassName) { 397 | $phpcsFile->fixer->replaceToken($endPtr, $replacement); 398 | } 399 | } 400 | 401 | $phpcsFile->fixer->endChangeset(); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /MO4/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The MO4 PHP coding standard. 4 | 5 | */Resources/* 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 0 241 | 242 | 243 | 0 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 0 255 | 256 | 257 | 0 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | --------------------------------------------------------------------------------