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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DynamicTypeAnalysis] Add Dynamic type infering #2264

Merged
merged 2 commits into from Nov 11, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to鈥
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -29,7 +29,6 @@ jobs:
- composer update --prefer-lowest --no-progress

-
stage: test
name: Simple checks
php: 7.1
script:
@@ -38,29 +37,25 @@ jobs:
- php ci/run_all_sets.php

-
stage: test
name: PHPStan
php: 7.1
script:
- composer phpstan

-
stage: test
php: 7.1
name: ECS
script:
- composer check-cs

-
stage: test
name: Unit tests
name: 'Unit tests'
script:
- vendor/bin/phpunit --testsuite main

-
stage: test
php: 7.3
name: Dog Food
name: Rector
script:
- composer rector

@@ -98,7 +98,8 @@
"Rector\\ZendToSymfony\\": "packages/ZendToSymfony/src",
"Rector\\Utils\\DocumentationGenerator\\": "utils/DocumentationGenerator/src",
"Rector\\Utils\\RectorGenerator\\": "utils/RectorGenerator/src",
"Rector\\StrictCodeQuality\\": "packages/StrictCodeQuality/src"
"Rector\\StrictCodeQuality\\": "packages/StrictCodeQuality/src",
"Rector\\DynamicTypeAnalysis\\": "packages/DynamicTypeAnalysis/src"
}
},
"autoload-dev": {
@@ -153,7 +154,8 @@
"Rector\\Twig\\Tests\\": "packages/Twig/tests",
"Rector\\TypeDeclaration\\Tests\\": "packages/TypeDeclaration/tests",
"Rector\\ZendToSymfony\\Tests\\": "packages/ZendToSymfony/tests",
"Rector\\StrictCodeQuality\\Tests\\": "packages/StrictCodeQuality/tests"
"Rector\\StrictCodeQuality\\Tests\\": "packages/StrictCodeQuality/tests",
"Rector\\DynamicTypeAnalysis\\Tests\\": "packages/DynamicTypeAnalysis/tests"
},
"classmap": [
"packages/Symfony/tests/Rector/FrameworkBundle/AbstractToConstructorInjectionRectorSource",
@@ -5,7 +5,6 @@ services:
file_get_contents: ['Nette\Utils\FileSystem', 'read']
unlink: ['Nette\Utils\FileSystem', 'delete']
rmdir: ['Nette\Utils\FileSystem', 'delete']
file_put_contents: ['Nette\Utils\FileSystem', 'write']

# strings
Rector\Nette\Rector\NotIdentical\StrposToStringsContainsRector: ~
@@ -14,3 +13,4 @@ services:
Rector\Nette\Rector\Identical\EndsWithFunctionToNetteUtilsStringsRector: ~
Rector\Nette\Rector\FuncCall\PregFunctionToNetteUtilsStringsRector: ~
Rector\Nette\Rector\FuncCall\JsonDecodeEncodeToNetteUtilsJsonDecodeEncodeRector: ~
Rector\Nette\Rector\FuncCall\FilePutContentsToFileSystemWriteRector: ~
@@ -0,0 +1,8 @@
services:
_defaults:
autowire: true
public: true

Rector\DynamicTypeAnalysis\:
resource: '../src'
exclude: '../src/{Rector/**/*Rector.php}'
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Rector\DynamicTypeAnalysis\Probe;
use Nette\Utils\FileSystem;
final class ProbeStaticStorage
{
public static function getFile(): string
{
return sys_get_temp_dir() . '/_rector_type_probe.txt';
}
public static function clear(): void
{
FileSystem::delete(self::getFile());
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Rector\DynamicTypeAnalysis\Probe;
use Nette\Utils\FileSystem;
/**
* @see https://stackoverflow.com/a/39525458/1348344
*/
final class TypeStaticProbe
{
/**
* @var mixed[]
*/
private static $itemDataByMethodReferenceAndPosition = [];
/**
* @param mixed $value
*/
public static function recordArgumentType($value, string $method, int $argumentPosition): void
{
$probeItem = self::createProbeItem($value, $method, $argumentPosition);
self::recordProbeItem($probeItem);
}
/**
* @param mixed $value
*/
public static function createProbeItem($value, string $method, int $argumentPosition): string
{
$type = self::resolveValueTypeToString($value);
$data = [$type, $method, $argumentPosition];
return implode(';', $data) . PHP_EOL;
}
/**
* @param mixed $value
*/
public static function resolveValueTypeToString($value): string
{
if (is_object($value)) {
return 'object:' . get_class($value);
}
if (is_array($value)) {
// try to resolve single nested array types
$arrayValueTypes = [];
foreach ($value as $singleValue) {
$arrayValueTypes[] = self::resolveValueTypeToString($singleValue);
}
$arrayValueTypes = array_unique($arrayValueTypes);
$arrayValueTypes = implode('|', $arrayValueTypes);
return 'array:' . $arrayValueTypes;
}
return gettype($value);
}
/**
* @return mixed[]
*/
public static function getDataForMethodByPosition(string $methodReference): array
{
$probeItemData = self::provideItemDataByMethodReferenceAndPosition();
return $probeItemData[$methodReference] ?? [];
}
private static function recordProbeItem(string $probeItem): void
{
$storageFile = ProbeStaticStorage::getFile();
if (file_exists($storageFile)) {
// append
file_put_contents($storageFile, $probeItem, FILE_APPEND);
} else {
// 1st write
FileSystem::write($storageFile, $probeItem);
}
}
/**
* @return mixed[]
*/
private static function provideItemDataByMethodReferenceAndPosition(): array
{
if (self::$itemDataByMethodReferenceAndPosition !== []) {
return self::$itemDataByMethodReferenceAndPosition;
}
$probeFileContent = FileSystem::read(ProbeStaticStorage::getFile());
$probeItems = explode(PHP_EOL, $probeFileContent);
// remove empty values
$probeItems = array_filter($probeItems);
$itemData = [];
foreach ($probeItems as $probeItem) {
[$type, $methodReference, $position] = explode(';', $probeItem);
$itemData[$methodReference][$position][] = $type;
}
self::$itemDataByMethodReferenceAndPosition = $itemData;
return $itemData;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Rector\DynamicTypeAnalysis\Rector\ClassMethod;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Interface_;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Rector\ValueObject\PhpVersionFeature;
/**
* @see \Rector\DynamicTypeAnalysis\Tests\Rector\ClassMethod\AddArgumentTypeWithProbeDataRector\AddArgumentTypeWithProbeDataRectorTest
*/
abstract class AbstractArgumentProbeRector extends AbstractRector
{
protected function shouldSkipClassMethod(ClassMethod $classMethod): bool
{
// we need at least scalar types to make this work
if (! $this->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) {
return true;
}
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
// skip interfaces
if ($classNode instanceof Interface_) {
return true;
}
// only record public methods = they're the entry points + prevent duplicated type recording
if (! $classMethod->isPublic()) {
return true;
}
// we need some params to analyze
if (count((array) $classMethod->params) === 0) {
return true;
}
// method without body doesn't need analysis
return count((array) $classMethod->stmts) === 0;
}
protected function getClassMethodReference(ClassMethod $classMethod): ?string
{
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
if ($className === null) {
return null;
}
$methodName = $this->getName($classMethod->name);
if ($methodName === null) {
return null;
}
return $className . '::' . $methodName;
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Rector\DynamicTypeAnalysis\Rector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\DynamicTypeAnalysis\Probe\TypeStaticProbe;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see \Rector\DynamicTypeAnalysis\Tests\Rector\ClassMethod\AddArgumentTypeWithProbeDataRector\AddArgumentTypeWithProbeDataRectorTest
*/
final class AddArgumentTypeWithProbeDataRector extends AbstractArgumentProbeRector
{
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Add argument type based on probed data', [
new CodeSample(
<<<'PHP'
class SomeClass
{
public function run($arg)
{
}
}
PHP
,
<<<'PHP'
class SomeClass
{
public function run(string $arg)
{
}
}
PHP
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ClassMethod::class];
}
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($this->shouldSkipClassMethod($node)) {
return null;
}
$classMethodReference = $this->getClassMethodReference($node);
if ($classMethodReference === null) {
return null;
}
$methodData = TypeStaticProbe::getDataForMethodByPosition($classMethodReference);
// no data for this method 鈫 skip
if ($methodData === []) {
return null;
}
$this->completeTypesToClassMethodParams($node, $methodData);
return $node;
}
/**
* @param mixed[] $methodData
*/
private function completeTypesToClassMethodParams(ClassMethod $classMethod, array $methodData): void
{
foreach ($classMethod->params as $position => $param) {
// do not override existing types
if ($param->type !== null) {
continue;
}
// no data for this parameter 鈫 skip
if (! isset($methodData[$position])) {
continue;
}
$parameterData = $methodData[$position];
// uniquate data
$parameterData = array_unique($parameterData);
if (count($parameterData) > 1) {
// is union or nullable type?
// @todo
continue;
}
// single value 鈫 can we add it?
if (count($parameterData) === 1) {
$typeNode = $this->staticTypeMapper->mapStringToPhpParserNode($parameterData[0]);
if ($typeNode === null) {
continue;
}
$param->type = $typeNode;
}
}
}
}
ProTip! Use n and p to navigate between commits in a pull request.
You can鈥檛 perform that action at this time.