Skip to content

Commit

Permalink
Adding basic idea of Preload fixer
Browse files Browse the repository at this point in the history
  • Loading branch information
Nyholm committed Mar 21, 2020
1 parent f71cee9 commit eb8ec4c
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 0 deletions.
185 changes: 185 additions & 0 deletions src/Fixer/Preload/ExplicitlyLoadClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\Preload;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ExplicitlyLoadClass extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'Adds extra `class_exists` to help PHP 7.4 preloading.',
[
]
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return $tokens->isTokenKindFound(T_CLASS) || $tokens->isTokenKindFound(T_TRAIT);
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
$candidates = $this->parse($tokens, '__construct');
$classesNotToLoad = $this->getPreloadedClasses($file, $tokens);

$classesToLoad = array_diff($candidates, $classesNotToLoad);
$this->injectClasses($tokens, $classesToLoad);
}

/**
* @param string $functionName
*
* @return string[] classes
*/
private function parse(Tokens $tokens, $functionName)
{
$classes = [];
$index = $this->findFunction($tokens, $functionName);
// if not public, get the types.
// TODO there may be other keyword but public/private/protected, eg "static"
if ('public' !== $tokens[$tokens->getPrevMeaningfulToken($index)]->getContent()) {
// Get argument types
$startedParsingArguments = false;
for ($i = $index; $i < \count($tokens); ++$i) {
$token = $tokens[$i];
// Look for when the arguments begin
if ('(' === $token->getContent()) {
$startedParsingArguments = true;

continue;
}

// If we have not reached the arguments yet
if (!$startedParsingArguments) {
continue;
}

// Are all arguments parsed?
if (')' === $token->getContent()) {
break;
}

if ($token->isGivenKind(T_STRING) && !$token->isKeyword() && !\in_array($token->getContent(), ['string', 'bool', 'array', 'float', 'int'], true)) {
$classes[] = $token->getContent();
}
}
}

// TODO parse body.

return $classes;
}

/**
* Get classes that are found by the preloader. Ie classes we shouldn't include in `class_exists`.
*
* @return string[]
*/
private function getPreloadedClasses(\SplFileInfo $file, Tokens $tokens)
{
$classes = [];

foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_STRING)) {
continue;
}

// TODO rework so it is way better
if ('class_exists' === $token->getContent()) {
$nextToken = $tokens[$index + 2];
$classes[] = $nextToken->getContent();
}
}

return $classes;
}

/**
* Find a function in the tokens.
*
* @param string $name
*
* @return null|int the index or null. The index is to the "function" token.
*/
private function findFunction(Tokens $tokens, $name)
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_FUNCTION)) {
continue;
}

$nextTokenIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextTokenIndex];

if ($nextToken->getContent() !== $name) {
continue;
}

return $index;
}

return null;
}

private function injectClasses(Tokens $tokens, array $classes)
{
$insertAfter = null;
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_CLASS)) {
continue;
}

$insertAfter = $tokens->getPrevMeaningfulToken($index);

break;
}

if (null === $insertAfter) {
return;
}

$newTokens = [];
foreach ($classes as $class) {
//$newTokens[] = new Token([T_STRING, 'class_exists('.$class.'::class);'."\n"]);
$newTokens[] = new Token([T_STRING, 'class_exists']);
$newTokens[] = new Token('(');
$newTokens[] = new Token([T_STRING, $class]);
$newTokens[] = new Token([T_DOUBLE_COLON, '::']);
$newTokens[] = new Token([CT::T_CLASS_CONSTANT, 'class']);
$newTokens[] = new Token(')');
$newTokens[] = new Token(';');
$newTokens[] = new Token([T_WHITESPACE, "\n"]);
}

$tokens->insertAt($insertAfter + 2, $newTokens);
}
}
59 changes: 59 additions & 0 deletions tests/Fixer/Preload/ExplicitlyLoadClassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Fixer\Preload;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @internal
*
* @covers \PhpCsFixer\Fixer\Preload\ExplicitlyLoadClass
*/
final class ExplicitlyLoadClassTest extends AbstractFixerTestCase
{
/**
* @param string $expected
* @param null|string $input
*
* @dataProvider provideFixCases
*/
public function testFix($expected, $input = null)
{
$this->doTest($expected, $input);
}

public function provideFixCases()
{
$testDir = \dirname(__DIR__, 2).'/Fixtures/Preload';
$finder = new Finder();
$finder->in($testDir)->name('*_Out.php');

/** @var SplFileInfo $file */
foreach ($finder as $file) {
$path = $file->getRealPath();
$output = file_get_contents($path);

$input = null;
$inputFile = substr($path, 0, -7).'In.php';
if (is_file($inputFile)) {
$input = file_get_contents($inputFile);
}

yield $file->getFilename() => [$output, $input];
}
}
}
17 changes: 17 additions & 0 deletions tests/Fixtures/Preload/Case001_In.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

use App\Foo;

/**
* Private constructor will run class_exists on arguments.
*/
class Case001
{
private $foo;
private $bar;
private function __construct(Foo $foo, string $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}
}
18 changes: 18 additions & 0 deletions tests/Fixtures/Preload/Case001_Out.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use App\Foo;

class_exists(Foo::class);
/**
* Private constructor will run class_exists on arguments.
*/
class Case001
{
private $foo;
private $bar;
private function __construct(Foo $foo, string $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}
}
13 changes: 13 additions & 0 deletions tests/Fixtures/Preload/Case002_Out.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/**
* Test public constructor will not generate extra class_exists
*/
class Case002
{
private $foo;
public function __construct(Foo $foo)
{
$this->foo = $foo;
}
}

0 comments on commit eb8ec4c

Please sign in to comment.