Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attributes to ClassMethod abstraction #93

Merged
merged 10 commits into from
Feb 15, 2024
41 changes: 41 additions & 0 deletions src/Checks/ClassConstCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace BambooHR\Guardrail\Checks;

use BambooHR\Guardrail\Output\OutputInterface;
use BambooHR\Guardrail\Scope;
use BambooHR\Guardrail\SymbolTable\SymbolTable;
use BambooHR\Guardrail\TypeComparer;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassLike;

class ClassConstCheck extends BaseCheck {
private $comparer;
function __construct(SymbolTable $symbolTable, OutputInterface $doc) {
parent::__construct($symbolTable, $doc);
$this->comparer=new TypeComparer($symbolTable);
}

function getCheckNodeTypes() {
return [ClassConst::class];
}

function run($fileName, Node $node, ClassLike $inside = null, Scope $scope = null) {
if ($node instanceof ClassConst) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the node here always be an instance of ClassConst?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I use the if to cast them so that static analyzers will know what we do about the type.

foreach($node->consts as $const) {
/** @var Node\Const_ $const */
$constValue = $const->value->getAttribute(TypeComparer::INFERRED_TYPE_ATTR );
if ($node->type && $constValue) {
if (!$this->comparer->isCompatibleWithTarget($node->type, $constValue, $scope)) {
$this->emitError($fileName, $node, ErrorConstants::TYPE_CONST_TYPE,
"Type mismatch between declared type and constant value " .
TypeComparer::typeToString($node->type) . " vs " .
TypeComparer::typeToString($constValue)
);
}
}
}
}
}
}
9 changes: 3 additions & 6 deletions src/Checks/ErrorConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ErrorConstants {
const TYPE_SIGNATURE_COUNT_EXCESS = 'Standard.Param.Count.Excess';
const TYPE_SIGNATURE_RETURN = 'Standard.Return.Type';
const TYPE_SIGNATURE_TYPE = 'Standard.Param.Type';
const TYPE_CONST_TYPE = 'Standard.Const.Type';
const TYPE_SIGNATURE_TYPE_NULL = "Standard.Null.Param";
const TYPE_UNIMPLEMENTED_METHOD = 'Standard.Inheritance.Unimplemented';
const TYPE_UNKNOWN_CLASS = 'Standard.Unknown.Class';
Expand Down Expand Up @@ -79,13 +80,9 @@ class ErrorConstants {
* @return string[]
*/
static function getConstants() {
$ret = [];
$selfReflection = new \ReflectionClass(self::class);
$constants = $selfReflection->getConstants();
sort($constants);
foreach ($constants as $name => $value) {
$ret[] = $value;
}
return $ret;
return array_values($constants);
}
}
}
6 changes: 5 additions & 1 deletion src/Checks/ParamTypesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public function getCheckNodeTypes() {
*/
protected function isAllowed(Node\ComplexType|Node\NullableType|Node\Name|Node\Identifier|null $name , ClassLike $inside=null) {
$return = true;

if ($name instanceof Node\NullableType && TypeComparer::isNamedIdentifier($name->type,"null")) {
return false;
}
TypeComparer::forEachAnyEveryType($name, function($name2) use ($inside, &$return) {
if($name2===null) {
return;
Expand Down Expand Up @@ -107,7 +111,7 @@ public function run($fileName, Node $node, ClassLike $inside=null, Scope $scope=

if ($node->getReturnType()) {
$returnType = $node->getReturnType();
if (!$this->isAllowed($returnType, $inside)) {
if (!TypeComparer::isNamedIdentifier($returnType,"never") && !$this->isAllowed($returnType, $inside)) {
$returnType = TypeComparer::typeToString($returnType);
$this->emitError($fileName, $node, ErrorConstants::TYPE_UNKNOWN_CLASS, "Reference to an unknown type '$returnType' in return value of $displayName");
}
Expand Down
5 changes: 3 additions & 2 deletions src/Checks/ReturnCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ public function run($fileName, Node $node, ClassLike $inside = null, Scope $scop
$returnType = $insideFunc->getReturnType();

$returnIsVoid = TypeComparer::isNamedIdentifier($returnType,"void");
if ($returnIsVoid) {
$returnIsNever = TypeComparer::isNamedIdentifier($returnType,"never");
if ($returnIsVoid || $returnIsNever) {
if ($node->expr != null) {
$this->emitError($fileName, $node, ErrorConstants::TYPE_SIGNATURE_RETURN, "Attempt to return a value from a void function $functionName");
$this->emitError($fileName, $node, ErrorConstants::TYPE_SIGNATURE_RETURN, "Attempt to return a value from a ".TypeComparer::typeToString($returnType)." function $functionName");
return;
}
} else if ($returnType && $node->expr == null) {
Expand Down
10 changes: 9 additions & 1 deletion src/Evaluators/Expression/CallLike.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,16 @@ function checkForPropertyCastedCall(Node\Expr\FuncCall $func, ScopeStack $scope)
) {
$varName = NodePatterns::getVariableOrPropertyName($func->args[0]->value);
$type = $this->getCastedCallType(strtolower($func->name));

if (!is_null($type) && !is_null($varName)) {
$this->tagScopeAsType($func, $scope, $varName,$type);

if ($func->args[0]->value instanceof Node\Expr\NullsafePropertyFetch) {
// TODO: confirm all elements are non-null or null-safe, then set all
// different lengths of chains as non-null
//$list = explode("->", $varName);
} else {
$this->tagScopeAsType($func, $scope, $varName, $type);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Evaluators/Expression/ConstFetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function getType(SymbolTable $table, Node\Expr\ConstFetch $expr):?Node\Identifie
return TypeComparer::identifierFromName("null");
}
if (strcasecmp($expr->name, "false") == 0 || strcasecmp($expr->name, "true") == 0) {
return TypeComparer::identifierFromName("bool");
return TypeComparer::identifierFromName($expr->name);
}
if (defined($expr->name)) {
// Guardrail doesn't declare any global constants. Any that exist are from the runtime.
Expand Down
2 changes: 2 additions & 0 deletions src/NodeVisitors/StaticAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use BambooHR\Guardrail\Checks\BreakCheck;
use BambooHR\Guardrail\Checks\CatchCheck;
use BambooHR\Guardrail\Checks\ClassConstantCheck;
use BambooHR\Guardrail\Checks\ClassConstCheck;
use BambooHR\Guardrail\Checks\ClassMethodStringCheck;
use BambooHR\Guardrail\Checks\ConditionalAssignmentCheck;
use BambooHR\Guardrail\Checks\ConstructorCheck;
Expand Down Expand Up @@ -149,6 +150,7 @@ function __construct($basePath, $index, OutputInterface $output, $config)
new UnsafeSuperGlobalCheck($this->index, $output),
new UseStatementCaseCheck($this->index, $output),
new ReadOnlyPropertyCheck($this->index, $output),
new ClassConstCheck($this->index, $output),
//new ClassStoredAsVariableCheck($this->index, $output)
];

Expand Down
11 changes: 10 additions & 1 deletion src/TypeComparer.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ function areSimpleTypesCompatible(Name|Identifier|null $target, Name|Identifier|
return true;
}

if (
($targetName=="false" || $targetName=="true" || $targetName=="null") &&
$valueName!=$targetName
) {
return false;
}

if (!$strict) {
if ($targetName=="mixed" || $valueName=="mixed") {
return true;
Expand All @@ -126,7 +133,9 @@ function areSimpleTypesCompatible(Name|Identifier|null $target, Name|Identifier|
*
*/
static function getChainedPropertyFetchName(Node $rootNode):?string {
if ($rootNode instanceof Node\Expr\PropertyFetch && $rootNode->name instanceof Identifier) {
if (($rootNode instanceof Node\Expr\PropertyFetch || $rootNode instanceof Node\Expr\NullsafePropertyFetch)
&& $rootNode->name instanceof Identifier
) {
$left = self::getChainedPropertyFetchName($rootNode->var);
return $left ? ($left."->".$rootNode->name) : null;
} else if ($rootNode instanceof Node\Expr\ArrayDimFetch) {
Expand Down
14 changes: 14 additions & 0 deletions tests/TestSuiteSetup.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php namespace BambooHR\Guardrail\Tests;

use BambooHR\Guardrail\Checks\BaseCheck;
use BambooHR\Guardrail\Checks\ErrorConstants;
use BambooHR\Guardrail\Output\OutputInterface;
use BambooHR\Guardrail\Output\XUnitOutput;
use BambooHR\Guardrail\Phases\AnalyzingPhase;
Expand Down Expand Up @@ -62,6 +63,19 @@ public function analyzeFileToOutput($fileName, $emit, array $additionalConfig =
return $output;
}

public function getStringErrorCount($fileData, $additionalConfig=[]):int {
$counts = $this->analyzeString($fileData,$additionalConfig)->getCounts();
return array_sum($counts);
}

public function analyzeString($fileData, $additionalConfig=[]) {
$fileName = "test.php";
$emit = ErrorConstants::getConstants();
unset( $emit[array_search(ErrorConstants::TYPE_AUTOLOAD_ERROR, $emit)] );
$additionalConfig = array_merge(["basePath" => "/"], $additionalConfig);
return $this->analyzeStringToOutput($fileName, $fileData, $emit, $additionalConfig);
}

public function analyzeStringToOutput(string $fileName, string $fileData, $emit, array $additionalConfig = []) {
if (!str_starts_with($fileData,"<?php")) {
$fileData = "<?php\n".$fileData;
Expand Down
Loading