DynamicTypeAnalysis
was removed from Rector on November 30, 2020 as it was not alligned with Rector philosophy of instant refactoring and was way too theoretical.
Updated Rector YAML to PHP configuration, as current standard.
In previous post we looked at how to migrate from docblocks to type declarations. From @param Type $param
to Type $param
. Even if it doesn't break code like coding standards do, works with inheritance, localized self
and static
and propagates types to all child classes, it's still not such a big deal.
But how do you complete type declarations if don't have any docblocks?
Well, you're doomed, because you should write docblocks everywhere where useful.
<?php
class SomeClass
{
public function getItems()
{
return ['Statie', 'EasyCodingStandard', 'Rector'];
}
}
If you only typed @return
annotation 4 years ago, where no-one thought there will ever be scalar type declarations and such annotations were only for geeks. Transition to PHP 7 features would be so easy:
<?php
class SomeClass
{
- public function getItems()
+ public function getItems(): array
{
return ['Statie', 'EasyCodingStandard', 'Rector'];
}
}
Now, I hope you've learned, that future compatibility is a thing and you'll write annotations to each property, so they Rector will convert them to type declarations once PHP 7.4 is out.
Well, but what if you've inherited such legacy code, are you to blame? No, no-one is to blame even if you wrote the code. That's how change works. We feel like we know all today, but in 5 years array-type declarations will be considered code smell and we keep it only for historical reasons.
So how can we fight the change together?
<?php
class SomeClass
{
public function getItems()
{
$items = ['Statie', 'EasyCodingStandard', 'Rector'];
return $this->sortItems($items);
}
private function sortItems(array $items)
{
sort($items);
return $items;
}
}
How would upgrade this code to type declarations?
<?php
class SomeClass
{
- public function getItems()
+ public function getItems(): array
{
$items = ['Statie', 'EasyCodingStandard', 'Rector'];
return $this->sortItems($items);
}
- private function sortItems(array $items)
+ private function sortItems(array $items): array
{
sort($items);
return $items;
}
}
Good job, that's correct! Now you can do it for the rest of your 25 000 lines of code.
Or... You can be lazy smart and use static analysis from PHPStan to do it for you. It already knows that:
$items
is array
with stringssort
sorts array
and keeps it an array
That was too easy... let's check case we'll find in our code:
<?php
// ...
-public function getResult()
+public function getResult(): float
{
if (true) {
return 5.2;
}
$value = 5.3;
return $value;
}
Don't get confused by incorrect doc:
/**
* @return bool
*/
-public function getNumber()
+public function getNumber(): int
{
return 5;
}
Is one object or null?
<?php
// ...
-public function resolve(Product $product)
+public function resolve(Product $product): ?Product
{
// ...
if (...) {
return null;
}
return $product;
}
If PHPStorm or PHPStan knows what type it is,
there is a big chance Rector can change it everywhere in your code.
Honza Kuchař spend one afternoon over Rector with me and motivated me to add this feature to Rector, so PHP developers can enjoy the laziness AST provides. Thank you, Honza! It's done (see PR).
Just setup Rector and upgrade your code:
<?php
// rector.php
declare(strict_types=1);
use Rector\TypeDeclaration\Rector\FunctionLike\ReturnTypeDeclarationRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(ReturnTypeDeclarationRector::class);
};
vendor/bin/rector process src
Done!
Just one more thing...
@var
?There is a big chance that if our code misses @param
and @return
type declarations, the @var
is missing as well. We don't have to go to legacy code to find such code.
Do you recognize this class?
<?php
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
class Command
{
private $application;
private $name;
private $processTitle;
private $aliases = [];
private $definition;
private $hidden = false;
private $help;
private $description;
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
private $code;
private $synopsis = [];
private $usages = [];
private $helperSet;
// ...
public function setApplication(Application $application = null)
{
$this->application = $application;
}
}
Quiz question: how would you utilize AST to autocomplete all @var
annotations to properties above?
$this->application = $application;
we know that $application
can be upgraded to:
+/**
+ * @var \Symfony\Component\Console\Application
+ */
private $application;
That's it! Let's put this algorithm into Rector:
<?php
// rector.php
declare(strict_types=1);
use Rector\TypeDeclaration\Rector\Property\CompleteVarDocTypePropertyRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(CompleteVarDocTypePropertyRector::class);
};
vendor/bin/rector process src
↓
<?php
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
class Command
{
+ /**
+ * @var \Symfony\Component\Console\Application
+ */
private $application;
+ /**
+ * @var string
+ */
private $name;
+ /**
+ * @var string
+ */
private $processTitle;
+ /**
+ * @var mixed[]|string[]
+ */
private $aliases = [];
+ /**
+ * @var \Symfony\Component\Console\Input\InputDefinition
+ */
private $definition;
+ /**
+ * @var bool
+ */
private $hidden = false;
+ /**
+ * @var string
+ */
private $help;
+ /**
+ * @var string
+ */
private $description;
+ /**
+ * @var bool
+ */
private $ignoreValidationErrors = false;
+ /**
+ * @var bool
+ */
private $applicationDefinitionMerged = false;
+ /**
+ * @var bool
+ */
private $applicationDefinitionMergedWithArgs = false;
+ /**
+ * @var callable
+ */
private $code;
+ /**
+ * @var mixed[]
+ */
private $synopsis = [];
+ /**
+ * @var mixed[]
+ */
private $usages = [];
+ /**
+ * @var \Symfony\Component\Console\Helper\HelperSet
+ */
private $helperSet;
In matter of seconds now we have:
If you want to how Rector works behind this scene, (check the PR). The code size to make this happen is not what you'd expect thanks to php-parser elegance (thanks Nikic!).
What more could be done to turn legacy code to elegant code? Share in comments, I'm eager to know.
Happy coding!