Skip to content

Commit

Permalink
Refined explode() types
Browse files Browse the repository at this point in the history
Fixes #5039

This patch removes the need for a custom function return type
provider for `explode()`, and instead replaces all that with a single
stub for the `explode()` function, which provides types for some of
the most common `$limit` input values.

With this change, the `$delimiter` is enforced to be a `non-empty-string`,
which will lead to downstream consumers having to adjust some code accordingly,
but that shouldn't affect the most common scenario of exploding a string
based with a constant `literal-string` delimiter, which most PHP devs tend to do.

This change didn't come with an accompanying test, since that would be a bit
wasteful, but it was verified locally with following script:

```php
<?php

$possible0  = explode(',', 'hello, world', -100);
$possible1  = explode(',', 'hello, world', -1);
$possible2  = explode(',', 'hello, world', 1);
$possible3  = explode(',', 'hello, world', 0);
$possible4  = explode(',', 'hello, world', 1);
$possible5  = explode(',', 'hello, world', 2);
$possible6  = explode(',', 'hello, world', 3);
$possible7  = explode(',', 'hello, world', 4);
try {
    $impossible1 = explode('', '', -1);
} catch (Throwable $impossible1) {}

$traced = [$possible0, $possible1, $possible2, $possible3, $possible4, $possible5, $possible6, $possible7, $impossible1];

/** @psalm-trace $traced */

var_dump($traced);

return $traced;
```

Running psalm locally, this produces:

```
psalm on  feature/#5039-more-refined-types-for-explode-core-function [✘!?] via 🐘 v8.1.13 via ❄️  impure (nix-shell) took 6s
❯ ./psalm --no-cache explode.php
Target PHP version: 7.4 (inferred from composer.json) Extensions enabled: dom, simplexml (unsupported extensions: ctype, json, libxml, mbstring, tokenizer)
Scanning files...
Analyzing files...

░

To whom it may concern: Psalm cannot detect unused classes, methods and properties
when analyzing individual files and folders. Run on the full project to enable
complete unused code detection.

ERROR: InvalidArgument - explode.php:12:28 - Argument 1 of explode expects non-empty-string, but '' provided (see https://psalm.dev/004)
    $impossible1 = explode('', '', -1);

ERROR: PossiblyUndefinedGlobalVariable - explode.php:15:108 - Possibly undefined global variable $impossible1 defined in try block (see https://psalm.dev/126)
$traced = [$possible0, $possible1, $possible2, $possible3, $possible4, $possible5, $possible6, $possible7, $impossible1];

ERROR: ForbiddenCode - explode.php:19:1 - Unsafe var_dump (see https://psalm.dev/002)
/** @psalm-trace $traced */

var_dump($traced);

ERROR: Trace - explode.php:19:1 - $traced: list{0: array<never, never>, 1: list{string}, 2: list{string}, 3: list{string}, 4: list{string}, 5: array{0: string, 1?: string}, 6: array{0: string, 1?: string, 2?: string}, 7: non-empty-list<string>, 8?: Throwable|list{string}} (see https://psalm.dev/224)
/** @psalm-trace $traced */

var_dump($traced);

------------------------------
4 errors found
------------------------------

Checks took 6.42 seconds and used 265.394MB of memory
Psalm was unable to infer types in the codebase
```

The actual runtime behavior on PHP 8.x: https://3v4l.org/0NKlW

```
array(9) {
  [0]=>
  array(0) {
  }
  [1]=>
  array(1) {
    [0]=>
    string(5) "hello"
  }
  [2]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [3]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [4]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [5]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [6]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [7]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [8]=>
  object(ValueError)#1 (7) {
    ["message":protected]=>
    string(51) "explode(): Argument #1 ($separator) cannot be empty"
    ["string":"Error":private]=>
    string(0) ""
    ["code":protected]=>
    int(0)
    ["file":protected]=>
    string(9) "/in/X9BfY"
    ["line":protected]=>
    int(12)
    ["trace":"Error":private]=>
    array(1) {
      [0]=>
      array(4) {
        ["file"]=>
        string(9) "/in/X9BfY"
        ["line"]=>
        int(12)
        ["function"]=>
        string(7) "explode"
        ["args"]=>
        array(3) {
          [0]=>
          string(0) ""
          [1]=>
          string(0) ""
          [2]=>
          int(-1)
        }
      }
    }
    ["previous":"Error":private]=>
    NULL
  }
}
```

On PHP 7:

```
Warning: explode(): Empty delimiter in /in/X9BfY on line 12
array(9) {
  [0]=>
  array(0) {
  }
  [1]=>
  array(1) {
    [0]=>
    string(5) "hello"
  }
  [2]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [3]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [4]=>
  array(1) {
    [0]=>
    string(12) "hello, world"
  }
  [5]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [6]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [7]=>
  array(2) {
    [0]=>
    string(5) "hello"
    [1]=>
    string(6) " world"
  }
  [8]=>
  bool(false)
}
```

```
  • Loading branch information
Ocramius committed Dec 28, 2022
1 parent dbcfe62 commit 0a87c54
Show file tree
Hide file tree
Showing 3 changed files with 20 additions and 105 deletions.
2 changes: 0 additions & 2 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Expand Up @@ -25,7 +25,6 @@
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayUniqueReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ExplodeReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\GetClassMethodsReturnTypeProvider;
Expand Down Expand Up @@ -94,7 +93,6 @@ public function __construct()
$this->registerClass(MktimeReturnTypeProvider::class);
$this->registerClass(BasenameReturnTypeProvider::class);
$this->registerClass(DirnameReturnTypeProvider::class);
$this->registerClass(ExplodeReturnTypeProvider::class);
$this->registerClass(GetObjectVarsReturnTypeProvider::class);
$this->registerClass(GetClassMethodsReturnTypeProvider::class);
$this->registerClass(FirstArgStringReturnTypeProvider::class);
Expand Down

This file was deleted.

20 changes: 20 additions & 0 deletions stubs/CoreGenericFunctions.phpstub
Expand Up @@ -725,6 +725,26 @@ function join($separator, array $array = []): string
/**
* @psalm-pure
*
* @param non-empty-string $separator
*
* @return (
* $limit is int<min, -2>
* ? array<empty, empty>
* : (
* $limit is int<-1, 1>
* ? array{string}
* : (
* $limit is 2
* ? array{0: string, 1?: string}
* : (
* $limit is 3
* ? array{0: string, 1?: string, 2?: string}
* : non-empty-list<string>
* )
* )
* )
* )
*
* @psalm-flow ($string) -(array-assignment)-> return
*/
function explode(string $separator, string $string, int $limit = -1) : array {}
Expand Down

0 comments on commit 0a87c54

Please sign in to comment.