This post describes how I started into PHPStan. You can get a feeling of how I approach complex things and start building a mental model about a new problem domain.

First things first: PHPStan?

PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs even before you write tests for the code. It moves PHP closer to compiled languages in the sense that the correctness of each line of the code can be checked before you run the actual line.

Read more about PHPStan in an introductory article Β»

today my 75th pull request to PHPStan was merged

Looking back I had a great journey contributing to PHPStan.

All started on the 11th Nov 2019, with a small PR fixing some windows-only glitches. At that time the PHPStan development setup was mainly used on macOS, so I had to fix a few windows DX things before starting into the real business.

As a newcomer to the project, I firstly worked on things I had expertise with, like improving DX and integration with the Github Actions pipeline.

non-empty-string inference improvements

To get a better feeling of the static analysis problem domain, I approached a simple problem at first, namely non-empty-string inference in url-functions.

I got addicted by the non-empty-string and introduced it all over the place. If you are curious, I did this very same thing for a lot of functions.

Why is this useful you might think? The more PHPStan is able to learn about more precise types while scanning your code, the less boilerplate code is required to persuade the tool of what you are trying to do.

/**
 * @return list<string>
 */
function x(string $string): array {
    if(strlen($string) === 0){
        return [];
    }

    // PHPStan knows, explode given a non-empty-string separator
    // will not return `false` on PHP7, or throw a `ValueError` on PHP8+
    return explode($string[0], '');
}

positive-int, integer-range-type and math

After getting this non-empty-string thing working, I concentrated my efforts on integer semantics. Doing so the same way as before, I firstly worked on getting positive-int type coverage into a few functions.

Ondrey guided me through all this - as always - and gave me a few pointers regarding the IntegerRangeType. I taught PHPStan math on integer ranges, so it was able to figure out stuff like:

/**
 * @param int<0, 10> $range1
 * @param int<3, 9> $range2
 * @param int<4, 5> $range3
 * @param int $int
 */
public function integerRangeMaxima($range1, $range2, $range3, $int) {
    assertType('int', min($range1, $range2, $int));
    assertType('int', max($range1, $range2, $int));

    assertType('int<0, 5>', min($range1, $range2, $range3));
    assertType('int<4, 10>', max($range1, $range2, $range3));
}

/**
 * @param int<1, 10> $r1
 * @param int<5, 10> $r2
 * @param int<5, max> $rMax
 */
public function integerRangeMath($r1, $r2, $rMax) {
    assertType('int<5, 14>', $r1 + 4);
    assertType('int<-3, 6>', $r1 - 4);
    assertType('int<4, 40>', $r1 * 4);
    assertType('float|int<0, 2>', $r1 / 4);
    assertType('int<9, max>', $rMax + 4);
    assertType('int<1, max>', $rMax - 4);
    assertType('int<20, max>', $rMax * 4);
    assertType('float|int<1, max>', $rMax / 4);

    assertType('int<6, 20>', $r1 + $r2);
    assertType('int<-9, 5>', $r1 - $r2);
    assertType('int<5, 100>', $r1 * $r2);
    assertType('float|int<0, 1>', $r1 / $r2);
}

assertType is a helper function, used for testing in PHPStan, to make sure a given expression will return the given type

It took me a lot of PRs to work out all the rough edges, but it was so much fun. If you are interested, have a look at all those test-cases covered.

non-empty-array inference

You guessed it already… In a similar fashion I worked on previous topics, I started working on non-empty-array inference. It was introduced in a few array functions, like array_combine, array_merge, array_flip etc.

As soon as PHPStan knows when arrays cannot be empty, it is able to help you by e.g. identifying redundant code like count($array) === 0.

When non-empty-array is used in combination with loops like foreach or for, PHPStan can assume a loop-body is evaluated at least once. This in turn helps making things possible like detecting dead code:

/**
 * @param non-empty-array<mixed> $array
 */
public function sayHello($array): void
{
    $x = null;
    foreach($array as $value) {
        $x = 25;
    }
    
    // since foreach will be iterated at least once,
    // $x can never be `null` at this point
    if ($x === null) { 
    }
}

try this snippet on phpstan.org/try

Read more about PHPStan doctypes.

πŸ’Œ Support my open source activities

In case you find this stuff useful, and your daily business depends on PHPStan understanding your code consider supporting my work.

Found a bug? Please help improve this article.


<
Blog Archive
Archive of all previous blog posts
>
Next Post
cs2pr - GitHub Pull Request annotation