diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b8631..fe60552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## Unreleased +- Replace `Symplify\CodingStandard\Sniffs\Naming\[AbstractClassNameSniff, ClassNameSuffixByParentSniff, InterfaceNameSniff and TraitNameSniff]` with equivalent versions backported to this repository. ## 2.1.0 - 2020-11-25 - Add various dangerous function calls to list of forbidden functions. diff --git a/composer.json b/composer.json index 5350a03..6a44a4a 100644 --- a/composer.json +++ b/composer.json @@ -12,13 +12,17 @@ "require": { "php": "^7.2", "friendsofphp/php-cs-fixer": "^2.16.3", - "symplify/auto-bind-parameter": "<7.2.20", - "symplify/autowire-array-parameter": "<7.2.20", - "symplify/coding-standard": "<7.2.20", - "symplify/easy-coding-standard": "^7.2.3", - "symplify/package-builder": "<7.2.20", - "symplify/set-config-resolver": "<7.2.20", - "symplify/smart-file-system": "<7.2.20" + "slevomat/coding-standard": "^6.4.1", + "symplify/auto-bind-parameter": "<8.1.21", + "symplify/autowire-array-parameter": "<8.1.21", + "symplify/coding-standard": "<8.1.21", + "symplify/console-color-diff": "<8.1.21", + "symplify/easy-coding-standard": "<8.1.21", + "symplify/package-builder": "<8.1.21", + "symplify/parameter-name-guard": "<8.1.21", + "symplify/phpstan-extensions": "<8.1.21", + "symplify/set-config-resolver": "<8.1.21", + "symplify/smart-file-system": "<8.1.21" }, "require-dev": { "j13k/yaml-lint": "dev-master", @@ -29,7 +33,10 @@ "phpunit/phpunit": "^7.1 || ^8.0" }, "config": { - "sort-packages": true + "sort-packages": true, + "preferred-install": { + "squizlabs/php_codesniffer": "source" + } }, "autoload": { "psr-4": { @@ -49,9 +56,12 @@ "@test" ], "analyze": [ - "vendor/bin/ecs check src/ tests/ -vv --ansi", + "vendor/bin/ecs check src/ tests/ --ansi", "vendor/bin/phpstan.phar analyze -c phpstan.neon --ansi" ], + "fix": [ + "./vendor/bin/ecs check ./src/ ./tests/ --ansi --fix" + ], "lint": [ "for FILE_NAME in *.yml *.yaml; do vendor/bin/yaml-lint \"$FILE_NAME\"; done", "vendor/bin/yaml-sort-checker" diff --git a/easy-coding-standard.yaml b/easy-coding-standard.yaml index 5763178..07d1967 100644 --- a/easy-coding-standard.yaml +++ b/easy-coding-standard.yaml @@ -7,6 +7,14 @@ imports: services: # Function http_build_query() should always have specified `$arg_separator` parameter Lmc\CodingStandard\Fixer\SpecifyArgSeparatorFixer: ~ + # Abstract class should have prefix "Abstract" + Lmc\CodingStandard\Sniffs\Naming\AbstractClassNameSniff: ~ + # Classes should have suffix by theirs parent class/interface + Lmc\CodingStandard\Sniffs\Naming\ClassNameSuffixByParentSniff: + # Interface should have suffix "Interface" + Lmc\CodingStandard\Sniffs\Naming\InterfaceNameSniff: ~ + # Trait should have suffix "Trait" + Lmc\CodingStandard\Sniffs\Naming\TraitNameSniff: ~ # Class and Interface names should be unique in a project, they should never be duplicated PHP_CodeSniffer\Standards\Generic\Sniffs\Classes\DuplicateClassNameSniff: ~ @@ -250,25 +258,6 @@ services: SlevomatCodingStandard\Sniffs\Exceptions\ReferenceThrowableOnlySniff: ~ # The @param, @return, @var and inline @var annotations should keep standard format Symplify\CodingStandard\Fixer\Commenting\ParamReturnAndVarTagMalformsFixer: ~ - # Abstract class should have prefix "Abstract" - Symplify\CodingStandard\Sniffs\Naming\AbstractClassNameSniff: ~ - # Classes should have suffix by theirs parent class/interface - Symplify\CodingStandard\Sniffs\Naming\ClassNameSuffixByParentSniff: - defaultParentClassToSuffixMap: - '*Command': 'Command' - '*Controller': 'Controller' - '*Repository': 'Repository' - '*Presenter': 'Presenter' - '*Request': 'Request' - '*Response': 'Response' - '*FixerInterface': 'Fixer' - '*Sniff': 'Sniff' - '*Exception': 'Exception' - '*Handler': 'Handler' - # Interface should have suffix "Interface" - Symplify\CodingStandard\Sniffs\Naming\InterfaceNameSniff: ~ - # Trait should have suffix "Trait" - Symplify\CodingStandard\Sniffs\Naming\TraitNameSniff: ~ parameters: skip: diff --git a/phpstan.neon b/phpstan.neon index 34aff1f..1bff653 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,9 @@ parameters: paths: - src/ - tests/ + bootstrapFiles: + - vendor/squizlabs/php_codesniffer/autoload.php + - vendor/squizlabs/php_codesniffer/src/Util/Tokens.php ignoreErrors: - message: '#Parameter \#(2|3) \$(argumentStart|argumentEnd) of method PhpCsFixer\\Tokenizer\\Analyzer\\ArgumentsAnalyzer::getArgumentInfo\(\) expects int#' path: %currentWorkingDirectory%/src/Fixer/SpecifyArgSeparatorFixer.php @@ -10,3 +13,4 @@ parameters: path: %currentWorkingDirectory%/tests/Fixer/SpecifyArgSeparatorFixerTest.php - message: '#Parameter \#1 \$filename of function file_get_contents expects string, string\|false given.#' path: %currentWorkingDirectory%/tests/Fixer/SpecifyArgSeparatorFixerTest.php + checkMissingIterableValueType: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c09f1d..c91b2c9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ diff --git a/src/Helper/Naming.php b/src/Helper/Naming.php new file mode 100644 index 0000000..09eb25c --- /dev/null +++ b/src/Helper/Naming.php @@ -0,0 +1,161 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Helper; + +use PHP_CodeSniffer\Files\File; +use SlevomatCodingStandard\Helpers\ClassHelper; +use SlevomatCodingStandard\Helpers\NamespaceHelper; +use SlevomatCodingStandard\Helpers\ReferencedNameHelper; +use SlevomatCodingStandard\Helpers\TokenHelper; + +final class Naming +{ + /** + * @var string + */ + private const NAMESPACE_SEPARATOR = '\\'; + + /** + * @var string[] + */ + private const CLASS_NAMES_BY_FILE_PATH = []; + + /** + * @var mixed[][] + */ + private $referencedNamesByFilePath = []; + + /** + * @var string[][] + */ + private $fqnClassNameByFilePathAndClassName = []; + + public function getFileClassName(File $file): ?string + { + // get name by path + if (isset(self::CLASS_NAMES_BY_FILE_PATH[$file->path])) { + return self::CLASS_NAMES_BY_FILE_PATH[$file->path]; + } + + $classPosition = TokenHelper::findNext($file, T_CLASS, 1); + if ($classPosition === null) { + return null; + } + + $className = ClassHelper::getFullyQualifiedName($file, $classPosition); + + return ltrim($className, '\\'); + } + + public function getClassName(File $file, int $classNameStartPosition): string + { + $tokens = $file->getTokens(); + + $firstNamePart = $tokens[$classNameStartPosition]['content']; + + // is class + if ($this->isClassName($file, $classNameStartPosition)) { + $namespace = NamespaceHelper::findCurrentNamespaceName($file, $classNameStartPosition); + if ($namespace) { + return $namespace . '\\' . $firstNamePart; + } + + return $firstNamePart; + } + + $classNameParts = []; + $classNameParts[] = $firstNamePart; + + $nextTokenPointer = $classNameStartPosition + 1; + while ($tokens[$nextTokenPointer]['code'] === T_NS_SEPARATOR) { + ++$nextTokenPointer; + $classNameParts[] = $tokens[$nextTokenPointer]['content']; + ++$nextTokenPointer; + } + + $completeClassName = implode(self::NAMESPACE_SEPARATOR, $classNameParts); + + $fqnClassName = $this->getFqnClassName($file, $completeClassName, $classNameStartPosition); + if ($fqnClassName !== '') { + return ltrim($fqnClassName, self::NAMESPACE_SEPARATOR); + } + + return $completeClassName; + } + + private function getFqnClassName(File $file, string $className, int $classTokenPosition): string + { + $referencedNames = $this->getReferencedNames($file); + + foreach ($referencedNames as $referencedName) { + if (isset($this->fqnClassNameByFilePathAndClassName[$file->path][$className])) { + return $this->fqnClassNameByFilePathAndClassName[$file->path][$className]; + } + + $resolvedName = NamespaceHelper::resolveClassName( + $file, + $referencedName->getNameAsReferencedInFile(), + $classTokenPosition + ); + + if ($referencedName->getNameAsReferencedInFile() === $className) { + $this->fqnClassNameByFilePathAndClassName[$file->path][$className] = $resolvedName; + + return $resolvedName; + } + } + + return ''; + } + + /** + * As in: + * class + */ + private function isClassName(File $file, int $position): bool + { + return (bool) $file->findPrevious(T_CLASS, $position, max(1, $position - 3)); + } + + /** + * @return mixed[] + */ + private function getReferencedNames(File $file): array + { + if (isset($this->referencedNamesByFilePath[$file->path])) { + return $this->referencedNamesByFilePath[$file->path]; + } + + $referencedNames = ReferencedNameHelper::getAllReferencedNames($file, 0); + + $this->referencedNamesByFilePath[$file->path] = $referencedNames; + + return $referencedNames; + } +} diff --git a/src/Helper/SniffClassWrapper.php b/src/Helper/SniffClassWrapper.php new file mode 100644 index 0000000..ed36207 --- /dev/null +++ b/src/Helper/SniffClassWrapper.php @@ -0,0 +1,121 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Helper; + +use PHP_CodeSniffer\Files\File; +use SlevomatCodingStandard\Helpers\TokenHelper; + +final class SniffClassWrapper +{ + /** + * @var int + */ + private $position; + + /** + * @var File + */ + private $file; + + /** + * @var Naming + */ + private $naming; + + public function __construct(File $file, int $position, Naming $naming) + { + $this->file = $file; + $this->position = $position; + $this->naming = $naming; + } + + public function getClassName(): ?string + { + return $this->naming->getClassName($this->file, $this->position + 2); + } + + /** + * @return string[] + */ + public function getPartialInterfaceNames(): array + { + if (!$this->doesImplementInterface()) { + return []; + } + + $implementPosition = $this->getImplementsPosition(); + if (!is_int($implementPosition)) { + return []; + } + + $openBracketPosition = $this->file->findNext(T_OPEN_CURLY_BRACKET, $this->position, $this->position + 15); + + // anonymous class + if (!$openBracketPosition) { + return []; + } + + $interfacePartialNamePosition = $this->file->findNext(T_STRING, $implementPosition, $openBracketPosition); + + $partialInterfacesNames = []; + $partialInterfacesNames[] = $this->file->getTokens()[$interfacePartialNamePosition]['content']; + + return $partialInterfacesNames; + } + + public function doesImplementInterface(): bool + { + return (bool) $this->getImplementsPosition(); + } + + public function doesExtendClass(): bool + { + return (bool) $this->file->findNext(T_EXTENDS, $this->position, $this->position + 5); + } + + public function getParentClassName(): ?string + { + $extendsTokenPosition = TokenHelper::findNext($this->file, T_EXTENDS, $this->position, $this->position + 10); + if ($extendsTokenPosition === null) { + return null; + } + + $parentClassPosition = (int) TokenHelper::findNext($this->file, T_STRING, $extendsTokenPosition); + + return $this->naming->getClassName($this->file, $parentClassPosition); + } + + /** + * @return bool|int + */ + private function getImplementsPosition() + { + return $this->file->findNext(T_IMPLEMENTS, $this->position, $this->position + 15); + } +} diff --git a/src/Sniffs/Naming/AbstractClassNameSniff.php b/src/Sniffs/Naming/AbstractClassNameSniff.php new file mode 100644 index 0000000..4a7e314 --- /dev/null +++ b/src/Sniffs/Naming/AbstractClassNameSniff.php @@ -0,0 +1,97 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Sniffs\Naming; + +use Nette\Utils\Strings; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +final class AbstractClassNameSniff implements Sniff +{ + /** + * @var string + */ + private const ERROR_MESSAGE = 'Abstract class should have prefix "Abstract".'; + + /** + * @var int + */ + private $position; + + /** + * @var File + */ + private $file; + + /** + * @return int[] + */ + public function register(): array + { + return [T_CLASS]; + } + + /** + * @param int $position + */ + public function process(File $file, $position): void + { + $this->file = $file; + $this->position = $position; + + if ($this->shouldBeSkipped()) { + return; + } + + $file->addError(self::ERROR_MESSAGE, $position, self::class); + } + + private function shouldBeSkipped(): bool + { + $className = $this->getClassName(); + + if (!$this->isClassAbstract() || $className === null) { + return true; + } + + return Strings::startsWith($className, 'Abstract'); + } + + private function isClassAbstract(): bool + { + $classProperties = $this->file->getClassProperties($this->position); + + return $classProperties['is_abstract']; + } + + private function getClassName(): ?string + { + return $this->file->getDeclarationName($this->position); + } +} diff --git a/src/Sniffs/Naming/ClassNameSuffixByParentSniff.php b/src/Sniffs/Naming/ClassNameSuffixByParentSniff.php new file mode 100644 index 0000000..21a87a9 --- /dev/null +++ b/src/Sniffs/Naming/ClassNameSuffixByParentSniff.php @@ -0,0 +1,149 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Sniffs\Naming; + +use Lmc\CodingStandard\Helper\Naming; +use Lmc\CodingStandard\Helper\SniffClassWrapper; +use Nette\Utils\Strings; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +final class ClassNameSuffixByParentSniff implements Sniff +{ + /** + * @var string + */ + private const ERROR_MESSAGE = 'Class "%s" should have suffix "%s" by parent class/interface'; + + /** + * @var string[] + */ + public $defaultParentClassToSuffixMap = [ + 'Command', + 'Controller', + 'Repository', + 'Presenter', + 'Request', + 'Response', + 'FixerInterface', + 'Sniff', + 'Exception', + 'Handler', + ]; + + /** + * @var string[] + */ + public $extraParentTypesToSuffixes = []; + + /** + * @return int[] + */ + public function register(): array + { + return [T_CLASS]; + } + + /** + * @param int $position + */ + public function process(File $file, $position): void + { + $classWrapper = $this->getWrapperForFirstClassInFile($file); + if ($classWrapper === null) { + return; + } + + $className = $classWrapper->getClassName(); + if ($className === null) { + return; + } + + $parentClassName = $classWrapper->getParentClassName(); + if ($parentClassName) { + $this->processType($file, $parentClassName, $className, $position); + } + + foreach ($classWrapper->getPartialInterfaceNames() as $interfaceName) { + $this->processType($file, $interfaceName, $className, $position); + } + } + + private function processType(File $file, string $currentParentType, string $className, int $position): void + { + foreach ($this->getClassToSuffixMap() as $parentType) { + if (!fnmatch('*' . $parentType, $currentParentType)) { + continue; + } + + // the class that implements $currentParentType, should end with $suffix + $suffix = $this->resolveExpectedSuffix($parentType); + if (Strings::endsWith($className, $suffix)) { + continue; + } + + $file->addError(sprintf(self::ERROR_MESSAGE, $className, $suffix), $position, self::class); + } + } + + /** + * @return string[] + */ + private function getClassToSuffixMap(): array + { + return array_merge($this->defaultParentClassToSuffixMap, $this->extraParentTypesToSuffixes); + } + + /** + * - SomeInterface => Some + * - AbstractSome => Some + */ + private function resolveExpectedSuffix(string $parentType): string + { + if (Strings::endsWith($parentType, 'Interface')) { + $parentType = Strings::substring($parentType, 0, -mb_strlen('Interface')); + } + + if (Strings::startsWith($parentType, 'Abstract')) { + $parentType = Strings::substring($parentType, mb_strlen('Abstract')); + } + + return $parentType; + } + + private function getWrapperForFirstClassInFile(File $file): ?SniffClassWrapper + { + $possibleClassPosition = $file->findNext(T_CLASS, 0); + if (!is_int($possibleClassPosition)) { + return null; + } + + return new SniffClassWrapper($file, $possibleClassPosition, new Naming()); + } +} diff --git a/src/Sniffs/Naming/InterfaceNameSniff.php b/src/Sniffs/Naming/InterfaceNameSniff.php new file mode 100644 index 0000000..0b2243f --- /dev/null +++ b/src/Sniffs/Naming/InterfaceNameSniff.php @@ -0,0 +1,79 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Sniffs\Naming; + +use Nette\Utils\Strings; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +final class InterfaceNameSniff implements Sniff +{ + /** + * @var string + */ + private const ERROR_MESSAGE = 'Interface should have suffix "Interface".'; + + /** + * @var int + */ + private $position; + + /** + * @var File + */ + private $file; + + /** + * @return int[] + */ + public function register(): array + { + return [T_INTERFACE]; + } + + /** + * @param int $position + */ + public function process(File $file, $position): void + { + $this->file = $file; + $this->position = $position; + + if (Strings::endsWith($this->getInterfaceName(), 'Interface')) { + return; + } + + $file->addError(self::ERROR_MESSAGE, $position, self::class); + } + + private function getInterfaceName(): string + { + return (string) $this->file->getDeclarationName($this->position); + } +} diff --git a/src/Sniffs/Naming/TraitNameSniff.php b/src/Sniffs/Naming/TraitNameSniff.php new file mode 100644 index 0000000..659606e --- /dev/null +++ b/src/Sniffs/Naming/TraitNameSniff.php @@ -0,0 +1,79 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Lmc\CodingStandard\Sniffs\Naming; + +use Nette\Utils\Strings; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +final class TraitNameSniff implements Sniff +{ + /** + * @var string + */ + private const ERROR_MESSAGE = 'Trait should have suffix "Trait".'; + + /** + * @var int + */ + private $position; + + /** + * @var File + */ + private $file; + + /** + * @return int[] + */ + public function register(): array + { + return [T_TRAIT]; + } + + /** + * @param int $position + */ + public function process(File $file, $position): void + { + $this->file = $file; + $this->position = $position; + + if (Strings::endsWith($this->getTraitName(), 'Trait')) { + return; + } + + $file->addError(self::ERROR_MESSAGE, $position, self::class); + } + + private function getTraitName(): string + { + return (string) $this->file->getDeclarationName($this->position); + } +} diff --git a/tests/Fixer/Fixtures/Correct.txt b/tests/Fixer/Fixtures/Correct.php.inc similarity index 100% rename from tests/Fixer/Fixtures/Correct.txt rename to tests/Fixer/Fixtures/Correct.php.inc diff --git a/tests/Fixer/Fixtures/Fixed.txt b/tests/Fixer/Fixtures/Fixed.php.inc similarity index 100% rename from tests/Fixer/Fixtures/Fixed.txt rename to tests/Fixer/Fixtures/Fixed.php.inc diff --git a/tests/Fixer/Fixtures/Wrong.txt b/tests/Fixer/Fixtures/Wrong.php.inc similarity index 100% rename from tests/Fixer/Fixtures/Wrong.txt rename to tests/Fixer/Fixtures/Wrong.php.inc diff --git a/tests/Fixer/SpecifyArgSeparatorFixerTest.php b/tests/Fixer/SpecifyArgSeparatorFixerTest.php index 4d2adbe..52a5781 100644 --- a/tests/Fixer/SpecifyArgSeparatorFixerTest.php +++ b/tests/Fixer/SpecifyArgSeparatorFixerTest.php @@ -31,8 +31,8 @@ public function shouldFixCode(string $inputFile, string $expectedOutputFile): vo public function provideFiles(): array { return [ - 'Correct file should not be changed' => ['Correct.txt', 'Correct.txt'], - 'Wrong file should be fixed' => ['Wrong.txt', 'Fixed.txt'], + 'Correct file should not be changed' => ['Correct.php.inc', 'Correct.php.inc'], + 'Wrong file should be fixed' => ['Wrong.php.inc', 'Fixed.php.inc'], ]; } } diff --git a/tests/Sniffs/AbstractSniffTestCase.php b/tests/Sniffs/AbstractSniffTestCase.php new file mode 100644 index 0000000..1bf9882 --- /dev/null +++ b/tests/Sniffs/AbstractSniffTestCase.php @@ -0,0 +1,43 @@ +registerSniffs([$this->getSniffFile()], [], []); + $ruleset->populateTokenListeners(); + + return new LocalFile($fixtureFile, $ruleset, $config); + } + + /** + * @param array $expectedErrors + * @param array $actualErrors + */ + protected function assertErrors(array $expectedErrors, array $actualErrors): void + { + $actualLinesToErrorsMap = []; + foreach ($actualErrors as $line => $error) { + $error = reset($error); + $errorMessage = reset($error)['message']; + $actualLinesToErrorsMap[$line] = $errorMessage; + } + + $this->assertSame($expectedErrors, $actualLinesToErrorsMap); + } + + abstract protected function getSniffFile(): string; +} diff --git a/tests/Sniffs/Naming/AbstractClassNameSniffTest.php b/tests/Sniffs/Naming/AbstractClassNameSniffTest.php new file mode 100644 index 0000000..e440e9c --- /dev/null +++ b/tests/Sniffs/Naming/AbstractClassNameSniffTest.php @@ -0,0 +1,44 @@ +applyFixturesToSniff($fixtureFile); + $sniff->process(); + + $foundErrors = $sniff->getErrors(); + + $this->assertErrors($expectedErrors, $foundErrors); + } + + /** + * @return array[] + */ + public function provideFixtures(): array + { + return [ + 'wrongly named' => [ + __DIR__ . '/Fixtures/AbstractClassNameSniffTest.wrong.php.inc', + [ + 3 => 'Abstract class should have prefix "Abstract".', + 13 => 'Abstract class should have prefix "Abstract".', + ], + ], + 'properly named' => [__DIR__ . '/Fixtures/AbstractClassNameSniffTest.correct.php.inc', []], + ]; + } + + protected function getSniffFile(): string + { + return __DIR__ . '/../../../src/Sniffs/Naming/AbstractClassNameSniff.php'; + } +} diff --git a/tests/Sniffs/Naming/ClassNameSuffixByParentSniffTest.php b/tests/Sniffs/Naming/ClassNameSuffixByParentSniffTest.php new file mode 100644 index 0000000..941a520 --- /dev/null +++ b/tests/Sniffs/Naming/ClassNameSuffixByParentSniffTest.php @@ -0,0 +1,83 @@ +applyFixturesToSniff($fixtureFile); + + /** @var ClassNameSuffixByParentSniff $sniffInstance */ + $sniffInstance = reset($sniff->ruleset->sniffs); + if ($classSuffixes !== null) { + $sniffInstance->defaultParentClassToSuffixMap = $classSuffixes; + } + + $sniff->process(); + + $foundErrors = $sniff->getErrors(); + + $this->assertErrors($expectedErrors, $foundErrors); + } + + /** + * @return array[] + */ + public function provideFixtures(): array + { + return [ + 'wrong with default ruleset' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/CommandWrong.php.inc', + null, + [5 => 'Class "WronglyNamed" should have suffix "Command" by parent class/interface'], + ], + 'properly named with default ruleset' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/CommandCorrect.php.inc', + null, + [], + ], + 'wrong with custom ruleset' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/CustomWrong.php.inc', + ['ParentClass'], + [3 => 'Class "WronglyNamed" should have suffix "ParentClass" by parent class/interface'], + ], + 'properly named with custom ruleset' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/CustomCorrect.php.inc', + ['ParentClass'], + [], + ], + 'wrong with interface' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/InterfaceWrong.php.inc', + ['FooBarInterface'], + [3 => 'Class "WronglyNamed" should have suffix "FooBar" by parent class/interface'], + ], + 'properly named interface' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/InterfaceCorrect.php.inc', + ['FooBarInterface'], + [], + ], + 'wrong with abstract class' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/AbstractWrong.php.inc', + ['AbstractSomething'], + [3 => 'Class "WronglyNamed" should have suffix "Something" by parent class/interface'], + ], + 'properly with abstract class' => [ + __DIR__ . '/Fixtures/ClassNameSuffixByParentSniffTest/AbstractCorrect.php.inc', + ['AbstractSomething'], + [], + ], + ]; + } + + protected function getSniffFile(): string + { + return __DIR__ . '/../../../src/Sniffs/Naming/ClassNameSuffixByParentSniff.php'; + } +} diff --git a/tests/Sniffs/Naming/Fixtures/AbstractClassNameSniffTest.correct.php.inc b/tests/Sniffs/Naming/Fixtures/AbstractClassNameSniffTest.correct.php.inc new file mode 100644 index 0000000..b14a793 --- /dev/null +++ b/tests/Sniffs/Naming/Fixtures/AbstractClassNameSniffTest.correct.php.inc @@ -0,0 +1,12 @@ +applyFixturesToSniff($fixtureFile); + $sniff->process(); + + $foundErrors = $sniff->getErrors(); + + $this->assertErrors($expectedErrors, $foundErrors); + } + + /** + * @return array[] + */ + public function provideFixtures(): array + { + return [ + 'wrongly named' => [ + __DIR__ . '/Fixtures/InterfaceNameSniffTest.wrong.php.inc', + [3 => 'Interface should have suffix "Interface".'], + ], + 'properly named' => [__DIR__ . '/Fixtures/InterfaceNameSniffTest.correct.php.inc', []], + ]; + } + + protected function getSniffFile(): string + { + return __DIR__ . '/../../../src/Sniffs/Naming/InterfaceNameSniff.php'; + } +} diff --git a/tests/Sniffs/Naming/TraitNameSniffTest.php b/tests/Sniffs/Naming/TraitNameSniffTest.php new file mode 100644 index 0000000..f28048d --- /dev/null +++ b/tests/Sniffs/Naming/TraitNameSniffTest.php @@ -0,0 +1,41 @@ +applyFixturesToSniff($fixtureFile); + $sniff->process(); + + $foundErrors = $sniff->getErrors(); + + $this->assertErrors($expectedErrors, $foundErrors); + } + + /** + * @return array[] + */ + public function provideFixtures(): array + { + return [ + 'wrongly named' => [ + __DIR__ . '/Fixtures/TraitNameSniffTest.wrong.php.inc', + [3 => 'Trait should have suffix "Trait".'], + ], + 'properly named' => [__DIR__ . '/Fixtures/TraitNameSniffTest.correct.php.inc', []], + ]; + } + + protected function getSniffFile(): string + { + return __DIR__ . '/../../../src/Sniffs/Naming/TraitNameSniff.php'; + } +}