diff --git a/.editorconfig b/.editorconfig index 2933ecf..a72c528 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,5 +10,5 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[*.{yml,yaml}] +[*.{neon,yml,yaml}] indent_size = 2 diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e85f15e..4d60f87 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -35,6 +35,9 @@ __DIR__.'/tests', __DIR__.'/tools', ]) + ->notPath([ + '#PHPStan/.+/data/.+#', + ]) ->append([ __FILE__, 'bin/parallel-phpunit', diff --git a/composer.json b/composer.json index 130b9e2..304eeac 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ }, "replace": { "nexusphp/clock": "self.version", - "nexusphp/option": "self.version" + "nexusphp/option": "self.version", + "nexusphp/phpstan-nexus": "self.version" }, "provide": { "psr/clock-implementation": "1.0" @@ -44,6 +45,9 @@ }, "files": [ "src/Nexus/Option/functions.php" + ], + "exclude-from-classmap": [ + "tests/PHPStan/**/data/**" ] }, "autoload-dev": { @@ -59,6 +63,13 @@ "preferred-install": "dist", "sort-packages": true }, + "extra": { + "phpstan": { + "includes": [ + "src/Nexus/PHPStan/extension.neon" + ] + } + }, "scripts": { "post-update-cmd": [ "@composer update --ansi --working-dir=tools" diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 53a04b7..251ddf7 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,6 +1,7 @@ includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon - phpstan-baseline.php + - src/Nexus/PHPStan/extension.neon parameters: phpVersion: 80200 @@ -14,6 +15,7 @@ parameters: - tools excludePaths: analyseAndScan: + - tests/PHPStan/**/data/** - tools/vendor/** bootstrapFiles: - vendor/autoload.php diff --git a/src/Nexus/PHPStan/LICENSE b/src/Nexus/PHPStan/LICENSE new file mode 100644 index 0000000..ce701d4 --- /dev/null +++ b/src/Nexus/PHPStan/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John Paul E. Balandan, CPA + +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. diff --git a/src/Nexus/PHPStan/README.md b/src/Nexus/PHPStan/README.md new file mode 100644 index 0000000..4886de6 --- /dev/null +++ b/src/Nexus/PHPStan/README.md @@ -0,0 +1,47 @@ +# PHPStan extensions for Nexus + +These are Nexus-specific extensions and rules for [PHPStan](https://github.com/phpstan/phpstan). + +## Installation + + composer require --dev nexusphp/phpstan-nexus + +If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! + +
+ Manual installation + +If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: + +```yml +includes: + - vendor/nexusphp/phpstan-nexus/extension.neon +``` + +
+ +## Rules + +The following rules will automatically be enabled once the `extension.neon` is added: + +1. Class constants, properties and methods, as well as functions, should follow the correct + naming convention: + * Class constants - UPPER_SNAKE_CASE format + * Class properties - camelCase format with no underscores + * Class methods - camelCase format with no underscores, except for magic methods where double + underscores are allowed + * Functions - lower_snake_case format + +## License + +Nexus Option is licensed under the [MIT License][5]. + +## Resources + +* [Report issues][2] and [send pull requests][3] in the [main Nexus repository][4] + +[1]: https://doc.rust-lang.org/std/option/enum.Option.html +[2]: https://github.com/NexusPHP/framework/issues +[3]: https://github.com/NexusPHP/framework/pulls +[4]: https://github.com/NexusPHP/framework +[5]: LICENSE diff --git a/src/Nexus/PHPStan/Rules/Constants/ClassConstantNamingRule.php b/src/Nexus/PHPStan/Rules/Constants/ClassConstantNamingRule.php new file mode 100644 index 0000000..6153095 --- /dev/null +++ b/src/Nexus/PHPStan/Rules/Constants/ClassConstantNamingRule.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\PHPStan\Rules\Constants; + +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; + +/** + * @implements Rule + */ +final class ClassConstantNamingRule implements Rule +{ + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + /** + * @param Node\Stmt\ClassConst $node + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $scope->isInClass()) { + throw new ShouldNotHappenException(); // @codeCoverageIgnore + } + + $errors = []; + + foreach ($node->consts as $const) { + $errors = [ + ...$errors, + ...$this->processSingleConstant($scope->getClassReflection(), $const), + ]; + } + + return $errors; + } + + /** + * @return list + */ + private function processSingleConstant(ClassReflection $classReflection, Node\Const_ $const): array + { + $constantName = $const->name->toString(); + $prototype = $this->findPrototype($classReflection, $constantName); + + if ( + null !== $prototype + && ! str_starts_with($prototype->getDeclaringClass()->getDisplayName(), 'Nexus\\') + ) { + return []; + } + + if (preg_match('/^[A-Z][A-Z0-9_]+$/', $constantName) !== 1) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Constant %s::%s should be in UPPER_SNAKE_CASE format.', + $classReflection->getDisplayName(), + $constantName, + )) + ->identifier('nexus.constantCasing') + ->line($const->getStartLine()) + ->build(), + ]; + } + + return []; + } + + private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection + { + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasConstant($constantName)) { + return $immediateInterface->getConstant($constantName); // @codeCoverageIgnore + } + } + + $parentClass = $classReflection->getParentClass(); + + if (null === $parentClass) { + return null; // @codeCoverageIgnore + } + + if (! $parentClass->hasConstant($constantName)) { + return null; + } + + $constant = $parentClass->getConstant($constantName); + + if ($constant->isPrivate()) { + return null; // @codeCoverageIgnore + } + + return $constant; + } +} diff --git a/src/Nexus/PHPStan/Rules/Functions/FunctionNamingRule.php b/src/Nexus/PHPStan/Rules/Functions/FunctionNamingRule.php new file mode 100644 index 0000000..3b73907 --- /dev/null +++ b/src/Nexus/PHPStan/Rules/Functions/FunctionNamingRule.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\PHPStan\Rules\Functions; + +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; + +/** + * @implements Rule + */ +final class FunctionNamingRule implements Rule +{ + public function getNodeType(): string + { + return Node\Stmt\Function_::class; + } + + /** + * @param Node\Stmt\Function_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if (null === $node->namespacedName) { + throw new ShouldNotHappenException(); // @codeCoverageIgnore + } + + $functionName = $node->namespacedName->toString(); + + if (! str_starts_with($functionName, 'Nexus\\')) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Function %s() should be namespaced using the "Nexus\\" namespace.', + $functionName, + )) + ->identifier('nexus.functionNamespace') + ->build(), + ]; + } + + $basename = basename(str_replace('\\', '/', $functionName)); + + if (preg_match('/^[a-z][a-z0-9_]+$/', $basename) !== 1) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Function %s() should be in lower snake case format.', + $functionName, + )) + ->identifier('nexus.functionCasing') + ->build(), + ]; + } + + $errors = []; + + foreach (array_values($node->params) as $index => $param) { + if (! $param->var instanceof Node\Expr\Variable) { + continue; // @codeCoverageIgnore + } + + if (! \is_string($param->var->name)) { + continue; // @codeCoverageIgnore + } + + if (preg_match('/^[a-z][a-zA-Z0-9]+$/', $param->var->name) !== 1) { + $errors[] = RuleErrorBuilder::message(\sprintf( + 'Parameter #%d $%s of function %s() should be in camelCase format.', + $index + 1, + $param->var->name, + $functionName, + )) + ->identifier('nexus.functionParamCasing') + ->line($param->getStartLine()) + ->build() + ; + } + } + + return $errors; + } +} diff --git a/src/Nexus/PHPStan/Rules/Methods/MethodNamingRule.php b/src/Nexus/PHPStan/Rules/Methods/MethodNamingRule.php new file mode 100644 index 0000000..8757ae8 --- /dev/null +++ b/src/Nexus/PHPStan/Rules/Methods/MethodNamingRule.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\PHPStan\Rules\Methods; + +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassMethodNode; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; + +/** + * @implements Rule + */ +final class MethodNamingRule implements Rule +{ + public function getNodeType(): string + { + return InClassMethodNode::class; + } + + /** + * @param InClassMethodNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $method = $node->getOriginalNode(); + $methodName = $method->name->toString(); + $methodReflection = $node->getMethodReflection(); + $methodPrototype = $methodReflection->getPrototype(); + + if ( + $methodPrototype !== $methodReflection + && ! str_starts_with($methodPrototype->getDeclaringClass()->getDisplayName(), 'Nexus\\') + ) { + return []; + } + + if (str_starts_with($methodName, '__')) { + if ($method->isMagic()) { + return []; + } + + return [ + RuleErrorBuilder::message(\sprintf( + 'Method %s::%s() should not start with double underscores.', + $node->getClassReflection()->getDisplayName(), + $methodName, + )) + ->identifier('nexus.methodDoubleUnderscore') + ->build(), + ]; + } + + if (str_starts_with($methodName, '_')) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Method %s::%s() should not start with an underscore.', + $node->getClassReflection()->getDisplayName(), + $methodName, + )) + ->identifier('nexus.methodUnderscore') + ->build(), + ]; + } + + if (preg_match('/^[a-z][a-zA-Z0-9]+$/', $methodName) !== 1) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Method %s::%s() should be written in camelCase format.', + $node->getClassReflection()->getDisplayName(), + $methodName, + )) + ->identifier('nexus.methodCasing') + ->build(), + ]; + } + + $errors = []; + + foreach (array_values($method->params) as $index => $param) { + if (! $param->var instanceof Node\Expr\Variable) { + continue; // @codeCoverageIgnore + } + + if (! \is_string($param->var->name)) { + continue; // @codeCoverageIgnore + } + + if (preg_match('/^[a-z][a-zA-Z0-9]+$/', $param->var->name) !== 1) { + $errors[] = RuleErrorBuilder::message(\sprintf( + 'Parameter #%d $%s of %s::%s() should be in camelCase with no underscores.', + $index + 1, + $param->var->name, + $node->getClassReflection()->getDisplayName(), + $methodName, + )) + ->identifier('nexus.methodParamNaming') + ->line($param->getStartLine()) + ->build() + ; + } + } + + return $errors; + } +} diff --git a/src/Nexus/PHPStan/Rules/Properties/PropertyNamingRule.php b/src/Nexus/PHPStan/Rules/Properties/PropertyNamingRule.php new file mode 100644 index 0000000..de2a73c --- /dev/null +++ b/src/Nexus/PHPStan/Rules/Properties/PropertyNamingRule.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\PHPStan\Rules\Properties; + +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Node\ClassPropertyNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; + +/** + * @implements Rule + */ +final class PropertyNamingRule implements Rule +{ + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + /** + * @param ClassPropertyNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + $propertyName = $node->getName(); + $propertyPrototype = $this->findPrototype($classReflection, $propertyName); + + if ( + null !== $propertyPrototype + && ! str_starts_with($propertyPrototype->getDeclaringClass()->getDisplayName(), 'Nexus\\') + ) { + return []; + } + + if (str_starts_with($propertyName, '_')) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Property %s::$%s should not start with an underscore.', + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('nexus.propertyUnderscore')->build(), + ]; + } + + if (preg_match('/^[a-z][a-zA-Z0-9]+$/', $propertyName) !== 1) { + return [ + RuleErrorBuilder::message(\sprintf( + 'Property %s::$%s should be in camelCase format.', + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('nexus.propertyCasing')->build(), + ]; + } + + return []; + } + + private function findPrototype(ClassReflection $classReflection, string $propertyName): ?PhpPropertyReflection + { + $parentClass = $classReflection->getParentClass(); + + if (null === $parentClass) { + return null; + } + + if (! $parentClass->hasNativeProperty($propertyName)) { + return null; // @codeCoverageIgnore + } + + $property = $parentClass->getNativeProperty($propertyName); + + if ($property->isPrivate()) { + return null; // @codeCoverageIgnore + } + + return $property; + } +} diff --git a/src/Nexus/PHPStan/composer.json b/src/Nexus/PHPStan/composer.json new file mode 100644 index 0000000..5ee2f75 --- /dev/null +++ b/src/Nexus/PHPStan/composer.json @@ -0,0 +1,49 @@ +{ + "name": "nexusphp/phpstan-nexus", + "description": "PHPStan extension for Nexus.", + "license": "MIT", + "type": "phpstan-extension", + "keywords": [ + "nexus", + "phpstan" + ], + "authors": [ + { + "name": "John Paul E. Balandan, CPA", + "email": "paulbalandan@gmail.com" + } + ], + "support": { + "issues": "https://github.com/NexusPHP/framework/issues", + "source": "https://github.com/NexusPHP/framework" + }, + "require": { + "php": "^8.2", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "Nexus\\PHPStan\\": "" + } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + } +} diff --git a/src/Nexus/PHPStan/extension.neon b/src/Nexus/PHPStan/extension.neon new file mode 100644 index 0000000..2ee1332 --- /dev/null +++ b/src/Nexus/PHPStan/extension.neon @@ -0,0 +1,5 @@ +rules: + - Nexus\PHPStan\Rules\Constants\ClassConstantNamingRule + - Nexus\PHPStan\Rules\Functions\FunctionNamingRule + - Nexus\PHPStan\Rules\Methods\MethodNamingRule + - Nexus\PHPStan\Rules\Properties\PropertyNamingRule diff --git a/tests/PHPStan/Rules/Constants/ClassConstantNamingRuleTest.php b/tests/PHPStan/Rules/Constants/ClassConstantNamingRuleTest.php new file mode 100644 index 0000000..6ee0c67 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/ClassConstantNamingRuleTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Tests\PHPStan\Rules\Constants; + +use Nexus\PHPStan\Rules\Constants\ClassConstantNamingRule; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + * + * @extends RuleTestCase + */ +#[CoversClass(ClassConstantNamingRule::class)] +#[Group('unit')] +final class ClassConstantNamingRuleTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__.'/data/class-constant-naming.php'], [ + [ + 'Constant Nexus\Tests\PHPStan\Rules\Constants\Foo::_QUX should be in UPPER_SNAKE_CASE format.', + 11, + ], + [ + 'Constant Nexus\Tests\PHPStan\Rules\Constants\Foo::Instant should be in UPPER_SNAKE_CASE format.', + 12, + ], + [ + 'Constant Nexus\Tests\PHPStan\Rules\Constants\Foo::temporal should be in UPPER_SNAKE_CASE format.', + 13, + ], + ]); + } + + protected function getRule(): Rule + { + return new ClassConstantNamingRule(); + } +} diff --git a/tests/PHPStan/Rules/Constants/data/class-constant-naming.php b/tests/PHPStan/Rules/Constants/data/class-constant-naming.php new file mode 100644 index 0000000..50f79c0 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/class-constant-naming.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Tests\PHPStan\Rules\Functions; + +use Nexus\PHPStan\Rules\Functions\FunctionNamingRule; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + * + * @extends RuleTestCase + */ +#[CoversClass(FunctionNamingRule::class)] +#[Group('unit')] +final class FunctionNamingRuleTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__.'/data/function-naming.php'], [ + [ + 'Function Nexus\Tests\PHPStan\Rules\Functions\Ball() should be in lower snake case format.', + 8, + ], + [ + 'Function Nexus\Tests\PHPStan\Rules\Functions\_align() should be in lower snake case format.', + 10, + ], + [ + 'Parameter #1 $_bar of function Nexus\Tests\PHPStan\Rules\Functions\foo() should be in camelCase format.', + 13, + ], + [ + 'Function deride() should be namespaced using the "Nexus\\" namespace.', + 20, + ], + ]); + } + + protected function getRule(): Rule + { + return new FunctionNamingRule(); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/function-naming.php b/tests/PHPStan/Rules/Functions/data/function-naming.php new file mode 100644 index 0000000..f4d2500 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-naming.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Tests\PHPStan\Rules\Methods; + +use Nexus\PHPStan\Rules\Methods\MethodNamingRule; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + * + * @extends RuleTestCase + */ +#[CoversClass(MethodNamingRule::class)] +#[Group('unit')] +final class MethodNamingRuleTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__.'/data/method-naming.php'], [ + [ + 'Method Nexus\Tests\PHPStan\Rules\Methods\Bar::__direct() should not start with double underscores.', + 11, + ], + [ + 'Method Nexus\Tests\PHPStan\Rules\Methods\Bar::_boo() should not start with an underscore.', + 18, + ], + [ + 'Method Nexus\Tests\PHPStan\Rules\Methods\Bar::base_band() should be written in camelCase format.', + 20, + ], + [ + 'Parameter #1 $max_lifetime of Nexus\Tests\PHPStan\Rules\Methods\Bar::readline() should be in camelCase with no underscores.', + 22, + ], + ]); + } + + protected function getRule(): Rule + { + return new MethodNamingRule(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/method-naming.php b/tests/PHPStan/Rules/Methods/data/method-naming.php new file mode 100644 index 0000000..d25ffe3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-naming.php @@ -0,0 +1,23 @@ + 0 ? $max_lifetime : false; + } + + public function _boo(): void {} + + public function base_band(): void {} + + abstract public function readline(int $max_lifetime): void; +} diff --git a/tests/PHPStan/Rules/Properties/PropertyNamingRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyNamingRuleTest.php new file mode 100644 index 0000000..7e16d2d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/PropertyNamingRuleTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Nexus\Tests\PHPStan\Rules\Properties; + +use Nexus\PHPStan\Rules\Properties\PropertyNamingRule; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + * + * @extends RuleTestCase + */ +#[CoversClass(PropertyNamingRule::class)] +#[Group('unit')] +final class PropertyNamingRuleTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__.'/data/property-naming.php'], [ + [ + 'Property Nexus\Tests\PHPStan\Rules\Properties\Bar::$_axe should not start with an underscore.', + 9, + ], + [ + 'Property Nexus\Tests\PHPStan\Rules\Properties\Bar::$basket_ should be in camelCase format.', + 10, + ], + [ + 'Property Nexus\Tests\PHPStan\Rules\Properties\Bar::$Status should be in camelCase format.', + 11, + ], + [ + 'Property Nexus\Tests\PHPStan\Rules\Properties\Foo::$basket_ should be in camelCase format.', + 17, + ], + ]); + } + + protected function getRule(): Rule + { + return new PropertyNamingRule(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/property-naming.php b/tests/PHPStan/Rules/Properties/data/property-naming.php new file mode 100644 index 0000000..00b231e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-naming.php @@ -0,0 +1,26 @@ +