diff --git a/src/Abstractions/ClassAbstraction.php b/src/Abstractions/ClassAbstraction.php index 9b7e665..10390da 100644 --- a/src/Abstractions/ClassAbstraction.php +++ b/src/Abstractions/ClassAbstraction.php @@ -63,6 +63,10 @@ public function isDeclaredAbstract() { } } + public function isReadOnly(): bool { + return ($this->class instanceof Class_ ? $this->class->isReadonly() : false); + } + /** * getMethodNames * @@ -199,27 +203,31 @@ public function getPropertyNames() { * @return Property */ public function getProperty($name) { - $properties = Grabber::filterByType($this->class->stmts, \PhpParser\Node\Stmt\Property::class); - foreach ($properties as $prop) { - /** @var \PhpParser\Node\Stmt\Property $prop */ - foreach ($prop->props as $propertyProperty) { - /** @var PropertyProperty $propertyProperty */ - if ($propertyProperty->name == $name) { - if ($prop->isPrivate()) { - $access = "private"; - } else if ($prop->isProtected()) { - $access = "protected"; - } else { - $access = "public"; + foreach ($this->class->stmts as $prop) { + if ($prop instanceof \PhpParser\Node\Stmt\Property) { + /** @var \PhpParser\Node\Stmt\Property $prop */ + foreach ($prop->props as $propertyProperty) { + /** @var PropertyProperty $propertyProperty */ + if ($propertyProperty->name == $name) { + if ($prop->isPrivate()) { + $access = "private"; + } else { + if ($prop->isProtected()) { + $access = "protected"; + } else { + $access = "public"; + } + } + $type = $prop->type; + //if (Config::shouldUseDocBlockForProperties() && empty($type)) { + // $type = Scope::nameFromName($propertyProperty->namespacedType); + //} + return new Property($this,$propertyProperty->name, $type, $access, $prop->isStatic(), $prop->isReadOnly()); } - $type = $prop->type; - //if (Config::shouldUseDocBlockForProperties() && empty($type)) { - // $type = Scope::nameFromName($propertyProperty->namespacedType); - //} - return new Property($propertyProperty->name, $type, $access, $prop->isStatic()); } } } + return null; } public function isEnum(): bool { diff --git a/src/Abstractions/ClassInterface.php b/src/Abstractions/ClassInterface.php index 04da28c..63deba9 100644 --- a/src/Abstractions/ClassInterface.php +++ b/src/Abstractions/ClassInterface.php @@ -94,4 +94,6 @@ public function getConstantExpr($name):null|Expr|Name; public function isInterface(); public function isEnum():bool; + + public function isReadOnly():bool; } \ No newline at end of file diff --git a/src/Abstractions/Property.php b/src/Abstractions/Property.php index 0b20214..a60870b 100644 --- a/src/Abstractions/Property.php +++ b/src/Abstractions/Property.php @@ -27,21 +27,22 @@ class Property { */ private $access; - /** - * @var bool - */ - private $static; + + private bool $isStatic; + + private bool $isReadOnly; /** * Property constructor. * */ - public function __construct(string $name,?\PhpParser\Node $type, string $access, string $isStatic) { + public function __construct(private ClassInterface $cls, string $name,?\PhpParser\Node $type, string $access, bool $isStatic, bool $isReadOnly) { $this->name = $name; $this->access = $access; $this->type = $type; - $this->static = $isStatic; + $this->isStatic = $isStatic; + $this->isReadOnly = $isReadOnly; } /** @@ -53,6 +54,10 @@ public function getName() { return $this->name; } + public function getClass():ClassInterface { + return $this->cls; + } + /** * getAccess * @@ -62,6 +67,10 @@ public function getAccess() { return $this->access; } + public function isReadOnly() { + return $this->isReadOnly; + } + /** * getType * @@ -77,6 +86,6 @@ public function getType() { * @return bool */ public function isStatic() { - return $this->static; + return $this->isStatic; } } \ No newline at end of file diff --git a/src/Abstractions/ReflectedClass.php b/src/Abstractions/ReflectedClass.php index 4391b28..239234d 100644 --- a/src/Abstractions/ReflectedClass.php +++ b/src/Abstractions/ReflectedClass.php @@ -163,10 +163,10 @@ public function getProperty($name) { $access = "public"; } $type = Util::reflectionTypeToPhpParserType($prop->getType()); - return new Property($prop->getName(), $type, $access, $modifiers & \ReflectionProperty::IS_STATIC ); + return new Property($this, $prop->getName(), $type, $access, $modifiers & \ReflectionProperty::IS_STATIC, $modifiers & \ReflectionProperty::IS_READONLY ); } return null; - } catch (\ReflectionException $exception) { + } catch (\ReflectionException) { return null; } } @@ -188,4 +188,12 @@ public function getPropertyNames() { public function isEnum(): bool { return $this->refl instanceof \ReflectionEnum; } + + public function isReadOnly(): bool { + /* + if (method_exists($this->refl,"isReadOnly")) { + return $this->refl->isReadOnly(); + }*/ + return false; + } } \ No newline at end of file diff --git a/src/Checks/CatchCheck.php b/src/Checks/CatchCheck.php index 5cfd1b2..805be54 100644 --- a/src/Checks/CatchCheck.php +++ b/src/Checks/CatchCheck.php @@ -54,6 +54,10 @@ public function run($fileName, Node $node, ClassLike $inside = null, Scope $scop } } else if (!$this->symbolTable->isDefinedClass($name)) { $this->emitError($fileName, $node, ErrorConstants::TYPE_UNKNOWN_CLASS, "Attempt to catch unknown type: $name"); + } else { + if (!$this->symbolTable->isParentClassOrInterface("throwable", $name)) { + $this->emitError($fileName, $node, ErrorConstants::TYPE_UNKNOWN_CLASS, "Attempt to catch exception $name that doesn't derive from \Throwable"); + } } } } diff --git a/src/Checks/ErrorConstants.php b/src/Checks/ErrorConstants.php index bebb8bb..6940076 100644 --- a/src/Checks/ErrorConstants.php +++ b/src/Checks/ErrorConstants.php @@ -31,6 +31,7 @@ class ErrorConstants { const TYPE_GLOBAL_EXPRESSION_ACCESSED = 'Standard.Global.Expression'; const TYPE_GLOBAL_STRING_ACCESSED = 'Standard.Global.String'; const TYPE_GOTO = 'Standard.Goto'; + const TYPE_READONLY_DECLARATION = "Standard.Incorrect.ReadOnly"; const TYPE_ILLEGAL_ENUM = 'Standard.Illegal.Enum'; const TYPE_INCORRECT_DYNAMIC_CALL = 'Standard.Incorrect.Dynamic'; const TYPE_INCORRECT_STATIC_CALL = 'Standard.Incorrect.Static'; diff --git a/src/Checks/PropertyFetchCheck.php b/src/Checks/PropertyFetchCheck.php index a4ebe01..4e578d1 100644 --- a/src/Checks/PropertyFetchCheck.php +++ b/src/Checks/PropertyFetchCheck.php @@ -65,12 +65,12 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope= // SimpleXMLElement has arbitrary properties based on the XML that was parsed. return; } - list($property, $declaredIn) = Util::findAbstractedProperty($typeStr, strval($node->name), $this->symbolTable); + $property= Util::findAbstractedProperty($typeStr, strval($node->name), $this->symbolTable); if (!$property) { $this->handleUndeclaredProperty($fileName, $node, $typeStr); } else { - $this->handleDeclaredProperty($fileName, $node, $typeStr, $inside, $property, $declaredIn); + $this->handleDeclaredProperty($fileName, $node, $typeStr, $inside, $property, $property->getClass()->getName()); } } } diff --git a/src/Checks/PropertyStoreCheck.php b/src/Checks/PropertyStoreCheck.php index c2c9612..ce45801 100644 --- a/src/Checks/PropertyStoreCheck.php +++ b/src/Checks/PropertyStoreCheck.php @@ -59,6 +59,28 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope= $targetType = $node->var->getAttribute(TypeComparer::INFERRED_TYPE_ATTR); $valueType = $node->expr->getAttribute(TypeComparer::INFERRED_TYPE_ATTR); + $nodeVarName=strval($node->var->name); + + //echo "Checking safety of assigning ".TypeComparer::typeToString($valueType). " into a ".TypeComparer::typeToString($targetType)."\n"; + + $insideConstructor=$this->insideConstructor($scope); + + TypeComparer::forEachType($targetType, function($individualType) use ($nodeVarName, $fileName, $node, $insideConstructor) { + if ($individualType instanceof Node\Identifier || $individualType instanceof Node\Name) { + $typeStr = strval($individualType); + if ($this->symbolTable->isDefinedClass($typeStr)) { + $property = Util::findAbstractedProperty($typeStr, $nodeVarName, $this->symbolTable); + if ($property && + !$insideConstructor && + ($property->isReadOnly() || $property->getClass()->isReadOnly()) + ) { + $this->emitError($fileName, $node, ErrorConstants::TYPE_ACCESS_VIOLATION, "Attempt to set read only variable " . $typeStr . "->" . $nodeVarName); + } + } + } + }); + + if (!$this->typeComparer->isCompatibleWithTarget($targetType, $valueType, $scope)) { if($targetType instanceof Node\Identifier && util::isScalarType(strval($targetType))) { $errorType = ErrorConstants::TYPE_ASSIGN_MISMATCH_SCALAR; @@ -69,4 +91,17 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope= } } } + + function insideConstructor(?Scope $scope) { + if (!$scope) { + return false; + } + foreach(array_reverse($scope->getParentNodes()) as $parentNode) { + if ($parentNode instanceof Node\FunctionLike || $parentNode instanceof Node\Stmt\Class_) { + return ($parentNode instanceof Node\Stmt\ClassMethod && + strcasecmp($parentNode->name,"__construct")==0); + } + } + return false; + } } diff --git a/src/Checks/ReadOnlyPropertyCheck.php b/src/Checks/ReadOnlyPropertyCheck.php new file mode 100644 index 0000000..acb6072 --- /dev/null +++ b/src/Checks/ReadOnlyPropertyCheck.php @@ -0,0 +1,32 @@ +getParentNodes(); + + /** @var Node\Stmt\Property $prop */ + $prop = end($parentNodes); + $isReadOnlyClass = $inside instanceof Node\Stmt\Class_ && $inside->isReadonly(); + if ($prop->isReadonly() || $isReadOnlyClass) { + if ($node->default !== null) { + $this->emitError($fileName, $node, ErrorConstants::TYPE_READONLY_DECLARATION, "Readonly properties can't have a default value"); + } + if ($prop->type === null) { + $this->emitError($fileName, $node, ErrorConstants::TYPE_READONLY_DECLARATION, "Readonly properties must have a declared type"); + } + } + } + } +} \ No newline at end of file diff --git a/src/Checks/StaticPropertyFetchCheck.php b/src/Checks/StaticPropertyFetchCheck.php index 6fdd727..a58bdc4 100644 --- a/src/Checks/StaticPropertyFetchCheck.php +++ b/src/Checks/StaticPropertyFetchCheck.php @@ -61,7 +61,7 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope= } if ($class instanceof Name && is_string($node->name)) { - list($property,$declaredIn) = Util::findAbstractedProperty($class, $node->name, $this->symbolTable); + $property = Util::findAbstractedProperty($class, $node->name, $this->symbolTable); if (!$property) { $method = Util::findAbstractedMethod($class, $node->name, $this->symbolTable); if ($method) { @@ -77,9 +77,9 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope= if (!$property->isStatic()) { $this->emitError($fileName, $node, ErrorConstants::TYPE_INCORRECT_STATIC_CALL, "Attempt to fetch a dynamic variable statically $class::" . $node->name); } - if ($property->getAccess() == "private" && (!$inside || !isset($inside->namespacedName) || strcasecmp($inside->namespacedName, $declaredIn) != 0)) { + if ($property->getAccess() == "private" && (!$inside || !isset($inside->namespacedName) || strcasecmp($inside->namespacedName, $property->getClass()->getName()) != 0)) { $this->emitError($fileName, $node, ErrorConstants::TYPE_ACCESS_VIOLATION, "Attempt to fetch private property " . $node->name); - } else if ($property->getAccess() == "protected" && (!$inside || !isset($inside->namespacedName) || !$this->symbolTable->isParentClassOrInterface($declaredIn, $inside->namespacedName))) { + } else if ($property->getAccess() == "protected" && (!$inside || !isset($inside->namespacedName) || !$this->symbolTable->isParentClassOrInterface($property->getClass()->getName(), $inside->namespacedName))) { $this->emitError($fileName, $node, ErrorConstants::TYPE_ACCESS_VIOLATION, "Attempt to fetch protected property " . $node->name); } } diff --git a/src/EnumCodeAugmenter.php b/src/EnumCodeAugmenter.php new file mode 100644 index 0000000..43b0af1 --- /dev/null +++ b/src/EnumCodeAugmenter.php @@ -0,0 +1,29 @@ +scalarType); + $property = new Property("name"); + $property->setType(new Node\Identifier("string")); + $property->makeReadonly(); + $enum->stmts[]= $property->getNode(); + if ($isBacked) { + $enum->stmts[] = new Node\Stmt\ClassMethod("values",["returnType"=>"array"]); + $property = new Property("value"); + $property->makeReadonly(); + $property->setType( $enum->scalarType ); + $enum->stmts[]=$property->getNode(); + + $enumName = $enum->namespacedName->toString(); + $param = (new Param("fromValue"))->setType($enum->scalarType); + $enum->stmts[]=new Node\Stmt\ClassMethod("tryFrom",["returnType" => $enumName, "flags"=>Node\Stmt\Class_::MODIFIER_STATIC, 'params'=>[$param->getNode()]]); + $enum->stmts[]=new Node\Stmt\ClassMethod("from", ["returnType" => $enumName, "flags"=>Node\Stmt\Class_::MODIFIER_STATIC, 'params'=>[$param->getNode()]]); + } + } +} diff --git a/src/Evaluators/Expression/PropertyFetch.php b/src/Evaluators/Expression/PropertyFetch.php index bef9c9b..59187e6 100644 --- a/src/Evaluators/Expression/PropertyFetch.php +++ b/src/Evaluators/Expression/PropertyFetch.php @@ -70,7 +70,7 @@ public function getProperty($class, Node\Identifier $name,SymbolTable $table) { function ($class) use ($propName, &$types, &$unknown, $table) { $classDef = $table->getAbstractedClass($class); if ($classDef) { - list($prop) = Util::findAbstractedProperty($class, $propName, $table); + $prop= Util::findAbstractedProperty($class, $propName, $table); if ($prop) { $types[] = $prop->getType(); } else { diff --git a/src/NodeVisitors/Grabber.php b/src/NodeVisitors/Grabber.php index c4f4177..6151758 100644 --- a/src/NodeVisitors/Grabber.php +++ b/src/NodeVisitors/Grabber.php @@ -5,6 +5,7 @@ * Apache 2.0 License */ +use BambooHR\Guardrail\EnumCodeAugmenter; use PhpParser\Error; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; @@ -181,6 +182,7 @@ static public function getClassFromFile( SymbolTable $table, $fileName, $classNa $traverser = new NodeTraverser; $traverser->addVisitor(new TraitImportingVisitor($table)); $stmts = $traverser->traverse($stmts); + } catch (UnknownTraitException $exception) { echo "[$className] Unknown trait! " . $exception->getMessage() . "\n"; // Ignore these for now. @@ -192,7 +194,11 @@ static public function getClassFromFile( SymbolTable $table, $fileName, $classNa } if ($stmts) { - return self::getClassFromStmts($table, $stmts, $className, $classType); + $cls = self::getClassFromStmts($table, $stmts, $className, $classType); + if ($cls instanceof Node\Stmt\Enum_) { + EnumCodeAugmenter::addEnumPropsAndMethods($cls); + } + return $cls; } return null; } diff --git a/src/NodeVisitors/PromotedPropertyVisitor.php b/src/NodeVisitors/PromotedPropertyVisitor.php index 4933191..285902d 100644 --- a/src/NodeVisitors/PromotedPropertyVisitor.php +++ b/src/NodeVisitors/PromotedPropertyVisitor.php @@ -30,7 +30,7 @@ public function enterNode(Node $node) { $newStmts=[]; $newProps=[]; foreach ($constructor->getParams() as $param) { - if ($param->flags & (Class_::MODIFIER_PRIVATE | Class_::MODIFIER_PROTECTED | Class_::MODIFIER_PUBLIC)) { + if ($param->flags & (Class_::MODIFIER_READONLY | Class_::MODIFIER_PRIVATE | Class_::MODIFIER_PROTECTED | Class_::MODIFIER_PUBLIC)) { $newProps[] = $this->buildProp($param); $newStmts[] = $this->buildAssign($param); } @@ -64,6 +64,9 @@ public function buildProp(Node\Param $param) { if($param->flags & Class_::MODIFIER_PROTECTED) { $prop->makeProtected(); } + if($param->flags & Class_::MODIFIER_READONLY) { + $prop->makeReadonly(); + } if($param->type) { $prop->setType( $param->type ); } diff --git a/src/NodeVisitors/StaticAnalyzer.php b/src/NodeVisitors/StaticAnalyzer.php index cd6831f..fb7c245 100644 --- a/src/NodeVisitors/StaticAnalyzer.php +++ b/src/NodeVisitors/StaticAnalyzer.php @@ -28,6 +28,7 @@ use BambooHR\Guardrail\Checks\PropertyFetchCheck; use BambooHR\Guardrail\Checks\PropertyStoreCheck; use BambooHR\Guardrail\Checks\Psr4Check; +use BambooHR\Guardrail\Checks\ReadOnlyPropertyCheck; use BambooHR\Guardrail\Checks\ReturnCheck; use BambooHR\Guardrail\Checks\SplatCheck; use BambooHR\Guardrail\Checks\StaticCallCheck; @@ -147,6 +148,7 @@ function __construct($basePath, $index, OutputInterface $output, $config) new ImagickCheck($this->index, $output), new UnsafeSuperGlobalCheck($this->index, $output), new UseStatementCaseCheck($this->index, $output), + new ReadOnlyPropertyCheck($this->index, $output), //new ClassStoredAsVariableCheck($this->index, $output) ]; diff --git a/src/NodeVisitors/SymbolTableIndexer.php b/src/NodeVisitors/SymbolTableIndexer.php index 03397c4..621d07b 100644 --- a/src/NodeVisitors/SymbolTableIndexer.php +++ b/src/NodeVisitors/SymbolTableIndexer.php @@ -5,10 +5,12 @@ * Apache 2.0 License */ +use BambooHR\Guardrail\EnumCodeAugmenter; use BambooHR\Guardrail\Output\OutputInterface; use PhpParser\Builder\ClassConst; use PhpParser\Builder\Enum_; use PhpParser\Builder\EnumCase; +use PhpParser\Builder\Param; use PhpParser\Builder\Property; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; @@ -78,7 +80,7 @@ public function setFilename($filename) { public function enterNode(Node $node) { if ($node instanceof Node\Stmt\Enum_) { $name=strval($node->namespacedName); - $this->addEnumPropsAndMethods($node); + EnumCodeAugmenter::addEnumPropsAndMethods($node); $this->index->addClass($name, $node, $this->filename); array_push($this->classStack, $node); } elseif ($node instanceof Class_) { @@ -121,25 +123,6 @@ public function enterNode(Node $node) { return null; } - public function addEnumPropsAndMethods(Node\Stmt\Enum_ $enum) { - $isBacked = !is_null($enum->scalarType); - $property = new Property("name"); - $property->setType(new Node\Identifier("string")); - $property->makeReadonly(); - $enum->stmts[]= $property->getNode(); - if ($isBacked) { - $enum->stmts[] = new Node\Stmt\ClassMethod("values",["returnType"=>"array"]); - $property = new Property("value"); - $property->makeReadonly(); - $property->setType( $enum->scalarType ); - $enum->stmts[]=$property->getNode(); - - $enumName = $enum->namespacedName->toString(); - $enum->stmts[]=new Node\Stmt\ClassMethod("tryFrom",["returnType" => $enumName]); - $enum->stmts[]=new Node\Stmt\ClassMethod("from", ["returnType" => $enumName]); - } - } - /** * leaveNode * diff --git a/src/SymbolTable/JsonSymbolTable.php b/src/SymbolTable/JsonSymbolTable.php index ea6d878..46277db 100644 --- a/src/SymbolTable/JsonSymbolTable.php +++ b/src/SymbolTable/JsonSymbolTable.php @@ -94,10 +94,13 @@ public function connect($processNumber) { * @throws Exception */ private function addType($name, $file, $type, $hasTrait = 0, $data = "") { - if (!isset($this->index[$type])) { + $name=strtolower($name); + if (!array_key_exists($type, $this->index)) { $this->index[$type] = []; } - $this->index[$type][strtolower($name)] = ['file' => $file, 'has_trait' => $hasTrait, 'data' => $data]; + if (!array_key_exists($name, $this->index[$type])) { + $this->index[$type][$name] = ['file' => $file, 'has_trait' => $hasTrait, 'data' => $data]; + } } /** diff --git a/src/SymbolTable/SymbolTable.php b/src/SymbolTable/SymbolTable.php index 034d6a1..6f325be 100644 --- a/src/SymbolTable/SymbolTable.php +++ b/src/SymbolTable/SymbolTable.php @@ -5,6 +5,7 @@ * Apache 2.0 License */ +use BambooHR\Guardrail\Abstractions\ClassInterface; use BambooHR\Guardrail\Abstractions\FunctionAbstraction as AbstractionFunction; use BambooHR\Guardrail\Abstractions\ClassAbstraction as AbstractionClass; use BambooHR\Guardrail\Abstractions\ReflectedClass; @@ -169,6 +170,20 @@ public function getAbstractedClass($name) { return $ob; } + function getAbstractedProperty(ClassInterface $class, $propertyName) { + + $cacheName= $propertyName."@".$class->getName(); + $ob = $this->cache->get("AProp:" . $cacheName); + if ($ob) { + return $ob; + } + $ob = $class->getProperty($propertyName); + if($ob) { + $this->cache->add("AProp:" . $cacheName, $ob); + } + return $ob; + } + /** * getAbstractedMethod * diff --git a/src/Util.php b/src/Util.php index 8a46f1e..d1dff4a 100644 --- a/src/Util.php +++ b/src/Util.php @@ -6,6 +6,7 @@ */ +use BambooHR\Guardrail\Abstractions\Property; use BambooHR\Guardrail\SymbolTable\SymbolTable; use PhpParser\Node\Expr\Exit_; use PhpParser\Node\Expr\MethodCall; @@ -230,20 +231,21 @@ static function findAbstractedConstantExpr(string $className, string $constantNa * * @return array First param is the abstracted method, second param is the class it was declared in. */ - static public function findAbstractedProperty($className, $name, SymbolTable $symbolTable) { + static public function findAbstractedProperty(string $className, string $name, SymbolTable $symbolTable):?Property { while ($className) { $class = $symbolTable->getAbstractedClass($className); if (!$class) { - return [null,""]; + return null; } - $prop = $class->getProperty($name); + $prop = $symbolTable->getAbstractedProperty($class, $name); if ($prop) { - return [$prop, $className]; + return $prop; } - $className = $class->getParentClassName(); + + $className=$class->getParentClassName(); } - return [null,""]; + return null; } /** diff --git a/tests/A.php b/tests/A.php deleted file mode 100644 index 51c1db8..0000000 --- a/tests/A.php +++ /dev/null @@ -1,13 +0,0 @@ -instance->foo(); // Ok - $this->instance->bar(); // Not ok. - } -} diff --git a/tests/inference.php b/tests/inference.php deleted file mode 100644 index 0b7f6bb..0000000 --- a/tests/inference.php +++ /dev/null @@ -1,13 +0,0 @@ -foo(); // Not an error -$a->bar(); // Error - -$b=$a; -$b->foo(); // Not an error -$b->bar(); // Error diff --git a/tests/scope.php b/tests/scope.php deleted file mode 100644 index d66810e..0000000 --- a/tests/scope.php +++ /dev/null @@ -1,18 +0,0 @@ -c(); - $a->d($a); - } - - function b(UnknownMethod $a) { - } - - function d(ScopeTest2 $c) { - } -} - -$test=new ScopeTest(); -$test->a($test); -$test->b($test); diff --git a/tests/tests.json b/tests/tests.json deleted file mode 100644 index 9e8fea4..0000000 --- a/tests/tests.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "index": [ - "." - ], - "test": [ - "." - ] -} diff --git a/tests/traits.php b/tests/traits.php deleted file mode 100644 index 3ecd169..0000000 --- a/tests/traits.php +++ /dev/null @@ -1,36 +0,0 @@ -traitFoo(); // Ok. - $this->bar(); // Not ok - } -} - -class DescendantTest extends MyTraitTest { - function callParentTrait() { - $this->traitFoo(); // Ok - $this->traitFoo2();// Ok - $this->bar(); // Not ok - } -} - -function wrapper() { - $myTraitTestInstance=new MyTraitTest(); - $myTraitTestInstance->traitFoo(); // Ok - $myTraitTestInstance->bar(); // Not ok -} - diff --git a/tests/units/Checks/TestData/TestEnumCheck.4.inc b/tests/units/Checks/TestData/TestEnumCheck.4.inc new file mode 100644 index 0000000..fc1bccb --- /dev/null +++ b/tests/units/Checks/TestData/TestEnumCheck.4.inc @@ -0,0 +1,22 @@ +value == 1 || $this->value==7; + } + + function isWeekDay() : bool { + return $this->value >=2 && $this->value<=6; + } +} + +$foo=Weekday::from(1); + diff --git a/tests/units/Checks/TestEnumCheck.php b/tests/units/Checks/TestEnumCheck.php index 0e1e6f0..bec3e28 100644 --- a/tests/units/Checks/TestEnumCheck.php +++ b/tests/units/Checks/TestEnumCheck.php @@ -69,10 +69,12 @@ enum Foo { $this->assertEquals(2, $output->getErrorCount(), "Failed to detect declaring a property in an enum"); } + public function testBackedCallToFrom() { + $this->assertEquals(0, $this->runAnalyzerOnFile('.4.inc', ErrorConstants::TYPE_UNKNOWN_METHOD), "Failed to find enum::from()" ); + } public function testLegalEnumUsage() { $this->assertEquals(0, $this->runAnalyzerOnFile('.3.inc', ErrorConstants::TYPE_ILLEGAL_ENUM), "Failed to detect traits with properties" ); } - } \ No newline at end of file diff --git a/tests/units/Checks/TestReadonlyProps.php b/tests/units/Checks/TestReadonlyProps.php new file mode 100644 index 0000000..3275d13 --- /dev/null +++ b/tests/units/Checks/TestReadonlyProps.php @@ -0,0 +1,77 @@ +analyzeStringToOutput("test.php", $func, ErrorConstants::TYPE_READONLY_DECLARATION, ["basePath" => "/"]); + $this->assertEquals(1, $output->getErrorCount(), "Assigning a constant into a readonly"); + } + + public function testClassLevelReadonlyConstant() { + $func = <<<'ENDCODE' + readonly class Foo { + public int $a=5; + } + + ENDCODE; + + $output = $this->analyzeStringToOutput("test.php", $func, ErrorConstants::TYPE_READONLY_DECLARATION, ["basePath" => "/"]); + $this->assertEquals(1, $output->getErrorCount(), "Assigning a constant into a readonly class"); + } + + public function testUntypedReadonlyClass() { + $func = <<<'ENDCODE' + readonly class Foo { + public $a; + } + + ENDCODE; + + $output = $this->analyzeStringToOutput("test.php", $func, ErrorConstants::TYPE_READONLY_DECLARATION, ["basePath" => "/"]); + $this->assertEquals(1, $output->getErrorCount(), "Untyped property in a readonly class"); + } + public function testUntypedReadonlyProp() { + $func = <<<'ENDCODE' + class Foo { + readonly public $a; + } + + ENDCODE; + + $output = $this->analyzeStringToOutput("test.php", $func, ErrorConstants::TYPE_READONLY_DECLARATION, ["basePath" => "/"]); + $this->assertEquals(1, $output->getErrorCount(), "Untyped property in a readonly property"); + } + + public function testValidReadonly() { + $func = <<<'ENDCODE' + readonly class Foo { + public int $a; + public string $b; + function __construct() { + $a=1; + $b="2"; + } + } + + class Foo2 { + readonly public int $a; + readonly public string $b; + } + + ENDCODE; + + $output = $this->analyzeStringToOutput("test.php", $func, ErrorConstants::TYPE_READONLY_DECLARATION, ["basePath" => "/"]); + $this->assertEquals(0, $output->getErrorCount(), "Untyped property in a readonly property"); + } +} \ No newline at end of file