├── .github ├── FUNDING.yml └── workflows │ └── all_tests.yml ├── .gitignore ├── tests ├── conf │ ├── throws.neon │ ├── readonly.neon │ └── deprecated.neon ├── data │ ├── Internal │ │ ├── FunctionInternalAttribute.php │ │ ├── TraitInternalAttribute.php │ │ ├── InterfaceInternalAttribute.php │ │ ├── PropertyInternalAttribute.php │ │ ├── InvalidMethodInternalAttribute.php │ │ ├── MethodInternalAttribute.php │ │ └── ClassInternalAttribute.php │ ├── Deprecated │ │ ├── FunctionDeprecatedAttribute.php │ │ ├── TraitDeprecatedAttribute.php │ │ ├── InterfaceDeprecatedAttribute.php │ │ ├── ClassDeprecatedAttribute.php │ │ ├── PropertyDeprecatedAttribute.php │ │ ├── InvalidMethodDeprecatedAttribute.php │ │ └── MethodDeprecatedAttribute.php │ ├── Immutable │ │ ├── TraitImmutableAttribute.php │ │ ├── InterfaceImmutableAttribute.php │ │ ├── ClassImmutableAttribute.php │ │ └── InvalidClassImmutableAttribute.php │ ├── Method │ │ ├── TraitMethodAttribute.php │ │ ├── InterfaceMethodAttribute.php │ │ ├── InvalidClassMethodAttribute.php │ │ └── ClassMethodAttribute.php │ ├── Property │ │ ├── TraitPropertyAttribute.php │ │ ├── InterfacePropertyAttribute.php │ │ ├── InvalidClassPropertyAttribute.php │ │ └── ClassPropertyAttribute.php │ ├── Pure │ │ ├── FunctionPureAttribute.php │ │ ├── InvalidMethodPureAttribute.php │ │ └── MethodPureAttribute.php │ ├── Mixin │ │ ├── TraitMixinAttribute.php │ │ ├── InterfaceMixinAttribute.php │ │ ├── InvalidClassMixinAttribute.php │ │ └── ClassMixinAttribute.php │ ├── DefineType │ │ ├── TraitDefineTypeAttribute.php │ │ ├── InterfaceDefineTypeAttribute.php │ │ ├── InvalidClassDefineTypeAttribute.php │ │ └── ClassDefineTypeAttribute.php │ ├── PropertyRead │ │ ├── TraitPropertyReadAttribute.php │ │ ├── InterfacePropertyReadAttribute.php │ │ ├── InvalidClassPropertyReadAttribute.php │ │ └── ClassPropertyReadAttribute.php │ ├── Returns │ │ ├── FunctionReturnsAttribute.php │ │ ├── InvalidMethodReturnsAttribute.php │ │ └── MethodReturnsAttribute.php │ ├── Param │ │ ├── FunctionParamAttribute.php │ │ ├── InvalidMethodParamAttribute.php │ │ └── MethodParamAttribute.php │ ├── ParamOut │ │ ├── FunctionParamOutAttribute.php │ │ ├── InvalidMethodParamOutAttribute.php │ │ └── MethodParamOutAttribute.php │ ├── PropertyWrite │ │ ├── TraitPropertyWriteAttribute.php │ │ ├── InterfacePropertyWriteAttribute.php │ │ ├── InvalidClassPropertyWriteAttribute.php │ │ └── ClassPropertyWriteAttribute.php │ ├── Impure │ │ ├── FunctionImpureAttribute.php │ │ ├── InvalidMethodImpureAttribute.php │ │ └── MethodImpureAttribute.php │ ├── AssertIfTrue │ │ ├── FunctionAssertIfTrueAttribute.php │ │ ├── InvalidMethodAssertIfTrueAttribute.php │ │ └── MethodAssertIfTrueAttribute.php │ ├── AssertIfFalse │ │ ├── FunctionAssertIfFalseAttribute.php │ │ ├── InvalidMethodAssertIfFalseAttribute.php │ │ └── MethodAssertIfFalseAttribute.php │ ├── Assert │ │ ├── FunctionAssertAttribute.php │ │ ├── InvalidMethodAssertAttribute.php │ │ └── MethodAssertAttribute.php │ ├── Throws │ │ ├── FunctionThrowsAttribute.php │ │ ├── InvalidMethodThrowsAttribute.php │ │ └── MethodThrowsAttribute.php │ ├── Template │ │ ├── FunctionTemplateAttribute.php │ │ ├── InterfaceTemplateAttribute.php │ │ ├── TraitTemplateAttribute.php │ │ ├── ClassTemplateAttribute.php │ │ ├── InvalidMethodTemplateAttribute.php │ │ └── MethodTemplateAttribute.php │ ├── ImportType │ │ ├── TraitImportTypeAttribute.php │ │ ├── InterfaceImportTypeAttribute.php │ │ ├── InvalidClassImportTypeAttribute.php │ │ └── ClassImportTypeAttribute.php │ ├── TemplateCovariant │ │ ├── InterfaceTemplateCovariantAttribute.php │ │ ├── InvalidClassTemplateCovariantAttribute.php │ │ ├── TraitTemplateCovariantAttribute.php │ │ └── ClassTemplateCovariantAttribute.php │ ├── TemplateContravariant │ │ ├── InterfaceTemplateContravariantAttribute.php │ │ ├── InvalidClassTemplateContravariantAttribute.php │ │ ├── TraitTemplateContravariantAttribute.php │ │ └── ClassTemplateContravariantAttribute.php │ ├── IsReadOnly │ │ ├── PropertyIsReadOnlyAttribute.php │ │ └── InvalidPropertyIsReadOnlyAttribute.php │ ├── TemplateExtends │ │ ├── ClassTemplateExtendsAttribute.php │ │ └── InvalidClassTemplateExtendsAttribute.php │ ├── RequireExtends │ │ ├── TraitRequireExtendsAttribute.php │ │ └── InvalidTraitRequireExtendsAttribute.php │ ├── Type │ │ ├── InvalidPropertyTypeAttribute.php │ │ └── PropertyTypeAttribute.php │ ├── TemplateUse │ │ ├── InvalidTraitTemplateUseAttribute.php │ │ └── TraitTemplateUseAttribute.php │ ├── RequireImplements │ │ ├── InvalidTraitRequireImplementsAttribute.php │ │ └── TraitRequireImplementsAttribute.php │ ├── TemplateImplements │ │ ├── InvalidInterfaceTemplateImplementsAttribute.php │ │ └── InterfaceTemplateImplementsAttribute.php │ └── SelfOut │ │ ├── InvalidMethodSelfOutAttribute.php │ │ └── MethodSelfOutAttribute.php ├── SelfOutAttributeTest.php ├── ImpureAttributeTest.php ├── TypeAttributeTest.php ├── IsReadOnlyAttributeTest.php ├── AssertAttributeTest.php ├── PureAttributeTest.php ├── AssertIfTrueAttributeTest.php ├── AssertIfFalseAttributeTest.php ├── ReturnsAttributeTest.php ├── ParamAttributeTest.php ├── ParamOutAttributeTest.php ├── ImportTypeAttributeTest.php ├── MixinAttributeTest.php ├── TemplateUseAttributeTest.php ├── RequireImplementsAttributeTest.php ├── RequireExtendsAttributeTest.php ├── MethodAttributeTest.php ├── PropertyAttributeTest.php ├── ImmutableAttributeTest.php ├── TemplateExtendsAttributeTest.php ├── TemplateContravariantAttributeTest.php ├── BaseAttributeTestCase.php ├── DefineTypeAttributeTest.php ├── TemplateImplementsAttributeTest.php ├── TemplateCovariantAttributeTest.php ├── PropertyReadAttributeTest.php ├── PropertyWriteAttributeTest.php ├── InternalAttributeTest.php ├── ThrowsAttributeTest.php ├── TemplateAttributeTest.php └── DeprecatedAttributeTest.php ├── ecs.php ├── extension.neon ├── phpstan.neon ├── psalm.xml ├── phpunit.xml ├── LICENSE ├── src └── Parser │ └── AttributeParser.php ├── composer.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [carlos-granados] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .idea 4 | .phpunit.cache 5 | -------------------------------------------------------------------------------- /tests/conf/throws.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | exceptions: 3 | check: 4 | tooWideThrowType: true -------------------------------------------------------------------------------- /tests/conf/readonly.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | featureToggles: 3 | readOnlyByPhpDoc: true 4 | -------------------------------------------------------------------------------- /tests/conf/deprecated.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ../../vendor/phpstan/phpstan-deprecation-rules/rules.neon 3 | -------------------------------------------------------------------------------- /tests/data/Internal/FunctionInternalAttribute.php: -------------------------------------------------------------------------------- 1 | withPaths([ 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ]) 13 | ->withPreparedSets( 14 | psr12: true, 15 | ); 16 | -------------------------------------------------------------------------------- /tests/data/Assert/FunctionAssertAttribute.php: -------------------------------------------------------------------------------- 1 | name = 'John'; 15 | -------------------------------------------------------------------------------- /tests/data/Throws/FunctionThrowsAttribute.php: -------------------------------------------------------------------------------- 1 | name = 'Mike'; 15 | } 16 | } 17 | 18 | $p = new PropertyIsReadOnlyAttribute(); 19 | $p->name = 'John'; 20 | -------------------------------------------------------------------------------- /tests/data/Template/TraitTemplateAttribute.php: -------------------------------------------------------------------------------- 1 | ')] // this class extends the base template 14 | class ClassTemplateExtendsAttributeChild extends ClassTemplateExtendsAttribute 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /tests/data/TemplateCovariant/TraitTemplateCovariantAttribute.php: -------------------------------------------------------------------------------- 1 | age = 7; 22 | -------------------------------------------------------------------------------- /tests/data/TemplateContravariant/TraitTemplateContravariantAttribute.php: -------------------------------------------------------------------------------- 1 | age; 22 | -------------------------------------------------------------------------------- /tests/data/Deprecated/InvalidMethodDeprecatedAttribute.php: -------------------------------------------------------------------------------- 1 | ')] 25 | public string $name = ''; 26 | } 27 | -------------------------------------------------------------------------------- /tests/data/IsReadOnly/InvalidPropertyIsReadOnlyAttribute.php: -------------------------------------------------------------------------------- 1 | invalidProperty = 'Mike'; 25 | $this->otherInvalidProperty = 'John'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/data/Pure/MethodPureAttribute.php: -------------------------------------------------------------------------------- 1 | returnDeprecated(); 32 | -------------------------------------------------------------------------------- /tests/data/ParamOut/InvalidMethodParamOutAttribute.php: -------------------------------------------------------------------------------- 1 | name; 25 | $bar = $class->age; 26 | $indexes = $class->index1 + $class->index2; 27 | -------------------------------------------------------------------------------- /tests/data/Param/InvalidMethodParamAttribute.php: -------------------------------------------------------------------------------- 1 | $name = $value; 20 | } 21 | } 22 | 23 | $class = new ClassPropertyWriteAttribute(); 24 | $class->name = 'John'; 25 | $class->age = 7; 26 | $class->index1 = []; 27 | $class->index2 = []; 28 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/data/Mixin/ClassMixinAttribute.php: -------------------------------------------------------------------------------- 1 | $name(...$arguments); 32 | } 33 | } 34 | 35 | $proxy = new ClassMixinAttributeProxy(); 36 | $proxy->proxied(); 37 | -------------------------------------------------------------------------------- /tests/data/RequireImplements/InvalidTraitRequireImplementsAttribute.php: -------------------------------------------------------------------------------- 1 | ')] 22 | public string $name = ''; 23 | } 24 | -------------------------------------------------------------------------------- /tests/data/Impure/MethodImpureAttribute.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | tests 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/data/Template/InvalidMethodTemplateAttribute.php: -------------------------------------------------------------------------------- 1 | ')] // this class uses the base trait 24 | #[TemplateUse( 25 | 'TraitTemplateUseAttribute2', 26 | 'TraitTemplateUseAttribute3' 27 | )] 28 | class ClassTemplateUseAttribute 29 | { 30 | use TraitTemplateUseAttribute; 31 | use TraitTemplateUseAttribute2, TraitTemplateUseAttribute3; 32 | } 33 | -------------------------------------------------------------------------------- /tests/data/SelfOut/InvalidMethodSelfOutAttribute.php: -------------------------------------------------------------------------------- 1 | ')] 22 | #[SelfOut('self')] 23 | public function addMore($item): void 24 | { 25 | } 26 | 27 | #[SelfOut('self')] 28 | public string $property; 29 | } 30 | -------------------------------------------------------------------------------- /tests/data/Returns/InvalidMethodReturnsAttribute.php: -------------------------------------------------------------------------------- 1 | badFunction(); 32 | -------------------------------------------------------------------------------- /tests/data/Property/ClassPropertyAttribute.php: -------------------------------------------------------------------------------- 1 | $name = $value; 28 | } 29 | } 30 | 31 | $class = new ClassPropertyAttribute(); 32 | $foo = $class->name; 33 | $class->age = 7; 34 | $indexes = $class->index1 + $class->index2; 35 | -------------------------------------------------------------------------------- /tests/data/Method/ClassMethodAttribute.php: -------------------------------------------------------------------------------- 1 | getString(); 28 | $class->setString($foo); 29 | $bar = ClassMethodAttribute::staticGetter(); 30 | -------------------------------------------------------------------------------- /tests/data/RequireExtends/InvalidTraitRequireExtendsAttribute.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/SelfOut/MethodSelfOutAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInvalidMethodSelfOutAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/SelfOut/InvalidMethodSelfOutAttribute.php'); 16 | 17 | $expectedErrors = [ 18 | 'PHPDoc tag @phpstan-self-out has invalid value (): Unexpected token "\n ", expected type at offset 75 on line 4' => 15, 19 | 'Attribute class PhpStaticAnalysis\Attributes\SelfOut does not have the property target.' => 27, 20 | ]; 21 | 22 | $this->checkExpectedErrors($errors, $expectedErrors); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/data/Internal/ClassInternalAttribute.php: -------------------------------------------------------------------------------- 1 | myFunction(); 25 | } 26 | } 27 | 28 | namespace newNamespace\other; 29 | 30 | class otherClass 31 | { 32 | public function otherFunction(): void 33 | { 34 | $class = new \test\PhpStaticAnalysis\PHPStanExtension\data\Internal\ClassInternalAttribute(); 35 | 36 | $class->myFunction(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/data/ImportType/ClassImportTypeAttribute.php: -------------------------------------------------------------------------------- 1 | ')] // this class implements the base interface 24 | #[TemplateImplements( 25 | 'InterfaceTemplateImplementsAttribute2', 26 | 'InterfaceTemplateImplementsAttribute3' 27 | )] 28 | class ClassTemplateImplementsAttribute implements InterfaceTemplateImplementsAttribute, InterfaceTemplateImplementsAttribute2, InterfaceTemplateImplementsAttribute3 29 | { 30 | } 31 | -------------------------------------------------------------------------------- /tests/data/TemplateExtends/InvalidClassTemplateExtendsAttribute.php: -------------------------------------------------------------------------------- 1 | ')] 24 | #[TemplateExtends('InvalidClassTemplateExtendsAttribute')] 25 | class InvalidClassTemplateExtendsAttributeChild3 extends InvalidClassTemplateExtendsAttribute 26 | { 27 | #[TemplateExtends('InvalidClassTemplateExtendsAttribute')] 28 | public string $name = ''; 29 | } 30 | -------------------------------------------------------------------------------- /tests/ImpureAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Impure/MethodImpureAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionImpureAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Impure/FunctionImpureAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodImpureAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Impure/InvalidMethodImpureAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'Attribute class PhpStaticAnalysis\Attributes\Impure does not have the property target.' => 11, 25 | ]; 26 | 27 | $this->checkExpectedErrors($errors, $expectedErrors); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/TypeAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Type/PropertyTypeAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInvalidPropertyTypeAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Type/InvalidPropertyTypeAttribute.php'); 16 | 17 | $expectedErrors = [ 18 | 'PHPDoc tag @var has invalid value (): Unexpected token "\n ", expected type at offset 11 on line 2' => 10, 19 | 'PHPDoc tag @var has invalid value ($a + $b): Unexpected token "$a", expected type at offset 12 on line 2' => 20, 20 | 'Attribute class PhpStaticAnalysis\Attributes\Type does not have the parameter target.' => 23, 21 | ]; 22 | 23 | $this->checkExpectedErrors($errors, $expectedErrors); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/IsReadOnlyAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/IsReadOnly/PropertyIsReadOnlyAttribute.php'); 10 | $expectedErrors = [ 11 | '@readonly property test\PhpStaticAnalysis\PHPStanExtension\data\IsReadOnly\PropertyIsReadOnlyAttribute::$name is assigned outside of its declaring class.' => 19, 12 | ]; 13 | 14 | $this->checkExpectedErrors($errors, $expectedErrors); 15 | } 16 | 17 | public function testInvalidPropertyIsReadOnlyAttribute(): void 18 | { 19 | $errors = $this->analyse(__DIR__ . '/data/IsReadOnly/InvalidPropertyIsReadOnlyAttribute.php'); 20 | 21 | $expectedErrors = [ 22 | 'Attribute class PhpStaticAnalysis\Attributes\IsReadOnly does not have the method target.' => 16, 23 | ]; 24 | 25 | $this->checkExpectedErrors($errors, $expectedErrors); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/data/Returns/MethodReturnsAttribute.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Assert/MethodAssertAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionAssertAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Assert/FunctionAssertAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodAssertAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Assert/InvalidMethodAssertAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'PHPDoc tag @phpstan-assert has invalid value (): Unexpected token "\n ", expected type at offset 22 on line 2' => 10, 25 | 'Attribute class PhpStaticAnalysis\Attributes\Assert does not have the property target.' => 14, 26 | ]; 27 | 28 | $this->checkExpectedErrors($errors, $expectedErrors); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/PureAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Pure/MethodPureAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionPureAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Pure/FunctionPureAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodPureAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Pure/InvalidMethodPureAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'Attribute class PhpStaticAnalysis\Attributes\Pure does not have the property target.' => 11, 25 | 'Method test\PhpStaticAnalysis\PHPStanExtension\data\Pure\InvalidMethodPureAttribute::getMoreName() is marked as pure but returns void.' => 14, 26 | ]; 27 | 28 | $this->checkExpectedErrors($errors, $expectedErrors); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/data/RequireImplements/TraitRequireImplementsAttribute.php: -------------------------------------------------------------------------------- 1 | ')] 26 | function getClassTemplateContravariantAttributeConsumer(): Consumer 27 | { 28 | return new Consumer(); 29 | } 30 | 31 | #[Param(consumer: 'Consumer')] 32 | function someFunctionThatConsumes(Consumer $consumer): void 33 | { 34 | $consumer->consume(new ClassTemplateContravariantAttributeChild()); 35 | } 36 | 37 | someFunctionThatConsumes(getClassTemplateContravariantAttributeConsumer()); 38 | -------------------------------------------------------------------------------- /tests/data/SelfOut/MethodSelfOutAttribute.php: -------------------------------------------------------------------------------- 1 | ')] // we specify the new type 15 | public function add($item): void 16 | { 17 | } 18 | 19 | /** 20 | * @deprecated 21 | */ 22 | #[Template('TItemValue')] 23 | #[Param(item: 'TItemValue')] 24 | #[SelfOut('self')] 25 | public function addMore($item): void 26 | { 27 | } 28 | 29 | /** 30 | * @self-out self 31 | */ 32 | #[Template('TItemValue')] 33 | #[Param(item: 'TItemValue')] 34 | #[SelfOut('self')] 35 | public function addEvenMore($item): void 36 | { 37 | } 38 | 39 | /** 40 | * @self-out self 41 | */ 42 | #[Template('TItemValue')] 43 | #[Param(item: 'TItemValue')] 44 | public function addMoreAndMore($item): void 45 | { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/AssertIfTrueAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/AssertIfTrue/MethodAssertIfTrueAttribute.php'); 11 | $this->assertCount(0, $errors); 12 | } 13 | 14 | public function testFunctionAssertIfTrueAttribute(): void 15 | { 16 | $errors = $this->analyse(__DIR__ . '/data/AssertIfTrue/FunctionAssertIfTrueAttribute.php'); 17 | $this->assertCount(0, $errors); 18 | } 19 | 20 | public function testInvalidMethodAssertIfTrueAttribute(): void 21 | { 22 | $errors = $this->analyse(__DIR__ . '/data/AssertIfTrue/InvalidMethodAssertIfTrueAttribute.php'); 23 | 24 | $expectedErrors = [ 25 | 'PHPDoc tag @phpstan-assert-if-true has invalid value (): Unexpected token "\n ", expected type at offset 30 on line 2' => 10, 26 | 'Attribute class PhpStaticAnalysis\Attributes\AssertIfTrue does not have the property target.' => 15, 27 | ]; 28 | 29 | $this->checkExpectedErrors($errors, $expectedErrors); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/AssertIfFalseAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/AssertIfFalse/MethodAssertIfFalseAttribute.php'); 11 | $this->assertCount(0, $errors); 12 | } 13 | 14 | public function testFunctionAssertIfFalseAttribute(): void 15 | { 16 | $errors = $this->analyse(__DIR__ . '/data/AssertIfFalse/FunctionAssertIfFalseAttribute.php'); 17 | $this->assertCount(0, $errors); 18 | } 19 | 20 | public function testInvalidMethodAssertIfFalseAttribute(): void 21 | { 22 | $errors = $this->analyse(__DIR__ . '/data/AssertIfFalse/InvalidMethodAssertIfFalseAttribute.php'); 23 | 24 | $expectedErrors = [ 25 | 'PHPDoc tag @phpstan-assert-if-false has invalid value (): Unexpected token "\n ", expected type at offset 31 on line 2' => 10, 26 | 'Attribute class PhpStaticAnalysis\Attributes\AssertIfFalse does not have the property target.' => 15, 27 | ]; 28 | 29 | $this->checkExpectedErrors($errors, $expectedErrors); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/ReturnsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Returns/MethodReturnsAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionReturnsAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Returns/FunctionReturnsAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodReturnsAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Returns/InvalidMethodReturnsAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'PHPDoc tag @return has invalid value (): Unexpected token "\n ", expected type at offset 14 on line 2' => 10, 25 | 'PHPDoc tag @return has invalid value ($a + $b): Unexpected token "$a", expected type at offset 15 on line 2' => 29, 26 | 'Attribute class PhpStaticAnalysis\Attributes\Returns does not have the property target.' => 34, 27 | ]; 28 | 29 | $this->checkExpectedErrors($errors, $expectedErrors); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/data/TemplateCovariant/ClassTemplateCovariantAttribute.php: -------------------------------------------------------------------------------- 1 | className(); 29 | return $object; 30 | } 31 | } 32 | 33 | #[Returns('Creator')] 34 | function getClassTemplateCovariantAttributeCreator(): Creator 35 | { 36 | return new Creator(ClassTemplateCovariantAttributeChild::class); 37 | } 38 | 39 | #[Param(creator: 'Creator')] 40 | function someFunctionThatCreates(Creator $creator): void 41 | { 42 | $creator->create(); 43 | } 44 | 45 | someFunctionThatCreates(getClassTemplateCovariantAttributeCreator()); 46 | -------------------------------------------------------------------------------- /src/Parser/AttributeParser.php: -------------------------------------------------------------------------------- 1 | parser->parseFile($file); 25 | return $this->traverseAst($ast); 26 | } 27 | 28 | public function parseString(string $sourceCode): array 29 | { 30 | $ast = $this->parser->parseString($sourceCode); 31 | return $this->traverseAst($ast); 32 | } 33 | 34 | #[Param(ast: 'Stmt[]')] 35 | #[Returns('Stmt[]')] 36 | private function traverseAst(array $ast): array 37 | { 38 | $traverser = new NodeTraverser(); 39 | $nodeVisitor = new AttributeNodeVisitor(AttributeNodeVisitor::TOOL_PHPSTAN); 40 | $traverser->addVisitor($nodeVisitor); 41 | 42 | $ast = $traverser->traverse($ast); 43 | Assert::allIsInstanceOf($ast, Stmt::class); 44 | return $ast; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/ParamAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Param/MethodParamAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionParamAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Param/FunctionParamAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodParamAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Param/InvalidMethodParamAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'PHPDoc tag @param has invalid value (): Unexpected token "\n ", expected type at offset 13 on line 2' => 10, 25 | 'PHPDoc tag @param has invalid value (string): Unexpected token "\n ", expected variable at offset 20 on line 2' => 16, 26 | 'PHPDoc tag @param has invalid value (count($a) $name): Unexpected token "(", expected variable at offset 19 on line 2' => 22, 27 | 'Attribute class PhpStaticAnalysis\Attributes\Param does not have the property target.' => 27, 28 | ]; 29 | 30 | $this->checkExpectedErrors($errors, $expectedErrors); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/ParamOutAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/ParamOut/MethodParamOutAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testFunctionParamOutAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/ParamOut/FunctionParamOutAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInvalidMethodParamOutAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/ParamOut/InvalidMethodParamOutAttribute.php'); 22 | 23 | $expectedErrors = [ 24 | 'PHPDoc tag @param-out has invalid value (): Unexpected token "\n ", expected type at offset 17 on line 2' => 10, 25 | 'PHPDoc tag @param-out has invalid value (string): Unexpected token "\n ", expected variable at offset 24 on line 2' => 16, 26 | 'PHPDoc tag @param-out has invalid value (count($a) $name): Unexpected token "(", expected variable at offset 23 on line 2' => 22, 27 | 'Attribute class PhpStaticAnalysis\Attributes\ParamOut does not have the property target.' => 27, 28 | ]; 29 | 30 | $this->checkExpectedErrors($errors, $expectedErrors); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/ImportTypeAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/ImportType/ClassImportTypeAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfaceImportTypeAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/ImportType/InterfaceImportTypeAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitImportTypeAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/ImportType/TraitImportTypeAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassImportTypeAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/ImportType/InvalidClassImportTypeAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @phpstan-import-type has invalid value (Unexpected token "(", expected \'*/\' at offset 98 on line 4): Unexpected token "(", expected \'*/\' at offset 98 on line 4' => 11, 31 | 'Attribute class PhpStaticAnalysis\Attributes\ImportType does not have the method target.' => 13, 32 | ]; 33 | 34 | $this->checkExpectedErrors($errors, $expectedErrors); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/MixinAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Mixin/ClassMixinAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfaceMixinAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Mixin/InterfaceMixinAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitMixinAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Mixin/TraitMixinAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassMixinAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Mixin/InvalidClassMixinAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @mixin has invalid value (): Unexpected token "\n * ", expected type at offset 13 on line 2' => 8, 31 | 'PHPDoc tag @mixin has invalid value (count($a)): Unexpected token "(", expected TOKEN_HORIZONTAL_WS at offset 29 on line 3' => 9, 32 | 'Attribute class PhpStaticAnalysis\Attributes\Mixin does not have the method target.' => 11, 33 | ]; 34 | 35 | $this->checkExpectedErrors($errors, $expectedErrors); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/TemplateUseAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/TemplateUse/TraitTemplateUseAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInvalidTraitTemplateUseAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/TemplateUse/InvalidTraitTemplateUseAttribute.php'); 16 | 17 | $expectedErrors = [ 18 | 'Parameter #1 ...$traits of attribute class PhpStaticAnalysis\Attributes\TemplateUse constructor expects string, int given.' => 13, 19 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateUse\InvalidClassTemplateUseAttribute uses generic trait test\PhpStaticAnalysis\PHPStanExtension\data\TemplateUse\InvalidTraitTemplateUseAttribute but does not specify its types: T' => 16, 20 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateUse\InvalidClassTemplateUseAttribute2 uses generic trait test\PhpStaticAnalysis\PHPStanExtension\data\TemplateUse\InvalidTraitTemplateUseAttribute but does not specify its types: T' => 22, 21 | 'Attribute class PhpStaticAnalysis\Attributes\TemplateUse does not have the property target.' => 24, 22 | ]; 23 | 24 | $this->checkExpectedErrors($errors, $expectedErrors); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/RequireImplementsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/RequireImplements/TraitRequireImplementsAttribute.php'); 10 | $expectedErrors = [ 11 | 'Trait test\PhpStaticAnalysis\PHPStanExtension\data\RequireImplements\TraitRequireImplementsAttribute requires using class to implement test\PhpStaticAnalysis\PHPStanExtension\data\RequireImplements\InterfaceRequireImplementsAttribute3, but test\PhpStaticAnalysis\PHPStanExtension\data\RequireImplements\ClassRequireImplementsAttribute2 does not.' => 33, 12 | ]; 13 | 14 | $this->checkExpectedErrors($errors, $expectedErrors); 15 | } 16 | 17 | public function testInvalidClassRequireImplementsAttribute(): void 18 | { 19 | $errors = $this->analyse(__DIR__ . '/data/RequireImplements/InvalidTraitRequireImplementsAttribute.php'); 20 | 21 | $expectedErrors = [ 22 | 'PHPDoc tag @phpstan-require-implements has invalid value (): Unexpected token "\n ", expected type at offset 34 on line 2' => 8, 23 | 'PHPDoc tag @phpstan-require-implements contains non-object type int.' => 12, 24 | 'Attribute class PhpStaticAnalysis\Attributes\RequireImplements does not have the property target.' => 15, 25 | ]; 26 | 27 | $this->checkExpectedErrors($errors, $expectedErrors); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/RequireExtendsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/RequireExtends/TraitRequireExtendsAttribute.php'); 10 | $expectedErrors = [ 11 | 'Trait test\PhpStaticAnalysis\PHPStanExtension\data\RequireExtends\TraitRequireExtendsAttribute requires using class to extend test\PhpStaticAnalysis\PHPStanExtension\data\RequireExtends\ClassRequireExtendsAttribute, but test\PhpStaticAnalysis\PHPStanExtension\data\RequireExtends\ClassRequireExtendsAttributeChild2 does not.' => 21, 12 | ]; 13 | 14 | $this->checkExpectedErrors($errors, $expectedErrors); 15 | } 16 | 17 | public function testInvalidClassRequireExtendsAttribute(): void 18 | { 19 | $errors = $this->analyse(__DIR__ . '/data/RequireExtends/InvalidTraitRequireExtendsAttribute.php'); 20 | 21 | $expectedErrors = [ 22 | 'PHPDoc tag @phpstan-require-extends has invalid value (): Unexpected token "\n ", expected type at offset 31 on line 2' => 8, 23 | 'PHPDoc tag @phpstan-require-extends contains non-object type int.' => 12, 24 | 'PHPDoc tag @phpstan-require-extends can only be used once.' => 17, 25 | 'Attribute class PhpStaticAnalysis\Attributes\RequireExtends does not have the property target.' => 21, 26 | ]; 27 | 28 | $this->checkExpectedErrors($errors, $expectedErrors); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/MethodAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Method/ClassMethodAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfaceMethodAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Method/InterfaceMethodAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitMethodAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Method/TraitMethodAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassMethodAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Method/InvalidClassMethodAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @method has invalid value (): Unexpected token "\n * ", expected type at offset 14 on line 2' => 9, 31 | 'PHPDoc tag @method has invalid value (string): Unexpected token "\n * ", expected \'(\' at offset 32 on line 3' => 10, 32 | 'Attribute class PhpStaticAnalysis\Attributes\Method does not have the method target.' => 13, 33 | 'Call to an undefined method test\PhpStaticAnalysis\PHPStanExtension\data\Method\InvalidClassMethodAttribute::badFunction().' => 31, 34 | ]; 35 | 36 | $this->checkExpectedErrors($errors, $expectedErrors); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/PropertyAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Property/ClassPropertyAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfacePropertyAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Property/InterfacePropertyAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitPropertyAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Property/TraitPropertyAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassPropertyAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Property/InvalidClassPropertyAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @property has invalid value (): Unexpected token "\n * ", expected type at offset 16 on line 2' => 8, 31 | 'PHPDoc tag @property has invalid value (string): Unexpected token "\n * ", expected variable at offset 36 on line 3' => 9, 32 | 'PHPDoc tag @property has invalid value (count($a) $name): Unexpected token "(", expected variable at offset 55 on line 4' => 10, 33 | 'Attribute class PhpStaticAnalysis\Attributes\Property does not have the method target.' => 12, 34 | ]; 35 | 36 | $this->checkExpectedErrors($errors, $expectedErrors); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/data/Template/MethodTemplateAttribute.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Immutable/ClassImmutableAttribute.php'); 11 | $expectedErrors = [ 12 | '@readonly property cannot have a default value.' => 10, 13 | '@readonly property test\PhpStaticAnalysis\PHPStanExtension\data\Immutable\ClassImmutableAttribute::$name is assigned outside of its declaring class.' => 14, 14 | ]; 15 | 16 | $this->checkExpectedErrors($errors, $expectedErrors); 17 | } 18 | 19 | public function testTraitImmutableAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Immutable/TraitImmutableAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInterfaceImmutableAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Immutable/InterfaceImmutableAttribute.php'); 28 | $this->assertCount(0, $errors); 29 | } 30 | 31 | public function testInvalidClassImmutableAttribute(): void 32 | { 33 | $errors = $this->analyse(__DIR__ . '/data/Immutable/InvalidClassImmutableAttribute.php'); 34 | 35 | $expectedErrors = [ 36 | 'Attribute class PhpStaticAnalysis\Attributes\Immutable does not have the property target.' => 13, 37 | '@readonly property cannot have a default value.' => 14, 38 | ]; 39 | 40 | $this->checkExpectedErrors($errors, $expectedErrors); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/TemplateExtendsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/TemplateExtends/ClassTemplateExtendsAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInvalidClassTemplateExtendsAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/TemplateExtends/InvalidClassTemplateExtendsAttribute.php'); 16 | 17 | $expectedErrors = [ 18 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateExtends\InvalidClassTemplateExtendsAttributeChild extends generic class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateExtends\InvalidClassTemplateExtendsAttribute but does not specify its types: T' => 13, 19 | 'PHPDoc tag @template-extends has invalid value (): Unexpected token "\n ", expected type at offset 24 on line 2' => 14, 20 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateExtends\InvalidClassTemplateExtendsAttributeChild2 extends generic class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateExtends\InvalidClassTemplateExtendsAttribute but does not specify its types: T' => 18, 21 | 'PHPDoc tag @template-extends has invalid value (+5): Unexpected token "+5", expected type at offset 25 on line 2' => 19, 22 | 'Attribute class PhpStaticAnalysis\Attributes\TemplateExtends does not have the property target.' => 27, 23 | ]; 24 | 25 | $this->checkExpectedErrors($errors, $expectedErrors); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/TemplateContravariantAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/TemplateContravariant/ClassTemplateContravariantAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testTraitTemplateContravariantAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/TemplateContravariant/TraitTemplateContravariantAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInterfaceTemplateContravariantAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/TemplateContravariant/InterfaceTemplateContravariantAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassTemplateContravariantAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/TemplateContravariant/InvalidClassTemplateContravariantAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @template-contravariant has invalid value (): Unexpected token "\n * ", expected type at offset 30 on line 2' => 8, 31 | 'PHPDoc tag @template-contravariant has invalid value (+5): Unexpected token "+5", expected type at offset 58 on line 3' => 9, 32 | 'Attribute class PhpStaticAnalysis\Attributes\TemplateContravariant does not have the property target.' => 12, 33 | ]; 34 | 35 | $this->checkExpectedErrors($errors, $expectedErrors); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/all_tests.yml: -------------------------------------------------------------------------------- 1 | name: "All Tests" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | test: 9 | name: "Run all checks for all supported PHP versions" 10 | 11 | runs-on: "ubuntu-22.04" 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: 17 | - "8.1" 18 | - "8.2" 19 | - "8.3" 20 | - "8.4" 21 | 22 | steps: 23 | - name: "Checkout" 24 | uses: "actions/checkout@v4" 25 | 26 | - name: "Install PHP" 27 | uses: "shivammathur/setup-php@v2" 28 | with: 29 | php-version: "${{ matrix.php-version }}" 30 | tools: composer 31 | 32 | - name: Get composer cache directory 33 | id: composercache 34 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 35 | 36 | - name: Cache dependencies 37 | uses: actions/cache@v4 38 | with: 39 | path: ${{ steps.composercache.outputs.dir }} 40 | key: "php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}" 41 | restore-keys: "php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}" 42 | 43 | - name: "Install composer dependencies" 44 | run: "composer install --no-interaction --no-progress" 45 | 46 | - name: "Run tests" 47 | run: "composer tests-without-psalm" 48 | 49 | - name: "Run Psalm" 50 | if: matrix.php-version != '8.4' # Psalm does not fully support PHP 8.4 yet 51 | run: "composer psalm" -------------------------------------------------------------------------------- /tests/BaseAttributeTestCase.php: -------------------------------------------------------------------------------- 1 | getFileHelper()->normalizePath($file); 18 | $analyser = self::getContainer()->getByType(Analyser::class); 19 | $fileHelper = self::getContainer()->getByType(FileHelper::class); 20 | $errors = $analyser->analyse([$file], null, null, true)->getErrors(); 21 | foreach ($errors as $error) { 22 | $this->assertSame($fileHelper->normalizePath($file), $error->getFilePath()); 23 | } 24 | 25 | return $errors; 26 | } 27 | 28 | #[Param( 29 | errors: 'Error[]', 30 | expectedErrors: 'array', 31 | )] 32 | protected function checkExpectedErrors( 33 | array $errors, 34 | array $expectedErrors 35 | ): void { 36 | $this->assertCount(count($expectedErrors), $errors); 37 | 38 | $errorNum = 0; 39 | foreach ($expectedErrors as $error => $line) { 40 | $this->assertSame($error, $errors[$errorNum]->getMessage()); 41 | $this->assertSame($line, $errors[$errorNum]->getLine()); 42 | $errorNum++; 43 | } 44 | } 45 | 46 | public static function getAdditionalConfigFiles(): array 47 | { 48 | return [ 49 | __DIR__ . '/../extension.neon', 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/DefineTypeAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/DefineType/ClassDefineTypeAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfaceDefineTypeAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/DefineType/InterfaceDefineTypeAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitDefineTypeAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/DefineType/TraitDefineTypeAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassDefineTypeAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/DefineType/InvalidClassDefineTypeAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'Type alias has an invalid name: string.' => 7, 31 | 'PHPDoc tag @phpstan-type has invalid value (): Unexpected token "\n * ", expected type at offset 20 on line 2' => 8, 32 | 'PHPDoc tag @phpstan-type string has invalid value: Unexpected token "\n * ", expected type at offset 44 on line 3' => 9, 33 | 'PHPDoc tag @phpstan-type name has invalid value: Unexpected token "(", expected TOKEN_PHPDOC_EOL at offset 72 on line 4' => 10, 34 | 'Attribute class PhpStaticAnalysis\Attributes\DefineType does not have the method target.' => 12, 35 | ]; 36 | 37 | $this->checkExpectedErrors($errors, $expectedErrors); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/TemplateImplementsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/TemplateImplements/InterfaceTemplateImplementsAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInvalidInterfaceTemplateImplementsAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/TemplateImplements/InvalidInterfaceTemplateImplementsAttribute.php'); 16 | 17 | $expectedErrors = [ 18 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateImplements\InvalidClassTemplateImplementsAttribute implements generic interface test\PhpStaticAnalysis\PHPStanExtension\data\TemplateImplements\InvalidInterfaceTemplateImplementsAttribute but does not specify its types: T' => 13, 19 | 'PHPDoc tag @template-implements has invalid value (): Unexpected token "\n ", expected type at offset 27 on line 2' => 14, 20 | 'Class test\PhpStaticAnalysis\PHPStanExtension\data\TemplateImplements\InvalidClassTemplateImplementsAttribute2 implements generic interface test\PhpStaticAnalysis\PHPStanExtension\data\TemplateImplements\InvalidInterfaceTemplateImplementsAttribute but does not specify its types: T' => 18, 21 | 'PHPDoc tag @template-implements has invalid value (+5): Unexpected token "+5", expected type at offset 28 on line 2' => 19, 22 | 'Attribute class PhpStaticAnalysis\Attributes\TemplateImplements does not have the property target.' => 21, 23 | ]; 24 | 25 | $this->checkExpectedErrors($errors, $expectedErrors); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/TemplateCovariantAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/TemplateCovariant/ClassTemplateCovariantAttribute.php'); 10 | $expectedErrors = [ 11 | 'PHPDoc tag @var with type TCreable is not subtype of native type object.' => 28, 12 | ]; 13 | $this->checkExpectedErrors($errors, $expectedErrors); 14 | } 15 | 16 | public function testTraitTemplateCovariantAttribute(): void 17 | { 18 | $errors = $this->analyse(__DIR__ . '/data/TemplateCovariant/TraitTemplateCovariantAttribute.php'); 19 | $this->assertCount(0, $errors); 20 | } 21 | 22 | public function testInterfaceTemplateCovariantAttribute(): void 23 | { 24 | $errors = $this->analyse(__DIR__ . '/data/TemplateCovariant/InterfaceTemplateCovariantAttribute.php'); 25 | $this->assertCount(0, $errors); 26 | } 27 | 28 | public function testInvalidClassTemplateCovariantAttribute(): void 29 | { 30 | $errors = $this->analyse(__DIR__ . '/data/TemplateCovariant/InvalidClassTemplateCovariantAttribute.php'); 31 | 32 | $expectedErrors = [ 33 | 'PHPDoc tag @template-covariant has invalid value (): Unexpected token "\n * ", expected type at offset 26 on line 2' => 8, 34 | 'PHPDoc tag @template-covariant has invalid value (+5): Unexpected token "+5", expected type at offset 50 on line 3' => 9, 35 | 'Attribute class PhpStaticAnalysis\Attributes\TemplateCovariant does not have the property target.' => 12, 36 | ]; 37 | 38 | $this->checkExpectedErrors($errors, $expectedErrors); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/PropertyReadAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/PropertyRead/ClassPropertyReadAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfacePropertyReadAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/PropertyRead/InterfacePropertyReadAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitPropertyReadAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/PropertyRead/TraitPropertyReadAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassPropertyReadAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/PropertyRead/InvalidClassPropertyReadAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @property-read has invalid value (): Unexpected token "\n * ", expected type at offset 21 on line 2' => 8, 31 | 'PHPDoc tag @property-read has invalid value (string): Unexpected token "\n * ", expected variable at offset 46 on line 3' => 9, 32 | 'PHPDoc tag @property-read has invalid value (count($a) $name): Unexpected token "(", expected variable at offset 70 on line 4' => 10, 33 | 'Attribute class PhpStaticAnalysis\Attributes\PropertyRead does not have the method target.' => 13, 34 | 'Property test\PhpStaticAnalysis\PHPStanExtension\data\PropertyRead\ClassPropertyReadAttribute::$age is not writable.' => 21, 35 | ]; 36 | 37 | $this->checkExpectedErrors($errors, $expectedErrors); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/PropertyWriteAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/PropertyWrite/ClassPropertyWriteAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testInterfacePropertyWriteAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/PropertyWrite/InterfacePropertyWriteAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testTraitPropertyWriteAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/PropertyWrite/TraitPropertyWriteAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testInvalidClassPropertyWriteAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/PropertyWrite/InvalidClassPropertyWriteAttribute.php'); 28 | 29 | $expectedErrors = [ 30 | 'PHPDoc tag @property-write has invalid value (): Unexpected token "\n * ", expected type at offset 22 on line 2' => 8, 31 | 'PHPDoc tag @property-write has invalid value (string): Unexpected token "\n * ", expected variable at offset 48 on line 3' => 9, 32 | 'PHPDoc tag @property-write has invalid value (count($a) $name): Unexpected token "(", expected variable at offset 73 on line 4' => 10, 33 | 'Attribute class PhpStaticAnalysis\Attributes\PropertyWrite does not have the method target.' => 13, 34 | 'Property test\PhpStaticAnalysis\PHPStanExtension\data\PropertyWrite\ClassPropertyWriteAttribute::$age is not readable.' => 21, 35 | ]; 36 | 37 | $this->checkExpectedErrors($errors, $expectedErrors); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/InternalAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Internal/ClassInternalAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testTraitInternalAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Internal/TraitInternalAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInterfaceInternalAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Internal/InterfaceInternalAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testMethodInternalAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Internal/MethodInternalAttribute.php'); 28 | $this->assertCount(0, $errors); 29 | } 30 | 31 | public function testFunctionInternalAttribute(): void 32 | { 33 | $errors = $this->analyse(__DIR__ . '/data/Internal/FunctionInternalAttribute.php'); 34 | $this->assertCount(0, $errors); 35 | } 36 | 37 | public function testPropertyInternalAttribute(): void 38 | { 39 | $errors = $this->analyse(__DIR__ . '/data/Internal/PropertyInternalAttribute.php'); 40 | $this->assertCount(0, $errors); 41 | } 42 | 43 | public function testInvalidMethodInternalAttribute(): void 44 | { 45 | $errors = $this->analyse(__DIR__ . '/data/Internal/InvalidMethodInternalAttribute.php'); 46 | $expectedErrors = [ 47 | 'Attribute class PhpStaticAnalysis\Attributes\Internal does not have the parameter target.' => 15, 48 | ]; 49 | 50 | $this->checkExpectedErrors($errors, $expectedErrors); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/ThrowsAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Throws/MethodThrowsAttribute.php'); 10 | $expectedErrors = [ 11 | 'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\MethodThrowsAttribute::countNoErrorName() has Exception in PHPDoc @throws tag but it\'s not thrown.' => 72, 12 | ]; 13 | 14 | $this->checkExpectedErrors($errors, $expectedErrors); 15 | } 16 | 17 | public function testFunctionThrowsAttribute(): void 18 | { 19 | $errors = $this->analyse(__DIR__ . '/data/Throws/FunctionThrowsAttribute.php'); 20 | $this->assertCount(0, $errors); 21 | } 22 | 23 | public function testInvalidMethodThrowsAttribute(): void 24 | { 25 | $errors = $this->analyse(__DIR__ . '/data/Throws/InvalidMethodThrowsAttribute.php'); 26 | 27 | $expectedErrors = [ 28 | 'PHPDoc tag @throws has invalid value (): Unexpected token "\n ", expected type at offset 14 on line 2' => 11, 29 | 'Method test\PhpStaticAnalysis\PHPStanExtension\data\Throws\InvalidMethodThrowsAttribute::getOtherNameLength() has string in PHPDoc @throws tag but it\'s not thrown.' => 16, 30 | 'PHPDoc tag @throws with type string is not subtype of Throwable' => 16, 31 | 'Attribute class PhpStaticAnalysis\Attributes\Throws does not have the property target.' => 22, 32 | ]; 33 | 34 | $this->checkExpectedErrors($errors, $expectedErrors); 35 | } 36 | 37 | public static function getAdditionalConfigFiles(): array 38 | { 39 | return array_merge( 40 | parent::getAdditionalConfigFiles(), 41 | [ 42 | __DIR__ . '/conf/throws.neon', 43 | ] 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/TemplateAttributeTest.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Template/ClassTemplateAttribute.php'); 10 | $this->assertCount(0, $errors); 11 | } 12 | 13 | public function testTraitTemplateAttribute(): void 14 | { 15 | $errors = $this->analyse(__DIR__ . '/data/Template/TraitTemplateAttribute.php'); 16 | $this->assertCount(0, $errors); 17 | } 18 | 19 | public function testInterfaceTemplateAttribute(): void 20 | { 21 | $errors = $this->analyse(__DIR__ . '/data/Template/InterfaceTemplateAttribute.php'); 22 | $this->assertCount(0, $errors); 23 | } 24 | 25 | public function testMethodTemplateAttribute(): void 26 | { 27 | $errors = $this->analyse(__DIR__ . '/data/Template/MethodTemplateAttribute.php'); 28 | $this->assertCount(0, $errors); 29 | } 30 | 31 | public function testFunctionTemplateAttribute(): void 32 | { 33 | $errors = $this->analyse(__DIR__ . '/data/Template/FunctionTemplateAttribute.php'); 34 | $this->assertCount(0, $errors); 35 | } 36 | 37 | public function testInvalidMethodTemplateAttribute(): void 38 | { 39 | $errors = $this->analyse(__DIR__ . '/data/Template/InvalidMethodTemplateAttribute.php'); 40 | 41 | $expectedErrors = [ 42 | 'PHPDoc tag @template has invalid value (): Unexpected token "\n ", expected type at offset 16 on line 2' => 12, 43 | 'PHPDoc tag @template has invalid value (+5): Unexpected token "+5", expected type at offset 17 on line 2' => 18, 44 | 'Attribute class PhpStaticAnalysis\Attributes\Template does not have the property target.' => 23, 45 | ]; 46 | 47 | $this->checkExpectedErrors($errors, $expectedErrors); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-static-analysis/phpstan-extension", 3 | "description": "PHPStan extension to read static analysis attributes", 4 | "type": "phpstan-extension", 5 | "keywords": ["dev", "static analysis"], 6 | "license": "MIT", 7 | "autoload": { 8 | "psr-4": { 9 | "PhpStaticAnalysis\\PHPStanExtension\\": "src/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "test\\PhpStaticAnalysis\\PHPStanExtension\\": "tests/" 15 | } 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Carlos Granados", 20 | "email": "carlos@fastdebug.io" 21 | } 22 | ], 23 | "minimum-stability": "dev", 24 | "prefer-stable": true, 25 | "require": { 26 | "php": ">=8.1", 27 | "php-static-analysis/attributes": "^0.5.0 || dev-main", 28 | "php-static-analysis/node-visitor": "^0.5.0 || dev-main", 29 | "phpstan/phpstan": "^2.0", 30 | "webmozart/assert": "^1.11" 31 | }, 32 | "require-dev": { 33 | "php-static-analysis/psalm-plugin": "^0.5.0 || dev-main", 34 | "phpstan/phpstan-deprecation-rules": "^2.0", 35 | "phpunit/phpunit": "^9.0", 36 | "symplify/easy-coding-standard": "^12.1", 37 | "vimeo/psalm": "^6" 38 | }, 39 | "extra": { 40 | "phpstan": { 41 | "includes": [ 42 | "extension.neon" 43 | ] 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true, 48 | "allow-plugins": { 49 | "php-static-analysis/psalm-plugin": true 50 | } 51 | }, 52 | "scripts": { 53 | "phpstan": "phpstan analyse", 54 | "phpstan-debug": "phpstan analyse --xdebug --debug", 55 | "ecs": "ecs", 56 | "ecs-fix": "ecs --fix", 57 | "phpunit": "phpunit", 58 | "psalm": "psalm", 59 | "tests": [ 60 | "@ecs", 61 | "@phpstan", 62 | "@phpunit", 63 | "@psalm" 64 | ], 65 | "tests-without-psalm": [ 66 | "@ecs", 67 | "@phpstan", 68 | "@phpunit" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/data/Throws/MethodThrowsAttribute.php: -------------------------------------------------------------------------------- 1 | analyse(__DIR__ . '/data/Deprecated/ClassDeprecatedAttribute.php'); 10 | $expectedErrors = [ 11 | 'Instantiation of deprecated class test\PhpStaticAnalysis\PHPStanExtension\data\Deprecated\ClassDeprecatedAttribute.' => 12, 12 | ]; 13 | 14 | $this->checkExpectedErrors($errors, $expectedErrors); 15 | } 16 | 17 | public function testTraitDeprecatedAttribute(): void 18 | { 19 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/TraitDeprecatedAttribute.php'); 20 | $this->assertCount(0, $errors); 21 | } 22 | 23 | public function testInterfaceDeprecatedAttribute(): void 24 | { 25 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/InterfaceDeprecatedAttribute.php'); 26 | $this->assertCount(0, $errors); 27 | } 28 | 29 | public function testMethodDeprecatedAttribute(): void 30 | { 31 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/MethodDeprecatedAttribute.php'); 32 | $expectedErrors = [ 33 | 'Call to deprecated method returnDeprecated() of class test\PhpStaticAnalysis\PHPStanExtension\data\Deprecated\MethodDeprecatedAttribute.' => 31, 34 | ]; 35 | 36 | $this->checkExpectedErrors($errors, $expectedErrors); 37 | } 38 | 39 | public function testFunctionDeprecatedAttribute(): void 40 | { 41 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/FunctionDeprecatedAttribute.php'); 42 | $this->assertCount(0, $errors); 43 | } 44 | 45 | public function testPropertyDeprecatedAttribute(): void 46 | { 47 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/PropertyDeprecatedAttribute.php'); 48 | $this->assertCount(0, $errors); 49 | } 50 | 51 | public function testInvalidMethodDeprecatedAttribute(): void 52 | { 53 | $errors = $this->analyse(__DIR__ . '/data/Deprecated/InvalidMethodDeprecatedAttribute.php'); 54 | 55 | $expectedErrors = [ 56 | 'Attribute class PhpStaticAnalysis\Attributes\Deprecated does not have the parameter target.' => 12, 57 | ]; 58 | 59 | $this->checkExpectedErrors($errors, $expectedErrors); 60 | } 61 | 62 | public static function getAdditionalConfigFiles(): array 63 | { 64 | return array_merge( 65 | parent::getAdditionalConfigFiles(), 66 | [ 67 | __DIR__ . '/conf/deprecated.neon', 68 | ] 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/data/AssertIfTrue/MethodAssertIfTrueAttribute.php: -------------------------------------------------------------------------------- 1 | name')] 31 | public function checkOtherPropertyString(mixed $name): bool 32 | { 33 | return is_string($this->name); 34 | } 35 | 36 | /** 37 | * @deprecated 38 | */ 39 | #[AssertIfTrue(name: 'string')] 40 | public function checkAnotherString(mixed $name): bool 41 | { 42 | return is_string($name); 43 | } 44 | 45 | /** 46 | * @assert int $name 47 | */ 48 | #[AssertIfTrue(name: 'string')] 49 | public function checkEvenMoreString(mixed $name): bool 50 | { 51 | return is_string($name); 52 | } 53 | 54 | #[AssertIfTrue( 55 | name1: 'string', 56 | name2: 'string' 57 | )] 58 | public function checkStrings(mixed $name1, mixed $name2): bool 59 | { 60 | return is_string($name1) && is_string($name2); 61 | } 62 | 63 | #[AssertIfTrue(name1: 'string')] 64 | #[AssertIfTrue(name2: 'string')] 65 | public function checkOtherStrings(mixed $name1, mixed $name2): bool 66 | { 67 | return is_string($name1) && is_string($name2); 68 | } 69 | 70 | /** 71 | * @assert string $name 72 | */ 73 | public function checkMoreAndMoreString(mixed $name): bool 74 | { 75 | return is_string($name); 76 | } 77 | 78 | public function checkStringInParam( 79 | #[AssertIfTrue('string')] 80 | mixed $name 81 | ): bool { 82 | return is_string($name); 83 | } 84 | 85 | public function checkStringInParamWithName( 86 | #[AssertIfTrue(name: 'string')] 87 | mixed $name 88 | ): bool { 89 | return is_string($name); 90 | } 91 | 92 | public function checkStringInTwoParams( 93 | #[AssertIfTrue('string')] 94 | mixed $name1, 95 | #[AssertIfTrue('string')] 96 | mixed $name2 97 | ): bool { 98 | return is_string($name1) && is_string($name2); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/data/AssertIfFalse/MethodAssertIfFalseAttribute.php: -------------------------------------------------------------------------------- 1 | name')] 31 | public function checkOtherPropertyString(mixed $name): bool 32 | { 33 | return !is_string($this->name); 34 | } 35 | 36 | /** 37 | * @deprecated 38 | */ 39 | #[AssertIfFalse(name: 'string')] 40 | public function checkAnotherString(mixed $name): bool 41 | { 42 | return !is_string($name); 43 | } 44 | 45 | /** 46 | * @assert int $name 47 | */ 48 | #[AssertIfFalse(name: 'string')] 49 | public function checkEvenMoreString(mixed $name): bool 50 | { 51 | return !is_string($name); 52 | } 53 | 54 | #[AssertIfFalse( 55 | name1: 'string', 56 | name2: 'string' 57 | )] 58 | public function checkStrings(mixed $name1, mixed $name2): bool 59 | { 60 | return !is_string($name1) || !is_string($name2); 61 | } 62 | 63 | #[AssertIfFalse(name1: 'string')] 64 | #[AssertIfFalse(name2: 'string')] 65 | public function checkOtherStrings(mixed $name1, mixed $name2): bool 66 | { 67 | return !is_string($name1) || !is_string($name2); 68 | } 69 | 70 | /** 71 | * @assert string $name 72 | */ 73 | public function checkMoreAndMoreString(mixed $name): bool 74 | { 75 | return !is_string($name); 76 | } 77 | 78 | public function checkStringInParam( 79 | #[AssertIfFalse('string')] 80 | mixed $name 81 | ): bool { 82 | return !is_string($name); 83 | } 84 | 85 | public function checkStringInParamWithName( 86 | #[AssertIfFalse(name: 'string')] 87 | mixed $name 88 | ): bool { 89 | return !is_string($name); 90 | } 91 | 92 | public function checkStringInTwoParams( 93 | #[AssertIfFalse('string')] 94 | mixed $name1, 95 | #[AssertIfFalse('string')] 96 | mixed $name2 97 | ): bool { 98 | return !is_string($name1) || !is_string($name2); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/data/Assert/MethodAssertAttribute.php: -------------------------------------------------------------------------------- 1 | */ 23 | private array $result; 24 | 25 | /** 26 | * @param array $array1 27 | * @param array $array2 28 | * @return array 29 | */ 30 | public function addArrays(array $array1, array $array2): array 31 | { 32 | $this->result = $array1 + $array2; 33 | return $this->result; 34 | } 35 | } 36 | ``` 37 | 38 | And this is how it would look like using the new attributes: 39 | 40 | ```php 41 | ')] 50 | private array $result; 51 | 52 | #[Param(array1: 'array')] 53 | #[Param(array2: 'array')] 54 | #[Returns('array')] 55 | public function addArrays(array $array1, array $array2): array 56 | { 57 | $this->array = $array1 + $array2; 58 | return $this->array; 59 | } 60 | } 61 | ``` 62 | 63 | ## Installation 64 | 65 | First of all, to make the attributes available for your codebase use: 66 | 67 | ``` 68 | composer require php-static-analysis/attributes 69 | ``` 70 | 71 | To use this extension, require it in Composer: 72 | 73 | ``` 74 | composer require --dev php-static-analysis/phpstan-extension 75 | ``` 76 | 77 | If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! 78 | 79 |
80 | Manual installation 81 | 82 | If you don't want to use `phpstan/extension-installer`, include `extension.neon` in your project's PHPStan config: 83 | 84 | ``` 85 | includes: 86 | - vendor/php-static-analysis/phpstan-extension/extension.neon 87 | ``` 88 |
89 | 90 | ## Using the extension 91 | 92 | This extension works by interacting with the parser that PHPStan uses to parse the code and replacing the new Attributes with PHPDoc annotations that PHPStan can understand. The functionality provided by the attribute is exactly the same as the one provided by the corresponding PHPDoc annotation. 93 | 94 | These are the available attributes and their corresponding PHPDoc annotations: 95 | 96 | | Attribute | PHPDoc Annotations | 97 | |-------------------------------------------------------------------------------------------------------------------|--------------------------------------| 98 | | [Assert](https://github.com/php-static-analysis/attributes/blob/main/doc/Assert.md) | `@assert` | 99 | | [AssertIfFalse](https://github.com/php-static-analysis/attributes/blob/main/doc/AssertIfFalse.md) | `@assert-if-false` | 100 | | [AssertIfTrue](https://github.com/php-static-analysis/attributes/blob/main/doc/AssertIfTrue.md) | `@assert-if-true` | 101 | | [DefineType](https://github.com/php-static-analysis/attributes/blob/main/doc/DefineType.md) | `@type` | 102 | | [Deprecated](https://github.com/php-static-analysis/attributes/blob/main/doc/Deprecated.md) | `@deprecated` | 103 | | [Immmutable](https://github.com/php-static-analysis/attributes/blob/main/doc/Immmutable.md) | `@immmutable` | 104 | | [ImportType](https://github.com/php-static-analysis/attributes/blob/main/doc/ImportType.md) | `@import-type` | 105 | | [Impure](https://github.com/php-static-analysis/attributes/blob/main/doc/Impure.md) | `@impure` | 106 | | [Internal](https://github.com/php-static-analysis/attributes/blob/main/doc/Internal.md) | `@internal` | 107 | | [IsReadOnly](https://github.com/php-static-analysis/attributes/blob/main/doc/IsReadOnly.md) | `@readonly` | 108 | | [Method](https://github.com/php-static-analysis/attributes/blob/main/doc/Method.md) | `@method` | 109 | | [Mixin](https://github.com/php-static-analysis/attributes/blob/main/doc/Mixin.md) | `@mixin` | 110 | | [Param](https://github.com/php-static-analysis/attributes/blob/main/doc/Param.md) | `@param` | 111 | | [ParamOut](https://github.com/php-static-analysis/attributes/blob/main/doc/ParamOut.md) | `@param-out` | 112 | | [Property](https://github.com/php-static-analysis/attributes/blob/main/doc/Property.md) | `@property` `@var` | 113 | | [PropertyRead](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyRead.md) | `@property-read` | 114 | | [PropertyWrite](https://github.com/php-static-analysis/attributes/blob/main/doc/PropertyWrite.md) | `@property-write` | 115 | | [Pure](https://github.com/php-static-analysis/attributes/blob/main/doc/Pure.md) | `@pure` | 116 | | [RequireExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireExtends.md) | `@require-extends` | 117 | | [RequireImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/RequireImplements.md) | `@require-implements` | 118 | | [Returns](https://github.com/php-static-analysis/attributes/blob/main/doc/Returns.md) | `@return` | 119 | | [SelfOut](https://github.com/php-static-analysis/attributes/blob/main/doc/SelfOut.md) | `@self-out` `@this-out` | 120 | | [Template](https://github.com/php-static-analysis/attributes/blob/main/doc/Template.md) | `@template` | 121 | | [TemplateContravariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateContravariant.md) | `@template-contravariant` | 122 | | [TemplateCovariant](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateCovariant.md) | `@template-covariant` | 123 | | [TemplateExtends](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateExtends.md) | `@extends` `@template-extends` | 124 | | [TemplateImplements](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateImplements.md) | `@implements` `@template-implements` | 125 | | [TemplateUse](https://github.com/php-static-analysis/attributes/blob/main/doc/TemplateUse.md) | `@use` `@template-use` | 126 | | [Throws](https://github.com/php-static-analysis/attributes/blob/main/doc/Throws.md) | `@throws` | 127 | | [Type](https://github.com/php-static-analysis/attributes/blob/main/doc/Type.md) | `@var` `@return` | 128 | 129 | ## Sponsor this project 130 | 131 | If you would like to support the development of this project, please consider [sponsoring me](https://github.com/sponsors/carlos-granados) 132 | 133 | 134 | 135 | 136 | --------------------------------------------------------------------------------