Rector 2.2: New rules for Array Docblocks

As you know, we provide an upgrade services to speed up the modernization of codebases. Part of this service is getting PHPStan to level 8 with no baseline (only edge cases).

Level 6 is known for requesting more detailed types over array, iterable or Iterator type hints. Bare mixed or array should be replaced with explicit key/value types, e.g., string[] or array<int, SomeObject>.

At first, we did this work manually. Later, we made custom Rector rules that we kept private.

Today, we are open-sourcing these rules to help you with the same task.

This feature is experimental. We look for your feedback in real projects. Found a glitch, or do you expect a different output? Let us known on GitHub.

1573 errors... Rector should be able to help with that, right?

Let's look at a few examples that are missing detailed types and that Rector can improve now:

1. Known Return Scalar

function getNames(): array
{
    return ['John', 'Jane'];
}

Now this is straightforward; it can be improved to:

+/**
+ * @return string[]
+ */
 function getNames(): array
 {
     // ...
 }

Why do this manually in 100s of places, if Rector can do it for you?


2. Known Return Objects

Let's look at another example:

function getUsers(): array
{
    $user = [];

    $users[] = new User('John');
    $users[] = new User('Jane');

    return $users;
}

No-brainer:

+/**
+ * @return User[]
+ */
 function getUsers(): array
 {
     // ...
 }

3. Known Shared Object Type

What if there are multiple different objects that all share a single contract interface?

final class ExtensionProvider
{
    public function provide(): array
    {
        return [
            new FirstExtension(),
            new SecondExtension(),
        ];
    }
}

In a real project, we would have to open all of those classes, check parent classes and interfaces, and try to find the first common one. Now we don't have to, Rector does it for us:

+    /**
+     * @return ExtensionInterface[]
+     */
     public function provide(): array
     {
         // ...
     }

4. Known array_map() return

We can infer the type from functions like array_map():

+/**
+ * @return string[]
+ */
 public function getNames(array $users): array
 {
     return array_map(fn (User $user): string => $user->getName(), $users);
 }

5. Known private method types

What if the method is private and is called only in a local class? We can now collect all the method calls and learn their type:

 final class IncomeCalculator
 {
     public function addCompanyTips(): void
     {
        $this->addTips([100, 200, 300]);
     }

     public function addPersonalTips(): void
     {
         $this->addTips([50, 150]);
     }

+    /**
+     * @param int[] $tips
+     */
     private function addTips(array $tips): void
     {
     }
 }

...and many more. Right now, the initial set contains 15 rules, and we plan to extend it further. Got an idea for an obvious rule that you keep doing manually and is not covered yet? Let us know.

We designed these rules to avoid filling useless types like mixed, mixed[], or array. If the Rector doesn't know better, it will skip these cases. We want to fill those types they way humans would do to improve code readability and static analysis.


Smart Override

Rector is smart enough to keep detailed types, but override those dummy ones:

 /**
- * @return mixed[]
+ * @return string[]
  */
 function getNames(): array
 {
    return ['one', 'two']
 }

Start with Levels

The best way to start using this set is via level feature. Add this single line to your rector.php config:

<?php

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withTypeCoverageDocblockLevel(0);

And take it one level at a time:

 <?php

 use Rector\Config\RectorConfig;

 return RectorConfig::configure()
-    ->withTypeCoverageDocblockLevel(0);
+    ->withTypeCoverageDocblockLevel(1);

In a rush or feeling lucky? Add full set:

<?php

 use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPreparedSets(typeDeclarationDocblocks: true);

We've put a lot of work into making rules balanced and reliable, but it's still in the early testing phase. Give it a go, let us know how slim your baseline got after a single Rector run.


Happy coding!