1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Basic implementation

This commit is contained in:
orklah 2021-07-19 19:40:17 +02:00
parent 9cb963f956
commit acfdb82856
6 changed files with 159 additions and 13 deletions

View File

@ -89,6 +89,7 @@
<xs:attribute name="reportInfo" type="xs:boolean" default="true" />
<xs:attribute name="restrictReturnTypes" type="xs:boolean" default="false" />
<xs:attribute name="limitMethodComplexity" type="xs:boolean" default="false" />
<xs:attribute name="triggerErrorExits" type="TriggerErrorExitsType" default="default" />
</xs:complexType>
<xs:complexType name="ProjectFilesType">
@ -646,4 +647,12 @@
<xs:enumeration value="suppress"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="TriggerErrorExitsType">
<xs:restriction base="xs:string">
<xs:enumeration value="default"/>
<xs:enumeration value="none"/>
<xs:enumeration value="always"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -547,6 +547,11 @@ class Config
/** @var list<ConfigIssue> */
public $config_issues = [];
/**
* @var 'default'|'none'|'always'
*/
public $trigger_error_exits = 'default';
protected function __construct()
{
self::$instance = $this;
@ -844,6 +849,7 @@ class Config
'reportInfo' => 'report_info',
'restrictReturnTypes' => 'restrict_return_types',
'limitMethodComplexity' => 'limit_method_complexity',
'triggerErrorExits' => 'trigger_error_exits',
];
foreach ($booleanAttributes as $xmlName => $internalName) {

View File

@ -102,19 +102,6 @@ class ScopeAnalyzer
}
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall
&& $stmt->expr->name instanceof PhpParser\Node\Name
&& $stmt->expr->name->parts === ['trigger_error']
&& isset($stmt->expr->args[1])
&& $stmt->expr->args[1]->value instanceof PhpParser\Node\Expr\ConstFetch
&& in_array(
end($stmt->expr->args[1]->value->name->parts),
['E_ERROR', 'E_PARSE', 'E_CORE_ERROR', 'E_COMPILE_ERROR', 'E_USER_ERROR']
)
) {
return array_values(array_unique(array_merge($control_actions, [self::ACTION_END])));
}
// This allows calls to functions that always exit to act as exit statements themselves
if ($nodes
&& ($stmt_expr_type = $nodes->getType($stmt->expr))

View File

@ -71,6 +71,7 @@ class FunctionReturnTypeProvider
$this->registerClass(ReturnTypeProvider\FirstArgStringReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\HexdecReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\MinMaxReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\TriggerErrorReturnTypeProvider::class);
}
/**

View File

@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Type;
use function in_array;
use const E_USER_DEPRECATED;
use const E_USER_ERROR;
use const E_USER_NOTICE;
use const E_USER_WARNING;
class TriggerErrorReturnTypeProvider implements \Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['trigger_error'];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union
{
$config = $event->getStatementsSource()->getCodebase()->config;
if ($config->trigger_error_exits === 'always') {
return new Type\Union([new Type\Atomic\TNever()]);
}
if ($config->trigger_error_exits === 'none') {
return new Type\Union([new Type\Atomic\TTrue()]);
}
//default behaviour
$call_args = $event->getCallArgs();
$statements_source = $event->getStatementsSource();
if (isset($call_args[1])
&& ($array_arg_type = $statements_source->getNodeTypeProvider()->getType($call_args[1]->value))
) {
$return_types = [];
foreach ($array_arg_type->getAtomicTypes() as $atomicType) {
if ($atomicType instanceof Type\Atomic\TLiteralInt) {
if (in_array($atomicType->value, [E_USER_WARNING, E_USER_DEPRECATED, E_USER_NOTICE], true)) {
$return_types[] = new Type\Atomic\TTrue();
} elseif ($atomicType->value === E_USER_ERROR) {
$return_types[] = new Type\Atomic\TNever();
} else {
// not recognized int literal. return false before PHP8, fatal error since
$return_types[] = new Type\Atomic\TFalse();
}
} else {
$return_types[] = new Type\Atomic\TBool();
}
}
return new Type\Union($return_types);
}
//default value is E_USER_NOTICE, so return true
return new Type\Union([new Type\Atomic\TTrue()]);
}
}

View File

@ -2126,4 +2126,83 @@ class FunctionCallTest extends TestCase
],
];
}
public function testTriggerErrorDefault(): void
{
$config = \Psalm\Config::getInstance();
$config->trigger_error_exits = 'default';
$this->addFile(
'somefile.php',
'<?php
/** @return true */
function returnsTrue(): bool {
return trigger_error("", E_USER_NOTICE);
}
/** @return never */
function returnsNever(): void {
trigger_error("", E_USER_ERROR);
}
/**
* @psalm-suppress ArgumentTypeCoercion
* @return mixed
*/
function returnsNeverOrBool(int $i) {
return trigger_error("", $i);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
public function testTriggerErrorAlways(): void
{
$config = \Psalm\Config::getInstance();
$config->trigger_error_exits = 'always';
$this->addFile(
'somefile.php',
'<?php
/** @return never */
function returnsNever1(): void {
trigger_error("", E_USER_NOTICE);
}
/** @return never */
function returnsNever2(): void {
trigger_error("", E_USER_ERROR);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
public function testTriggerErrorNone(): void
{
$config = \Psalm\Config::getInstance();
$config->trigger_error_exits = 'none';
$this->addFile(
'somefile.php',
'<?php
/** @return true */
function returnsTrue1(): bool {
return trigger_error("", E_USER_NOTICE);
}
/** @return true */
function returnsTrue2(): bool {
return trigger_error("", E_USER_ERROR);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
}