* * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Symfony\CS\Fixer\Contrib; use Symfony\CS\AbstractFixer; use Symfony\CS\Tokenizer\Token; use Symfony\CS\Tokenizer\Tokens; /** * @author Matteo Beccati */ class Php4ConstructorFixer extends AbstractFixer { /** * {@inheritdoc} */ public function fix(\SplFileInfo $file, $content) { $tokens = Tokens::fromCode($content); $classes = array_keys($tokens->findGivenKind(T_CLASS)); $numClasses = count($classes); for ($i = 0; $i < $numClasses; ++$i) { $index = $classes[$i]; // is it inside a namespace? $nspIndex = $tokens->getPrevTokenOfKind($index, array(array(T_NAMESPACE, 'namespace'))); if (null !== $nspIndex) { $nspIndex = $tokens->getNextMeaningfulToken($nspIndex); // make sure it's not the global namespace, as PHP4 constructors are allowed in there if (!$tokens[$nspIndex]->equals('{')) { // unless it's the global namespace, the index currently points to the name $nspIndex = $tokens->getNextTokenOfKind($nspIndex, array(';', '{')); if ($tokens[$nspIndex]->equals(';')) { // the class is inside a (non-block) namespace, no PHP4-code should be in there break; } // the index points to the { of a block-namespace $nspEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nspIndex); if ($index < $nspEnd) { // the class is inside a block namespace, skip other classes that might be in it for ($j = $i + 1; $j < $numClasses; ++$j) { if ($classes[$j] < $nspEnd) { ++$i; } } // and continue checking the classes that might follow continue; } } } $classNameIndex = $tokens->getNextMeaningfulToken($index); $className = $tokens[$classNameIndex]->getContent(); $classStart = $tokens->getNextTokenOfKind($classNameIndex, array('{')); $classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStart); $this->fixConstructor($tokens, $className, $classStart, $classEnd); $this->fixParent($tokens, $classStart, $classEnd); } return $tokens->generateCode(); } /** * {@inheritdoc} */ public function getName() { return 'php4_constructor'; } /** * {@inheritdoc} */ public function getDescription() { return 'Convert PHP4-style constructors to __construct. Warning! This could change code behavior.'; } /** * Fix constructor within a class, if possible. * * @param Tokens $tokens the Tokens instance * @param string $className the class name * @param int $classStart the class start index * @param int $classEnd the class end index */ private function fixConstructor(Tokens $tokens, $className, $classStart, $classEnd) { $php4 = $this->findFunction($tokens, $className, $classStart, $classEnd); if (null === $php4) { // no PHP4-constructor! return; } if (!empty($php4['modifiers'][T_ABSTRACT]) || !empty($php4['modifiers'][T_STATIC])) { // PHP4 constructor can't be abstract or static return; } $php5 = $this->findFunction($tokens, '__construct', $classStart, $classEnd); if (null === $php5) { // no PHP5-constructor, we can rename the old one to __construct $tokens[$php4['nameIndex']]->setContent('__construct'); // in some (rare) cases we might have just created an infinite recursion issue $this->fixInfiniteRecursion($tokens, $php4['bodyIndex'], $php4['endIndex']); return; } // does the PHP4-constructor only call $this->__construct($args, ...)? list($seq, $case) = $this->getWrapperMethodSequence($tokens, '__construct', $php4['startIndex'], $php4['bodyIndex']); if (null !== $tokens->findSequence($seq, $php4['bodyIndex'] - 1, $php4['endIndex'], $case)) { // good, delete it! for ($i = $php4['startIndex']; $i <= $php4['endIndex']; ++$i) { $tokens[$i]->clear(); } return; } // does __construct only call the PHP4-constructor (with the same args)? list($seq, $case) = $this->getWrapperMethodSequence($tokens, $className, $php4['startIndex'], $php4['bodyIndex']); if (null !== $tokens->findSequence($seq, $php5['bodyIndex'] - 1, $php5['endIndex'], $case)) { // that was a weird choice, but we can safely delete it and... for ($i = $php5['startIndex']; $i <= $php5['endIndex']; ++$i) { $tokens[$i]->clear(); } // rename the PHP4 one to __construct $tokens[$php4['nameIndex']]->setContent('__construct'); } } /** * Fix calls to the parent constructor within a class. * * @param Tokens $tokens the Tokens instance * @param int $classStart the class start index * @param int $classEnd the class end index */ private function fixParent(Tokens $tokens, $classStart, $classEnd) { // check calls to the parent constructor foreach ($tokens->findGivenKind(T_EXTENDS) as $index => $token) { $parentIndex = $tokens->getNextMeaningfulToken($index); $parentClass = $tokens[$parentIndex]->getContent(); // using parent::ParentClassName() or ParentClassName::ParentClassName() $parentSeq = $tokens->findSequence(array( array(T_STRING), array(T_DOUBLE_COLON), array(T_STRING, $parentClass), '(', ), $classStart, $classEnd, array(2 => false)); if (null !== $parentSeq) { // we only need indexes $parentSeq = array_keys($parentSeq); // match either of the possibilities if ($tokens[$parentSeq[0]]->equalsAny(array(array(T_STRING, 'parent'), array(T_STRING, $parentClass)), false)) { // replace with parent::__construct $tokens[$parentSeq[0]]->setContent('parent'); $tokens[$parentSeq[2]]->setContent('__construct'); } } // using $this->ParentClassName() $parentSeq = $tokens->findSequence(array( array(T_VARIABLE, '$this'), array(T_OBJECT_OPERATOR), array(T_STRING, $parentClass), '(', ), $classStart, $classEnd, array(2 => false)); if (null !== $parentSeq) { // we only need indexes $parentSeq = array_keys($parentSeq); // replace call with parent::__construct() $tokens[$parentSeq[0]] = new Token(array( T_STRING, 'parent', $tokens[$parentSeq[0]]->getLine(), )); $tokens[$parentSeq[1]] = new Token(array( T_DOUBLE_COLON, '::', $tokens[$parentSeq[1]]->getLine(), )); $tokens[$parentSeq[2]]->setContent('__construct'); } } } /** * Fix a particular infinite recursion issue happening when the parent class has __construct and the child has only * a PHP4 constructor that calls the parent constructor as $this->__construct(). * * @param Tokens $tokens the Tokens instance * @param int $start the PHP4 constructor body start * @param int $end the PHP4 constructor body end */ private function fixInfiniteRecursion(Tokens $tokens, $start, $end) { $seq = array( array(T_VARIABLE, '$this'), array(T_OBJECT_OPERATOR), array(T_STRING, '__construct'), ); while (true) { $callSeq = $tokens->findSequence($seq, $start, $end, array(2 => false)); if (null === $callSeq) { return; } $callSeq = array_keys($callSeq); $tokens[$callSeq[0]] = new Token(array( T_STRING, 'parent', $tokens[$callSeq[0]]->getLine(), )); $tokens[$callSeq[1]] = new Token(array( T_DOUBLE_COLON, '::', $tokens[$callSeq[1]]->getLine(), )); } } /** * Generate the sequence of tokens necessary for the body of a wrapper method that simply * calls $this->{$method}( [args...] ) with the same arguments as its own signature. * * @param Tokens $tokens the Tokens instance * @param string $method the wrapped method name * @param int $startIndex function/method start index * @param int $bodyIndex function/method body index * * @return array an array containing the sequence and case sensitiveness [ 0 => $seq, 1 => $case ] */ private function getWrapperMethodSequence(Tokens $tokens, $method, $startIndex, $bodyIndex) { // initialise sequence as { $this->{$method}( $seq = array( '{', array(T_VARIABLE, '$this'), array(T_OBJECT_OPERATOR), array(T_STRING, $method), '(', ); $case = array(3 => false); // parse method parameters, if any $index = $startIndex; while (true) { // find the next variable name $index = $tokens->getNextTokenOfKind($index, array(array(T_VARIABLE))); if (null === $index || $index >= $bodyIndex) { // we've reached the body already break; } // append a comma if it's not the first variable if (count($seq) > 5) { $seq[] = ','; } // append variable name to the sequence $seq[] = array(T_VARIABLE, $tokens[$index]->getContent()); } // almost done, close the sequence with ); } $seq[] = ')'; $seq[] = ';'; $seq[] = '}'; return array($seq, $case); } /** * Find a function or method matching a given name within certain bounds. * * @param Tokens $tokens the Tokens instance * @param string $name the function/Method name * @param int $startIndex the search start index * @param int $endIndex the search end index * * @return array|null An associative array, if a match is found: * * - nameIndex (int): The index of the function/method name. * - startIndex (int): The index of the function/method start. * - endIndex (int): The index of the function/method end. * - bodyIndex (int): The index of the function/method body. * - modifiers (array): The modifiers as array keys and their index as * the values, e.g. array(T_PUBLIC => 10). */ private function findFunction(Tokens $tokens, $name, $startIndex, $endIndex) { $function = $tokens->findSequence(array( array(T_FUNCTION), array(T_STRING, $name), '(', ), $startIndex, $endIndex, false); if (null === $function) { return; } // keep only the indexes $function = array_keys($function); // find previous block, saving method modifiers for later use $possibleModifiers = array(T_PUBLIC, T_PROTECTED, T_PRIVATE, T_STATIC, T_ABSTRACT, T_FINAL); $modifiers = array(); $prevBlock = $tokens->getPrevMeaningfulToken($function[0]); while (null !== $prevBlock && $tokens[$prevBlock]->isGivenKind($possibleModifiers)) { $modifiers[$tokens[$prevBlock]->getId()] = $prevBlock; $prevBlock = $tokens->getPrevMeaningfulToken($prevBlock); } if (isset($modifiers[T_ABSTRACT])) { // abstract methods have no body $bodyStart = null; $funcEnd = $tokens->getNextTokenOfKind($function[2], array(';')); } else { // find method body start and the end of the function definition $bodyStart = $tokens->getNextTokenOfKind($function[2], array('{')); $funcEnd = $bodyStart !== null ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $bodyStart) : null; } return array( 'nameIndex' => $function[1], 'startIndex' => $prevBlock + 1, 'endIndex' => $funcEnd, 'bodyIndex' => $bodyStart, 'modifiers' => $modifiers, ); } }