Skip to content

Commit

Permalink
Basic implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
orklah committed Jul 20, 2021
1 parent 9cb963f commit acfdb82
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 13 deletions.
9 changes: 9 additions & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
6 changes: 6 additions & 0 deletions src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -844,6 +849,7 @@ private static function fromXmlAndPaths(
'reportInfo' => 'report_info',
'restrictReturnTypes' => 'restrict_return_types',
'limitMethodComplexity' => 'limit_method_complexity',
'triggerErrorExits' => 'trigger_error_exits',
];

foreach ($booleanAttributes as $xmlName => $internalName) {
Expand Down
13 changes: 0 additions & 13 deletions src/Psalm/Internal/Analyzer/ScopeAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,6 @@ public static function getControlActions(
}

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))
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public function __construct()
$this->registerClass(ReturnTypeProvider\FirstArgStringReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\HexdecReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\MinMaxReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\TriggerErrorReturnTypeProvider::class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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()]);
}
}
79 changes: 79 additions & 0 deletions tests/FunctionCallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2126,4 +2126,83 @@ function takesAString(string $s): void{
],
];
}

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());
}
}

0 comments on commit acfdb82

Please sign in to comment.