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

Trigger error #6142

Merged
merged 6 commits into from
Jul 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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="never"/>
<xs:enumeration value="always"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
10 changes: 10 additions & 0 deletions docs/running_psalm/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,16 @@ When `false`, Psalm will not consider issue at lower level than `errorLevel` as

When `false`, Psalm will not report `ParamNameMismatch` issues in your code anymore. This does not replace the use of individual `@no-named-arguments` to prevent external access to a library's method or to reduce the type to a `list` when using variadics

#### triggerErrorExits

```xml
<psalm
triggerErrorExits="[string]"
>
```

Describe the behavior of trigger_error. `always` means it always exits, `never` means it never exits, `default` means it exits only for `E_USER_ERROR`. Default is `default`

### Running Psalm

#### autoloader
Expand Down
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'|'never'|'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
14 changes: 0 additions & 14 deletions src/Psalm/Internal/Analyzer/ScopeAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use function array_unique;
use function array_values;
use function count;
use function end;
use function in_array;
use function strtolower;

Expand Down Expand Up @@ -102,19 +101,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,66 @@
<?php declare(strict_types=1);

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use Psalm\Internal\Type\TypeCombiner;
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
{
$codebase = $event->getStatementsSource()->getCodebase();
$config = $codebase->config;
if ($config->trigger_error_exits === 'always') {
return new Type\Union([new Type\Atomic\TNever()]);
}

if ($config->trigger_error_exits === 'never') {
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) {
orklah marked this conversation as resolved.
Show resolved Hide resolved
$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 TypeCombiner::combine($return_types, $codebase);
}

//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 testTriggerErrorNever(): void
{
$config = \Psalm\Config::getInstance();
$config->trigger_error_exits = 'never';

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